Actions
FAB and extended FAB
The floating action button holds the screen's most important action. Three sizes and six colors; the extended FAB adds a label and collapses on scroll.
- COMPONENT_SPEC §5
- CSS · React
- Open in the playground
Examples
FAB sizes
Baseline (56dp, default), medium and large. A FAB shows only an icon, so it needs an aria-label.
import { Fab, Icon } from '@kyralabs/m3exp-web/react';
export default function Fabs() {
return (
<>
<Fab icon={<Icon name="edit" />} aria-label="Compose" />
<Fab
size="medium"
color="secondary-container"
icon={<Icon name="edit" />}
aria-label="Compose"
/>
<Fab
size="large"
color="tertiary-container"
icon={<Icon name="edit" />}
aria-label="Compose"
/>
</>
);
}
<button type="button" class="m3e-fab" aria-label="Compose">
<span class="m3e-icon" aria-hidden="true">edit</span>
</button>
<button
type="button"
class="m3e-fab m3e-fab--size-medium m3e-fab--secondary-container"
aria-label="Compose"
>
<span class="m3e-icon" aria-hidden="true">edit</span>
</button>
<button
type="button"
class="m3e-fab m3e-fab--size-large m3e-fab--tertiary-container"
aria-label="Compose"
>
<span class="m3e-icon" aria-hidden="true">edit</span>
</button>
Run the HTML version (CSS classes only).
Colors
Primary, secondary and tertiary, each as a container color or the solid color.
import { Fab, Icon } from '@kyralabs/m3exp-web/react';
const COLORS = [
'primary-container',
'secondary-container',
'tertiary-container',
'primary',
'secondary',
'tertiary',
] as const;
export default function Colors() {
return (
<>
{COLORS.map((color) => (
<Fab
key={color}
color={color}
icon={<Icon name="add" />}
aria-label={`Create (${color})`}
/>
))}
</>
);
}
<button type="button" class="m3e-fab" aria-label="Create (primary-container)">
<span class="m3e-icon" aria-hidden="true">add</span>
</button>
<button
type="button"
class="m3e-fab m3e-fab--secondary-container"
aria-label="Create (secondary-container)"
>
<span class="m3e-icon" aria-hidden="true">add</span>
</button>
<button
type="button"
class="m3e-fab m3e-fab--tertiary-container"
aria-label="Create (tertiary-container)"
>
<span class="m3e-icon" aria-hidden="true">add</span>
</button>
<button type="button" class="m3e-fab m3e-fab--primary" aria-label="Create (primary)">
<span class="m3e-icon" aria-hidden="true">add</span>
</button>
<button type="button" class="m3e-fab m3e-fab--secondary" aria-label="Create (secondary)">
<span class="m3e-icon" aria-hidden="true">add</span>
</button>
<button type="button" class="m3e-fab m3e-fab--tertiary" aria-label="Create (tertiary)">
<span class="m3e-icon" aria-hidden="true">add</span>
</button>
Run the HTML version (CSS classes only).
Small FAB (deprecated)
Deprecated (D4): m3.material.io no longer recommends the small FAB (40dp; its touch target still reaches 48dp). size='small' ships until 2.0 and warns once in development.
import { Fab, Icon } from '@kyralabs/m3exp-web/react';
/** Deprecated (D4): the small FAB is no longer recommended; use the baseline FAB. */
export default function LegacySmall() {
return <Fab size="small" icon={<Icon name="edit" />} aria-label="Compose" />;
}
<!-- Deprecated (D4): the small FAB is no longer recommended; use the baseline FAB. -->
<button type="button" class="m3e-fab m3e-fab--size-small" aria-label="Compose">
<span class="m3e-icon" aria-hidden="true">edit</span>
</button>
Run the HTML version (CSS classes only).
Extended FAB
An icon and a label. isExpanded={false} collapses it to the icon on a spring; the label stays its accessible name.
import { useState } from 'react';
import { ExtendedFab, Icon, Switch } from '@kyralabs/m3exp-web/react';
export default function Extended() {
const [expanded, setExpanded] = useState(true);
return (
<>
<ExtendedFab icon={<Icon name="edit" />} label="Compose" isExpanded={expanded} />
<ExtendedFab
size="medium"
color="secondary"
icon={<Icon name="navigation" />}
label="Navigate"
/>
<Switch isSelected={expanded} onChange={setExpanded}>
Expanded
</Switch>
</>
);
}
<button type="button" class="m3e-extended-fab">
<span class="m3e-icon" aria-hidden="true">edit</span>
<span class="m3e-extended-fab__label">Compose</span>
</button>
<button
type="button"
class="m3e-extended-fab m3e-extended-fab--size-medium m3e-extended-fab--secondary"
>
<span class="m3e-icon" aria-hidden="true">navigation</span>
<span class="m3e-extended-fab__label">Navigate</span>
</button>
<button type="button" class="m3e-extended-fab m3e-extended-fab--collapsed">
<span class="m3e-icon" aria-hidden="true">edit</span>
<span class="m3e-extended-fab__label">Compose</span>
</button>
Run the HTML version (CSS classes only).
API
React
<Fab>
Floating action button (COMPONENT_SPEC §5; Compose FloatingActionButton, MediumFloatingActionButton, LargeFloatingActionButton): React Aria behavior on the m3e-css .m3e-fab classes. Elevation follows the ElevationDefaults tweens in CSS.
| Prop | Type | Default | Description |
|---|---|---|---|
icon
required
|
ReactNode
|
The icon, e.g. <Icon name="add" />.
|
|
size
|
"small" | "medium" | "large" | "baseline"
|
baseline |
Default baseline (56dp). 'small' is deprecated (D4) and warns once in development.
|
aria-label
|
string
|
Required unless aria-labelledby names the button: a FAB shows only an icon.
|
|
color
|
"primary-container" | "secondary-container" | "tertiary-container" | "primary" | "secondary" | "tertiary"
|
||
className
|
string
|
||
type
|
'button' | 'submit' | 'reset'
|
||
onPress
|
(e: PressEvent) => void
|
||
onPressStart
|
(e: PressEvent) => void
|
||
onPressEnd
|
(e: PressEvent) => void
|
Also accepts HTML attributes not listed here.
<ExtendedFab>
Extended FAB (COMPONENT_SPEC §5; Compose Small/Medium/LargeExtendedFloatingActionButton): React Aria behavior on the m3e-css .m3e-extended-fab classes. isExpanded={false} collapses it to the icon with an interruptible width spring; the label stays the accessible name.
| Prop | Type | Default | Description |
|---|---|---|---|
label
required
|
ReactNode
|
Visible label; it is also the button's accessible name, collapsed or not. | |
icon
|
ReactNode
|
Leading icon, e.g. <Icon name="add" />. Optional; collapsing needs one.
|
|
size
|
"small" | "medium" | "large"
|
small |
Default small (56dp).
|
isExpanded
|
boolean
|
Default true. false collapses to the icon (a FAB-sized square) with an interruptible width
spring; ignored without an icon. See useExtendedFabScroll for scroll-driven collapse.
|
|
color
|
"primary-container" | "secondary-container" | "tertiary-container" | "primary" | "secondary" | "tertiary"
|
||
className
|
string
|
||
type
|
'button' | 'submit' | 'reset'
|
||
onPress
|
(e: PressEvent) => void
|
||
onPressStart
|
(e: PressEvent) => void
|
||
onPressEnd
|
(e: PressEvent) => void
|
||
expanded
|
boolean
|
Deprecated:
Since 1.8: use isExpanded. Removed in 2.0.
Default true; false collapses to the icon.
|
Also accepts HTML attributes not listed here.
useExtendedFabScroll()
Scroll-driven expanded for <ExtendedFab> (COMPONENT_SPEC §5 "scroll-collapse"). The Compose samples (AnimatedExtendedFloatingActionButtonSample, FloatingActionButtonSamples.kt) expand the FAB only while the list is at its start (firstVisibleItemIndex == 0); m3.material.io also shows it expanding again on scroll up (expandOnScrollUp).
fabClassName()
Classes for a FAB (the m3e-css contract).
extendedFabClassName()
Classes for an extended FAB (the m3e-css contract).
CSS
fab.css
@import '@kyralabs/m3e-css/fab.css';
FAB and extended FAB (COMPONENT_SPEC §5). One file: the spec treats them as one component, and they share Compose's private FloatingActionButton (FloatingActionButton.kt L155-190): the same container colors, elevation and state rules. No JavaScript needed; m3e-react springs the extended FAB's width (interruptible, velocity kept) on the same classes.
<button class="m3e-fab m3e-fab--size-medium" aria-label="Create">
<span class="m3e-icon" aria-hidden="true">add</span>
</button>
<button class="m3e-extended-fab m3e-extended-fab--size-medium">
<span class="m3e-icon" aria-hidden="true">add</span>
<span class="m3e-extended-fab__label">Create</span>
</button>
FAB sizes: baseline (default) --size-medium --size-large; --size-small is deprecated (D4: m3.material.io "The small FAB is no longer recommended"). Extended FAB sizes: small (default) --size-medium --size-large. --collapsed shows the extended FAB icon only; the label stays in the accessibility tree (opacity 0), so it remains the button's name. Colors (both blocks): primary container (default) --secondary-container --tertiary-container --primary --secondary --tertiary.
70 custom properties it reads
| Custom property | Default |
|---|---|
--m3e-comp-elevation-incoming-duration
|
120ms |
--m3e-comp-elevation-incoming-easing
|
cubic-bezier(0.4, 0, 0.2, 1) |
--m3e-comp-elevation-outgoing-duration-hovered
|
120ms |
--m3e-comp-elevation-outgoing-easing
|
cubic-bezier(0.4, 0, 0.6, 1) |
--m3e-comp-extended-fab-large-container-height
|
96px |
--m3e-comp-extended-fab-large-container-shape
|
var(--m3e-sys-shape-corner-extra-large) |
--m3e-comp-extended-fab-large-leading-space
|
28px |
--m3e-comp-extended-fab-large-trailing-space
|
28px |
--m3e-comp-extended-fab-medium-container-height
|
80px |
--m3e-comp-extended-fab-medium-icon-size
|
28px |
--m3e-comp-extended-fab-medium-leading-space
|
26px |
--m3e-comp-extended-fab-medium-trailing-space
|
26px |
--m3e-comp-extended-fab-primary-label-text-font
|
var(--m3e-sys-typescale-label-large-font) |
--m3e-comp-extended-fab-primary-label-text-line-height
|
var(--m3e-sys-typescale-label-large-line-height) |
--m3e-comp-extended-fab-primary-label-text-size
|
var(--m3e-sys-typescale-label-large-size) |
--m3e-comp-extended-fab-primary-label-text-tracking
|
var(--m3e-sys-typescale-label-large-tracking) |
--m3e-comp-extended-fab-primary-label-text-weight
|
var(--m3e-sys-typescale-label-large-weight) |
--m3e-comp-extended-fab-small-container-height
|
56px |
--m3e-comp-extended-fab-small-container-shape
|
var(--m3e-sys-shape-corner-large) |
--m3e-comp-extended-fab-small-icon-label-space
|
8px |
--m3e-comp-extended-fab-small-icon-size
|
24px |
--m3e-comp-extended-fab-small-leading-space
|
16px |
--m3e-comp-extended-fab-small-trailing-space
|
16px |
--m3e-comp-fab-baseline-container-height
|
56px |
--m3e-comp-fab-baseline-container-shape
|
var(--m3e-sys-shape-corner-large) |
--m3e-comp-fab-baseline-container-width
|
56px |
--m3e-comp-fab-baseline-icon-size
|
24px |
--m3e-comp-fab-defaults-extended-label-opacity-duration
|
var(--m3e-sys-motion-spring-fast-effects-duration) |
--m3e-comp-fab-defaults-extended-label-opacity-easing
|
var(--m3e-sys-motion-spring-fast-effects-easing) |
--m3e-comp-fab-defaults-extended-width-duration
|
var(--m3e-sys-motion-spring-fast-spatial-duration) |
--m3e-comp-fab-defaults-extended-width-easing
|
var(--m3e-sys-motion-spring-fast-spatial-easing) |
--m3e-comp-fab-defaults-large-extended-icon-padding
|
16px |
--m3e-comp-fab-defaults-large-extended-label-font
|
var(--m3e-sys-typescale-headline-small-font) |
--m3e-comp-fab-defaults-large-extended-label-line-height
|
var(--m3e-sys-typescale-headline-small-line-height) |
--m3e-comp-fab-defaults-large-extended-label-size
|
var(--m3e-sys-typescale-headline-small-size) |
--m3e-comp-fab-defaults-large-extended-label-tracking
|
var(--m3e-sys-typescale-headline-small-tracking) |
--m3e-comp-fab-defaults-large-extended-label-weight
|
var(--m3e-sys-typescale-headline-small-weight) |
--m3e-comp-fab-defaults-large-icon-size
|
36px |
--m3e-comp-fab-defaults-medium-extended-icon-padding
|
12px |
--m3e-comp-fab-defaults-medium-extended-label-font
|
var(--m3e-sys-typescale-title-large-font) |
--m3e-comp-fab-defaults-medium-extended-label-line-height
|
var(--m3e-sys-typescale-title-large-line-height) |
--m3e-comp-fab-defaults-medium-extended-label-size
|
var(--m3e-sys-typescale-title-large-size) |
--m3e-comp-fab-defaults-medium-extended-label-tracking
|
var(--m3e-sys-typescale-title-large-tracking) |
--m3e-comp-fab-defaults-medium-extended-label-weight
|
var(--m3e-sys-typescale-title-large-weight) |
--m3e-comp-fab-defaults-medium-extended-shape
|
var(--m3e-sys-shape-corner-large-increased) |
--m3e-comp-fab-defaults-medium-shape
|
var(--m3e-sys-shape-corner-large-increased) |
--m3e-comp-fab-defaults-small-extended-label-font
|
var(--m3e-sys-typescale-title-medium-font) |
--m3e-comp-fab-defaults-small-extended-label-line-height
|
var(--m3e-sys-typescale-title-medium-line-height) |
--m3e-comp-fab-defaults-small-extended-label-size
|
var(--m3e-sys-typescale-title-medium-size) |
--m3e-comp-fab-defaults-small-extended-label-tracking
|
var(--m3e-sys-typescale-title-medium-tracking) |
--m3e-comp-fab-defaults-small-extended-label-weight
|
var(--m3e-sys-typescale-title-medium-weight) |
--m3e-comp-fab-large-container-height
|
96px |
--m3e-comp-fab-large-container-shape
|
var(--m3e-sys-shape-corner-extra-large) |
--m3e-comp-fab-large-container-width
|
96px |
--m3e-comp-fab-medium-container-height
|
80px |
--m3e-comp-fab-medium-container-width
|
80px |
--m3e-comp-fab-medium-icon-size
|
28px |
--m3e-comp-fab-primary-container-container-color
|
var(--m3e-sys-color-primary-container) |
--m3e-comp-fab-primary-container-container-shadow
|
var(--m3e-sys-elevation-level3-shadow) |
--m3e-comp-fab-primary-container-container-shadow-focused
|
var(--m3e-sys-elevation-level3-shadow) |
--m3e-comp-fab-primary-container-container-shadow-hovered
|
var(--m3e-sys-elevation-level4-shadow) |
--m3e-comp-fab-primary-container-container-shadow-pressed
|
var(--m3e-sys-elevation-level3-shadow) |
--m3e-comp-fab-primary-container-icon-color
|
var(--m3e-sys-color-on-primary-container) |
--m3e-comp-fab-secondary-container-container-color
|
var(--m3e-sys-color-secondary-container) |
--m3e-comp-fab-secondary-container-icon-color
|
var(--m3e-sys-color-on-secondary-container) |
--m3e-comp-fab-small-container-height
|
40px |
--m3e-comp-fab-small-container-shape
|
var(--m3e-sys-shape-corner-medium) |
--m3e-comp-fab-small-container-width
|
40px |
--m3e-comp-fab-small-icon-size
|
24px |
--m3e-comp-minimum-interactive-component-size
|
48px |
Accessibility
<button aria-label>; extended FAB label is its name (also when collapsed).
Every interactive part has a target of at least 48 × 48 px, a visible focus ring for
keyboard focus only, and works under forced-colors and
prefers-reduced-motion.
Sources
Values come from androidx Compose Material3 1.5.0-beta01 at 19991f2c and from
m3.material.io, as cited below from COMPONENT_SPEC §5 and the m3e-css sources.
FloatingActionButton.kt, Fab*Tokens.kt, ExtendedFab*Tokens.kt, Shapes.kt (ShapeDefaults), FloatingActionButtonSamples.kt; m3.material.io FAB and Extended FAB pages (color styles, scroll behavior).
Fab{Small,Baseline,Medium,Large}Tokens.kt, ExtendedFab{Small,Medium,Large,Primary}Tokens.kt, FabPrimaryContainerTokens.kt@19991f2c plus the FloatingActionButtonDefaults / extended FAB values Compose renders (components/fab-defaults.css; SOURCES register #23).
Elevation (FloatingActionButtonDefaults.elevation()): Level3, hovered Level4, focused and pressed Level3. No disabled state (Compose FABs have none).
fab.css sources
(compose/material3 @ 19991f2c):
- FAB sizes: Fab{Small,Baseline,Medium,Large}Tokens. Medium shape: FloatingActionButtonDefaults .mediumShape = ShapeDefaults.LargeIncreased (L1022; FabMediumTokens has no shape yet). Large icon: FloatingActionButtonDefaults.LargeIconSize 36dp (L1010; upstream: FabLargeTokens.IconSize "is incorrect"); medium icon: MediumIconSize = FabMediumTokens.IconSize (L1007). Compose's Icon takes these sizes in the samples (FloatingActionButtonSamples.kt), so the catalog renders them.
- Extended FAB (expressive Small/Medium/LargeExtendedFloatingActionButton, L668-836, L930-1000): ExtendedFab{Small,Medium,Large}Tokens height, shape, leading/trailing space and icon size; minimum width = container height (L1434-1462); icon-label padding: small token, medium 12dp and large 16dp literals (L1456, L1469; upstream TODOs); label style titleMedium / titleLarge / headlineSmall (L1444, L1457, L1470); medium shape LargeIncreased (L1038). All in fab-defaults.css. Large extended icons use LargeIconSize, as the samples do.
- Expand/collapse (L961-998): width = lerp(container height, full row width, progress) on the fast spatial spring (L963); label alpha on the fast effects spring (L964); the row keeps its full width, starts at the inline start and is clipped by the container. Here: the label track goes 0fr ↔ 1fr (a fr track below 1 sizes to that fraction of its content, so the width lerps) with the springs' linear() curves; m3e-react drives the exact lerp with interruptible springs.
- Colors: FloatingActionButtonDefaults.containerColor = FabPrimaryContainerTokens; secondary container: FabSecondaryContainerTokens; the other styles use contentColorFor(container) (ColorScheme.kt L1096-1105), the pairs m3.material.io lists (FAB › Specs › Color styles). The state layer takes the content color (m3.material.io FAB › Specs › States).
- Elevation: FloatingActionButtonDefaults.elevation() (L1060) = FabPrimaryContainerTokens: Level3, hovered Level4, focused and pressed Level3; animated with the ElevationDefaults tweens (L1424 → internal/Elevation.kt, elevation-motion.css). Compose FABs have no disabled state.
- Touch target: ≥ LocalMinimumInteractiveComponentSize via ::after (only the small FAB is below); ::before is the state layer.