Skip to content

Actions

Button

Buttons start the actions people take most: save, send, continue. Five color configurations from high to low emphasis, five sizes, round or square, and toggles.

Examples

Color configurations

Filled for the main action, tonal and elevated for secondary ones, outlined and text for low emphasis.

React HTML
import { Button } from '@kyralabs/m3exp-web/react';

export default function Variants() {
  return (
    <>
      <Button variant="filled">Filled</Button>
      <Button variant="tonal">Tonal</Button>
      <Button variant="elevated">Elevated</Button>
      <Button variant="outlined">Outlined</Button>
      <Button variant="text">Text</Button>
    </>
  );
}
<button type="button" class="m3e-button m3e-button--filled">Filled</button>
<button type="button" class="m3e-button m3e-button--tonal">Tonal</button>
<button type="button" class="m3e-button m3e-button--elevated">Elevated</button>
<button type="button" class="m3e-button m3e-button--outlined">Outlined</button>
<button type="button" class="m3e-button m3e-button--text">Text</button>

Run the HTML version (CSS classes only).

Sizes

Extra small to extra large. Small (40dp) is the default; label and icon grow with the size.

React HTML
import { Button, Icon } from '@kyralabs/m3exp-web/react';

export default function Sizes() {
  return (
    <>
      <Button size="xs" icon={<Icon name="add" />}>
        Extra small
      </Button>
      <Button size="s" icon={<Icon name="add" />}>
        Small
      </Button>
      <Button size="m" icon={<Icon name="add" />}>
        Medium
      </Button>
      <Button size="l" icon={<Icon name="add" />}>
        Large
      </Button>
      <Button size="xl" icon={<Icon name="add" />}>
        Extra large
      </Button>
    </>
  );
}
<button type="button" class="m3e-button m3e-button--filled m3e-button--size-xs">
  <span class="m3e-icon" aria-hidden="true">add</span>
  Extra small
</button>
<button type="button" class="m3e-button m3e-button--filled m3e-button--size-s">
  <span class="m3e-icon" aria-hidden="true">add</span>
  Small
</button>
<button type="button" class="m3e-button m3e-button--filled m3e-button--size-m">
  <span class="m3e-icon" aria-hidden="true">add</span>
  Medium
</button>
<button type="button" class="m3e-button m3e-button--filled m3e-button--size-l">
  <span class="m3e-icon" aria-hidden="true">add</span>
  Large
</button>
<button type="button" class="m3e-button m3e-button--filled m3e-button--size-xl">
  <span class="m3e-icon" aria-hidden="true">add</span>
  Extra large
</button>

Run the HTML version (CSS classes only).

Shapes

Round (default) or square. Press any button: its corners morph on a spring.

React HTML
import { Button } from '@kyralabs/m3exp-web/react';

export default function Shapes() {
  return (
    <>
      <Button variant="tonal" size="m" shape="round">
        Round
      </Button>
      <Button variant="tonal" size="m" shape="square">
        Square
      </Button>
    </>
  );
}
<button type="button" class="m3e-button m3e-button--tonal m3e-button--size-m">Round</button>
<button type="button" class="m3e-button m3e-button--tonal m3e-button--size-m m3e-button--square">
  Square
</button>

Run the HTML version (CSS classes only).

Icons

A leading icon or a trailingIcon.

React HTML
import { Button, Icon } from '@kyralabs/m3exp-web/react';

export default function Icons() {
  return (
    <>
      <Button variant="filled" icon={<Icon name="send" />}>
        Send
      </Button>
      <Button variant="outlined" trailingIcon={<Icon name="arrow_forward" />}>
        Next
      </Button>
    </>
  );
}
<button type="button" class="m3e-button m3e-button--filled">
  <span class="m3e-icon" aria-hidden="true">send</span>
  Send
</button>
<button type="button" class="m3e-button m3e-button--outlined">
  Next
  <span class="m3e-icon" aria-hidden="true">arrow_forward</span>
</button>

Run the HTML version (CSS classes only).

Toggle buttons

ToggleButton adds aria-pressed; selected round buttons turn square and change color.

