Skip to content

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.

Examples

FAB sizes

Baseline (56dp, default), medium and large. A FAB shows only an icon, so it needs an aria-label.

React HTML
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.

React HTML
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.

React HTML
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.

React HTML
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.