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.
- COMPONENT_SPEC §1
- CSS · React
- Open in the playground
Examples
Color configurations
Filled for the main action, tonal and elevated for secondary ones, outlined and text for low emphasis.
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.
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.
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.
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.
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.
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.