React HTML
import { ToggleButton } from '@kyralabs/m3exp-web/react';

export default function Toggle() {
  return (
    <>
      <ToggleButton variant="filled">Filled</ToggleButton>
      <ToggleButton variant="tonal" defaultSelected>
        Tonal
      </ToggleButton>
      <ToggleButton variant="elevated">Elevated</ToggleButton>
      <ToggleButton variant="outlined" defaultSelected>
        Outlined
      </ToggleButton>
    </>
  );
}
<button type="button" class="m3e-button m3e-button--filled" aria-pressed="false">Filled</button>
<button type="button" class="m3e-button m3e-button--tonal" aria-pressed="true">Tonal</button>
<button type="button" class="m3e-button m3e-button--elevated" aria-pressed="false">Elevated</button>
<button type="button" class="m3e-button m3e-button--outlined" aria-pressed="true">Outlined</button>

Run the HTML version (CSS classes only).

Disabled and links

Disabled buttons leave the tab order unless focusableWhenDisabled; with href the button renders a link.

Link
React HTML
import { Button } from '@kyralabs/m3exp-web/react';

export default function States() {
  return (
    <>
      <Button variant="filled" isDisabled>
        Disabled
      </Button>
      <Button variant="outlined" isDisabled>
        Disabled
      </Button>
      <Button variant="tonal" href="/">
        Link
      </Button>
    </>
  );
}
<button type="button" class="m3e-button m3e-button--filled" disabled>Disabled</button>
<button type="button" class="m3e-button m3e-button--outlined" disabled>Disabled</button>
<a class="m3e-button m3e-button--tonal" href="/">Link</a>

Run the HTML version (CSS classes only).

Usage

Prominent actions. Use text/outlined for low emphasis. Not for navigation (links styled as buttons only for in-page CTAs).

API

React

<Button>

Material 3 Expressive button (COMPONENT_SPEC §1): React Aria behavior on the m3e-css .m3e-button classes, with the press morph as an interruptible spring.

Prop Type Default Description
href string Renders a link (<a class="m3e-button">) instead of a <button>.
focusableWhenDisabled boolean Keeps a disabled button in the tab order (aria-disabled instead of disabled), so it can still show a tooltip or explain why it is unavailable.
variant "elevated" | "filled" | "tonal" | "outlined" | "text" filled Color configuration. Default filled.
size "xs" | "s" | "m" | "l" | "xl" s Default s (40dp).
shape "round" | "square" round Resting shape. Default round.
icon ReactNode Leading icon, e.g. <Icon name="add" />.
trailingIcon ReactNode Trailing icon.
target AnchorHTMLAttributes<HTMLAnchorElement>['target']
rel string
type 'button' | 'submit' | 'reset'
isDisabled boolean
children ReactNode
className string
onPress (e: PressEvent) => void
onPressStart (e: PressEvent) => void
onPressEnd (e: PressEvent) => void

Also accepts HTML attributes not listed here.

<ToggleButton>

Toggle button (COMPONENT_SPEC §1, Compose ToggleButton): aria-pressed; round buttons turn square when selected and square ones round, on the fast spatial spring.

Prop Type Default Description
variant 'filled' | 'tonal' | 'elevated' | 'outlined' filled Color configuration. Default filled. Toggle buttons have no text variant (Compose).
isSelected boolean Controlled selection.
defaultSelected boolean Uncontrolled initial selection.
size "xs" | "s" | "m" | "l" | "xl" s Default s (40dp).
shape "round" | "square" round Resting shape. Default round.
icon ReactNode Leading icon, e.g. <Icon name="add" />.
trailingIcon ReactNode Trailing icon.
isDisabled boolean
children ReactNode
className string
onChange (isSelected: boolean) => void
onPress (e: PressEvent) => void

Also accepts HTML attributes not listed here.

buttonClassName()

Classes for a button (the m3e-css contract).

CSS

button.css

@import '@kyralabs/m3e-css/button.css';

Button and toggle button (COMPONENT_SPEC §1). No JavaScript needed; m3e-react adds interruptible springs and React Aria behavior on the same classes.

<button class="m3e-button m3e-button--filled m3e-button--size-m m3e-button--round">
  <span class="m3e-icon" aria-hidden="true">save</span> Save
</button>
<button class="m3e-button m3e-button--tonal" aria-pressed="false">Toggle</button>

Variants: --filled (default) --tonal --elevated --outlined --text. Sizes: --size-xs --size-s (default) --size-m --size-l --size-xl. Shapes: --round (default) --square. A button with aria-pressed is a toggle: aria-pressed="true" is selected (aria-checked likewise, for the radio buttons of a single-select button group).

102 custom properties it reads
Custom property Default
--m3e-comp-button-large-container-height 96px
--m3e-comp-button-large-container-shape-pressed var(--m3e-sys-shape-corner-large)
--m3e-comp-button-large-container-shape-square var(--m3e-sys-shape-corner-extra-large)
--m3e-comp-button-large-icon-label-space 12px
--m3e-comp-button-large-icon-size 32px
--m3e-comp-button-large-leading-space 48px
--m3e-comp-button-large-outlined-outline-width 2px
--m3e-comp-button-large-trailing-space 48px
--m3e-comp-button-large-vertical-padding 32px
--m3e-comp-button-medium-container-height 56px
--m3e-comp-button-medium-container-shape-pressed var(--m3e-sys-shape-corner-medium)
--m3e-comp-button-medium-container-shape-square var(--m3e-sys-shape-corner-large)
--m3e-comp-button-medium-icon-label-space 8px
--m3e-comp-button-medium-icon-size 24px
--m3e-comp-button-medium-leading-space 24px
--m3e-comp-button-medium-outlined-outline-width 1px
--m3e-comp-button-medium-trailing-space 24px
--m3e-comp-button-medium-vertical-padding 16px
--m3e-comp-button-min-width 58px
--m3e-comp-button-small-container-height 40px
--m3e-comp-button-small-container-shape-pressed var(--m3e-sys-shape-corner-small)
--m3e-comp-button-small-container-shape-square var(--m3e-sys-shape-corner-medium)
--m3e-comp-button-small-icon-label-space 8px
--m3e-comp-button-small-icon-size 20px
--m3e-comp-button-small-leading-space 16px
--m3e-comp-button-small-outlined-outline-width 1px
--m3e-comp-button-small-trailing-space 16px
--m3e-comp-button-small-vertical-padding 10px
--m3e-comp-button-x-large-container-height 136px
--m3e-comp-button-x-large-container-shape-pressed var(--m3e-sys-shape-corner-large)
--m3e-comp-button-x-large-container-shape-square var(--m3e-sys-shape-corner-extra-large)
--m3e-comp-button-x-large-icon-label-space 16px
--m3e-comp-button-x-large-icon-size 40px
--m3e-comp-button-x-large-leading-space 64px
--m3e-comp-button-x-large-outlined-outline-width 3px
--m3e-comp-button-x-large-trailing-space 64px
--m3e-comp-button-x-large-vertical-padding 48px
--m3e-comp-button-x-small-container-height 32px
--m3e-comp-button-x-small-container-shape-pressed var(--m3e-sys-shape-corner-small)
--m3e-comp-button-x-small-container-shape-square var(--m3e-sys-shape-corner-medium)
--m3e-comp-button-x-small-content-padding-horizontal 12px
--m3e-comp-button-x-small-content-padding-vertical 6px
--m3e-comp-button-x-small-icon-size 20px
--m3e-comp-button-x-small-icon-spacing 4px
--m3e-comp-button-x-small-outlined-outline-width 1px
--m3e-comp-elevated-button-container-color var(--m3e-sys-color-surface-container-low)
--m3e-comp-elevated-button-container-color-disabled var(--m3e-sys-color-on-surface)
--m3e-comp-elevated-button-container-color-selected var(--m3e-sys-color-primary)
--m3e-comp-elevated-button-container-color-unselected var(--m3e-sys-color-surface-container-low)
--m3e-comp-elevated-button-container-shadow var(--m3e-sys-elevation-level1-shadow)
--m3e-comp-elevated-button-container-shadow-disabled var(--m3e-sys-elevation-level0-shadow)
--m3e-comp-elevated-button-container-shadow-focused var(--m3e-sys-elevation-level1-shadow)
--m3e-comp-elevated-button-container-shadow-hovered var(--m3e-sys-elevation-level2-shadow)
--m3e-comp-elevated-button-container-shadow-pressed var(--m3e-sys-elevation-level1-shadow)
--m3e-comp-elevated-button-label-text-color var(--m3e-sys-color-primary)
--m3e-comp-elevated-button-label-text-color-disabled var(--m3e-sys-color-on-surface-variant)
--m3e-comp-elevated-button-label-text-color-selected var(--m3e-sys-color-on-primary)
--m3e-comp-elevated-button-label-text-color-unselected var(--m3e-sys-color-primary)
--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-filled-button-container-color var(--m3e-sys-color-primary)
--m3e-comp-filled-button-container-color-disabled var(--m3e-sys-color-on-surface)
--m3e-comp-filled-button-container-color-selected var(--m3e-sys-color-primary)
--m3e-comp-filled-button-container-color-unselected var(--m3e-sys-color-surface-container)
--m3e-comp-filled-button-container-shadow var(--m3e-sys-elevation-level0-shadow)
--m3e-comp-filled-button-container-shadow-disabled var(--m3e-sys-elevation-level0-shadow)
--m3e-comp-filled-button-container-shadow-focused var(--m3e-sys-elevation-level0-shadow)
--m3e-comp-filled-button-container-shadow-hovered var(--m3e-sys-elevation-level1-shadow)
--m3e-comp-filled-button-container-shadow-pressed var(--m3e-sys-elevation-level0-shadow)
--m3e-comp-filled-button-label-text-color var(--m3e-sys-color-on-primary)
--m3e-comp-filled-button-label-text-color-disabled var(--m3e-sys-color-on-surface-variant)
--m3e-comp-filled-button-label-text-color-selected var(--m3e-sys-color-on-primary)
--m3e-comp-filled-button-label-text-color-unselected var(--m3e-sys-color-on-surface-variant)
--m3e-comp-filled-tonal-button-container-color var(--m3e-sys-color-secondary-container)
--m3e-comp-filled-tonal-button-container-color-disabled var(--m3e-sys-color-on-surface)
--m3e-comp-filled-tonal-button-container-shadow var(--m3e-sys-elevation-level0-shadow)
--m3e-comp-filled-tonal-button-container-shadow-disabled var(--m3e-sys-elevation-level0-shadow)
--m3e-comp-filled-tonal-button-container-shadow-focus var(--m3e-sys-elevation-level0-shadow)
--m3e-comp-filled-tonal-button-container-shadow-hover var(--m3e-sys-elevation-level1-shadow)
--m3e-comp-filled-tonal-button-container-shadow-pressed var(--m3e-sys-elevation-level0-shadow)
--m3e-comp-filled-tonal-button-label-text-color var(--m3e-sys-color-on-secondary-container)
--m3e-comp-filled-tonal-button-label-text-color-disabled var(--m3e-sys-color-on-surface)
--m3e-comp-minimum-interactive-component-size 48px
--m3e-comp-outlined-button-container-color-selected var(--m3e-sys-color-inverse-surface)
--m3e-comp-outlined-button-container-opacity-disabled 0.1
--m3e-comp-outlined-button-label-text-color var(--m3e-sys-color-on-surface-variant)
--m3e-comp-outlined-button-label-text-color-disabled var(--m3e-sys-color-on-surface-variant)
--m3e-comp-outlined-button-label-text-color-selected var(--m3e-sys-color-inverse-on-surface)
--m3e-comp-outlined-button-label-text-color-unselected var(--m3e-sys-color-on-surface-variant)
--m3e-comp-outlined-button-outline-color var(--m3e-sys-color-outline-variant)
--m3e-comp-outlined-button-outline-color-disabled var(--m3e-sys-color-outline-variant)
--m3e-comp-text-button-label-color-disabled var(--m3e-sys-color-on-surface-variant)
--m3e-comp-text-button-label-opacity-disabled 0.38
--m3e-comp-toggle-button-small-pressed-shape 6px
--m3e-comp-tonal-button-container-color-disabled var(--m3e-sys-color-on-surface)
--m3e-comp-tonal-button-container-color-selected var(--m3e-sys-color-secondary)
--m3e-comp-tonal-button-container-color-unselected var(--m3e-sys-color-secondary-container)
--m3e-comp-tonal-button-label-text-color-disabled var(--m3e-sys-color-on-surface-variant)
--m3e-comp-tonal-button-label-text-color-selected var(--m3e-sys-color-on-secondary)
--m3e-comp-tonal-button-label-text-color-unselected var(--m3e-sys-color-on-secondary-container)

Accessibility

<button>; Enter/Space activate. Toggle uses aria-pressed. Icon-only requires aria-label.

press morph on pointerdown, release on pointerup/cancel. Toggle flips aria-pressed. Disabled buttons not focusable unless focusableWhenDisabled.

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.

Differences from Compose

ripple approximated; no haptics.

Sources

Values come from androidx Compose Material3 1.5.0-beta01 at 19991f2c and from m3.material.io, as cited below from COMPONENT_SPEC §1 and the m3e-css sources.

m3.material.io/components/buttons/overview; Button*Tokens.kt; ButtonDefaults.

Button{XSmall,Small,Medium,Large,XLarge}Tokens.kt@19991f2c plus the ButtonDefaults values Compose renders (components/button-defaults.css; SOURCES register #18–19).

  • Round = CircleShape (half the container height). Small toggle buttons press to 6dp (ToggleButtonDefaults.pressedShape).
  • Colors: filled = primary/on-primary; tonal = secondary-container/on-secondary-container (FilledTonalButtonTokens; toggles use TonalButtonTokens); elevated = surface-container-low/primary, Level1; outlined = outline-variant border, on-surface-variant label; text = primary label (ButtonDefaults override).
  • Toggle: unselected filled = surface-container / on-surface-variant, selected = primary / on-primary; tonal selected = secondary / on-secondary; elevated selected = primary / on-primary; outlined selected = inverse-surface / inverse-on-surface without border. No text toggle (Compose has none).
button.css sources

(compose/material3 @ 19991f2c):

  • Sizes, shapes, icon size/spacing, outline width: Button{XSmall,Small,Medium,Large,XLarge}Tokens. Vertical padding, minimum width and the XS padding/spacing: ButtonDefaults (button-defaults.css, where Compose overrides ButtonXSmallTokens pending an upstream correction).
  • Label type style per size: ButtonDefaults.textStyleFor (Button.kt L1640): labelLarge (XS, S), titleMedium (M), headlineSmall (L), headlineLarge (XL).
  • Colors and elevation: the token objects Compose uses. Button.kt: FilledButtonTokens, ElevatedButtonTokens, FilledTonalButtonTokens, OutlinedButtonTokens, and ColorScheme Primary for text button content (Button.kt L1414, upstream TODO over TextButtonTokens.LabelColor). ToggleButton.kt: FilledButtonTokens / ElevatedButtonTokens Unselected/Selected*, TonalButtonTokens, OutlinedButtonTokens (selected outlined toggles drop the border).
  • Round = CircleShape (ShapeTokens.CornerFull) = half the container height here, so the press morph springs from a finite radius. Pressed shape: Button*Tokens.PressedContainerShape (small toggles: ToggleButtonDefaults.pressedShape). Selected: round → SelectedContainerShapeSquare, square → round.
  • Motion: press morph on the default effects spring (Button.kt L212-215, chosen upstream "to prevent any bounce"); toggle shape on the fast spatial spring (ToggleButton.kt L176); colors on the fast effects spring (COMPONENT_SPEC §1); elevation with the ElevationDefaults tweens (internal/Elevation.kt, elevation-motion.css), as Compose's ButtonElevation does.
  • Touch target: ≥ LocalMinimumInteractiveComponentSize via ::after; ::before is the state layer.