Button

Updated: Sep 24, 2026
Button is the control for one explicit, immediate action. It owns its intrinsic width and height, its material and shape, focus and press motion, the leading icon or avatar slot, label expansion and truncation, the disabled treatment, and tooltip integration. Supply content and a handler, and let the component own its appearance, size, and motion so interaction states stay synchronized.
A button with a leading icon or avatar rests collapsed and expands to reveal its label on focus or press. A text-only button, or any button with alwaysShowText, rests expanded. Put peer actions in ButtonRail or ButtonGroup rather than in a flex row of your own, so spacing, focus travel, and overflow are handled for you.
Button is built on Container, which is built on InteractableBase. Read those pages for the full interaction, material, and tooltip contract.

Usage

import { Button, ButtonRail, IconTintColor } from '@wearables-ui-toolkit/mrbd';
import type { IconSource } from '@wearables-ui-toolkit/mrbd';

type ComposeActionsProps = {
  replyIcon: IconSource;
  reactIcon: IconSource;
  deleteIcon: IconSource;
  canDelete: boolean;
  onReply: () => void;
  onReact: () => void;
  onDelete: () => void;
};

export function ComposeActions({
  replyIcon,
  reactIcon,
  deleteIcon,
  canDelete,
  onReply,
  onReact,
  onDelete,
}: ComposeActionsProps) {
  return (
    <ButtonRail>
      <Button icon={replyIcon} title="Reply" onClick={onReply} />
      <Button icon={reactIcon} title="React" onClick={onReact} />
      <Button
        icon={deleteIcon}
        title="Delete"
        iconTintColor={IconTintColor.NEGATIVE}
        disabled={!canDelete}
        onClick={onDelete}
      />
    </ButtonRail>
  );
}
An avatar button uses a person or entity as the identity of the action, and alwaysShowText keeps the name visible while unfocused:
<Button
  avatarSrc={contact.photoUrl}
  avatarAlt={contact.name}
  title={contact.name}
  subtitle="Call"
  alwaysShowText
  onClick={() => call(contact)}
/>
An icon-only button needs an accessible label, and benefits from a tooltip when the glyph is not self-evident:
import { Button, TooltipMode } from '@wearables-ui-toolkit/mrbd';

<Button
  icon={moreIcon}
  ariaLabel="More options"
  tooltipText="More options"
  tooltipMode={TooltipMode.FOCUSED}
  onClick={openMenu}
/>;
Content you do not control can be capped and ellipsized instead of pushing the lane wider, and trailingTag marks the action as an in-development capability:
import { Button, TrailingTag } from '@wearables-ui-toolkit/mrbd';

<Button
  icon={translateIcon}
  title={feature.displayName}
  trailingTag={TrailingTag.BETA}
  enforceMaxWidth
  onClick={openFeature}
/>;
A disabled action can explain itself when someone tries to activate it:
<Button
  icon={sendIcon}
  title="Send"
  disabled={!isDraftValid}
  tooltipText="Add a message first"
  tooltipMode={TooltipMode.DISABLED_CLICK}
  onClick={send}
/>

Props

The full prop type is ButtonProps. It extends the Container prop type, minus the props the button owns itself.

Own props

PropTypeDefaultDescription
title
string
—
Primary label. Hidden while collapsed unless alwaysShowText is set or there is no leading accessory.
subtitle
string
—
Secondary line rendered under the title, in the secondary text role. Follows the same show/hide rules as the title.
alwaysShowText
boolean
false
Keep the label visible in the default (unfocused) state, so the button rests expanded.
icon
IconSource
—
Leading icon. Ignored when an avatar prop is also supplied.
iconTintColor
IconTintColor
IconTintColor.PRIMARY
Semantic tint applied to the icon when applyIconTinting is true.
applyIconTinting
boolean
true
Tint the icon with the resolved color role. Set false for full-color artwork.
showIconActiveIndicator
boolean
false
Show the small active dot under the icon. The dot is reserved in layout either way, so toggling it does not shift the glyph.
iconRotation
number
0
Icon rotation, in degrees.
animateIconRotation
boolean
false
Animate changes to iconRotation instead of applying them immediately.
iconRotationDuration
number
1000
Duration of the rotation transition, in milliseconds.
avatarSrc
string
—
Avatar image URL. Supplying it switches the leading slot to avatar mode.
avatarPrimaryContent
ReactNode
—
Content for the avatar primary slot. Also switches the leading slot to avatar mode. The avatar owns the slot size.
avatarAlt
string
—
Accessible label for the avatar image.
statusIndicator
StatusIndicatorType
—
Status dot rendered on the avatar.
statusIndicatorIcon
IconSource
—
Glyph drawn over the avatar status dot.
avatarBadgeSrc
string
—
Image URL for the avatar badge slot.
avatarBadgeContent
ReactNode
—
Content for the avatar badge slot. The avatar owns the badge size.
placeholderStyle
PlaceholderStyle
PlaceholderStyle.AVATAR
Placeholder treatment used by the avatar when the slot has neither an image nor primary content.
trailingTag
TrailingTag
TrailingTag.NONE
Design-system-owned tag rendered after the title. The label is fixed; there is no caller-supplied tag text.
enforceMaxWidth
boolean
false
Cap the expanded width and truncate the title and subtitle with an ellipsis.
width
number \| string
—
Explicit width. Only a numeric value is honored; a string falls back to the component’s minimum width. Overrides intrinsic sizing, so avoid it.

Notable inherited props

PropTypeDefaultDescription
onClick
(event: MouseEvent) => void
—
Activation handler. Receives a real DOM click event for pointer and keyboard activation.
onActivate
(event: InteractableActivationEvent) => void
—
Called once per activation; keyboard activation receives the originating keyboard event.
onLongPress
(event) => void
—
Called when the press is held past the long-press threshold.
onStateChange
(prev: InteractionState, next: InteractionState) => void
—
Called on every default / focused / pressed / disabled transition.
disabled
boolean
false
Dim the button and suppress activation. The button stays focusable.
interactive
boolean
true
Master switch for focusable, pressable, and clickable.
focusable
boolean
inherits interactive
Whether the button takes focus.
pressable
boolean
inherits interactive
Whether the button enters the pressed state.
clickable
boolean
inherits interactive
Whether the button activates on click.
initialFocusEligible
boolean
true
Whether the button may receive initial focus when a route mounts.
ariaLabel
string
—
Accessible name. Required for icon-only or avatar-only buttons. A standard aria-label prop takes precedence over it.
tooltipText
string
—
Tooltip copy.
tooltipContent
ReactNode
—
Rich tooltip body, used instead of tooltipText.
tooltipMode
TooltipMode
TooltipMode.NONE
When the tooltip appears: on focus, after a dwell delay, on disabled activation, or always.
tooltipPosition
TooltipPosition
TooltipPosition.ANCHORED
Where the tooltip is placed relative to the button.
tooltipShowTail
boolean
true
Whether the tooltip draws a tail pointing at the button.
partialFocusSupportedAxis
PartialFocusSupportedAxis
PartialFocusSupportedAxis.XY
Which axes give partial-focus rubberband feedback when directional focus cannot move.
onInvalidFocusDirection
(direction: InvalidFocusDirection) => void
—
Called when directional focus input cannot move focus out of the button in the requested direction.
material
ContainerMaterial
the button material
Material used for the background and stroke layers.
shapeProvider
ShapeProvider
full-radius rounded rectangle
Geometry used for clipping and every material path.
className
string
''
Extra class on the root element.
style
CSSProperties
—
Extra inline styles on the root element. They merge over the component’s own transform and width constraints, so use them for placement, not for restyling.
Button does not accept children or height: content comes from the props above, and the height is component-owned. The role and the container’s clipping are component-owned too — the button never clips its own box, so its focus glow can render outside it.

Types and enums

IconTintColor

The closed set of icon tints. Free-form CSS colors are intentionally not accepted.
MemberResolves to
PRIMARY
The primary icon color role (default)
SECONDARY
The secondary icon color role, composited with a lighten blend
ACCENT
The accent icon color role
ON_CHECKED
The primary-button background color role, used as the glyph color on a checked control
ACTION
The persistent action color role
POSITIVE
The persistent positive color role
WARNING
The persistent warning color role
NEGATIVE
The persistent negative color role
INFO
The persistent info color role
getIconTintCSSVariable(tint) returns a var() reference to the matching color token, and getIconTintBlendMode(tint) returns the CSS mix-blend-mode a tint requires ('lighten' for SECONDARY, otherwise undefined). Both are exported for components that build their own icon slots.

TrailingTag

MemberDescription
NONE
No trailing tag (default)
BETA
Renders the design-system “Beta” tag after the title
The tag only renders when the button also has text, and it fades in and out with the label.
StatusIndicatorType and PlaceholderStyle are documented on the Avatar page.

Imperative handle

Button forwards a ButtonHandle.
MethodDescription
animateActionTransition(options: ButtonActionTransitionOptions): void
Morphs the button to a completed-action state: the current icon shrinks away, the ending icon (and optionally an ending title) is swapped in while the glyph is invisible, then the new icon springs back to full size. A no-op unless endingIcon is supplied.
getElement(): HTMLDivElement \| null
Returns the root element, or null before mount and after unmount. Use it for focus restoration and measurement.
ButtonActionTransitionOptions has two optional members:
MemberTypeDescription
endingIcon
IconSource
Icon to display once the transition completes, such as a “sent” checkmark. Required for the call to do anything.
endingTitle
string
Title to swap in partway through the transition. An empty string is treated the same as omitting it.
import { useRef } from 'react';
import { Button } from '@wearables-ui-toolkit/mrbd';
import type { ButtonHandle } from '@wearables-ui-toolkit/mrbd';

const buttonRef = useRef<ButtonHandle>(null);

async function send() {
  await postMessage();
  buttonRef.current?.animateActionTransition({
    endingIcon: checkIcon,
    endingTitle: 'Sent',
  });
}

<Button ref={buttonRef} icon={sendIcon} title="Send" onClick={send} />;
Updating the declarative icon or title prop clears the corresponding override, so props stay the source of truth even while a transition is in flight. Under reduced motion the swap is applied immediately with no animation.

Behavior

Leading accessory. Exactly one leading slot renders. An avatar prop (avatarSrc or avatarPrimaryContent) wins over icon; with neither, the button is text-only. Do not supply both. The avatar is rendered at AvatarSize.LARGE — the slot size is component-owned, so the avatar props that would change it are not exposed here.
Expansion. With a leading accessory and text, the button rests at its square collapsed size and expands to the natural content width on focus or press, fading the label in slightly behind the width change; it collapses again on blur. Text that is present but hidden still occupies the accessibility tree, so a collapsed button is still named by its label. Text-only buttons and alwaysShowText buttons rest expanded and paint at full width on the first frame. While collapsed, the icon or avatar counter-scales against the container so the glyph stays visually steady.
Sizing and measurement. The button measures its own content, re-measures once web fonts finish loading, and re-measures when its content resizes; measurements requested during an animation are deferred and applied when the animation completes. A button whose width depends on text it has not been able to measure yet does not animate toward a fallback width — it applies the correct state once the measurement lands. That is why forcing a width or wrapping the button in a stretching layout breaks it. While the width animates, the button publishes a layout compensation that ButtonGroup and ButtonRail read as horizontal margins, so the gaps between actions stay even as it expands and collapses. It also publishes the geometry it will settle at in the default and focused states, which is what lets an enclosing ButtonRail project the final row layout on the frame focus changes and move its scroll in step with the expansion instead of chasing it.
Truncation. With enforceMaxWidth, the expanded width is capped at a maximum that depends on the leading mode (widest for text-only, narrower with an icon, narrowest with an avatar) and the labels truncate with an ellipsis. Truncation only engages once the measured content actually exceeds that cap, so short labels are never constrained.
Material and state. The default material is the button material on a full-radius shape, so the control reads as a pill. Focus and press change the container scale and cross-fade the material between its idle, focused, and pressed layers: the resting state is inset slightly, focus goes to full size, and press insets again. A button in avatar mode without alwaysShowText hides its material entirely while unfocused, so the avatar reads as a standalone element, and the material returns on focus or press.
Icon rotation.iconRotation rotates the icon slot by the given number of degrees. With animateIconRotation, changes ease out over iconRotationDuration; otherwise, and whenever reduced motion is requested, the new angle is applied immediately.
Disabled. A disabled button dims to --uit-disabled-opacity, keeps its focus stop, exposes aria-disabled, and suppresses activation — it still reports focus and blur state changes. Pair it with TooltipMode.DISABLED_CLICK when someone needs to know why the action is unavailable.
Reduced motion. When the display prefers reduced motion, expansion, the material cross-fade, icon rotation, and the action transition all snap to their end state instead of animating.
Fast traversal and page entry. While focus is moving quickly through a lane, state changes snap to their end state instead of animating, so a long rail does not flicker open and closed. State changes also snap while a route is entering, so a button that takes page-entry focus paints focused immediately. Let the component own that timing rather than gating labels with application timers.

Accessibility

  • The button renders with role="button", takes a focus stop (tabIndex is 0 by default), and activates on Enter, Space, and pointer click.
  • The accessible name comes from the rendered title and subtitle text. For a button with no text — icon-only or avatar-only — you must supply ariaLabel or the standard aria-label; when both are present, aria-label wins.
  • Icons are decorative and hidden from assistive technology, and the trailing tag is hidden as well, so anything they communicate must also be in the label.
  • avatarAlt labels the avatar image; pass a localized string.
  • Disabled buttons remain reachable so their tooltip can be read; they report aria-disabled rather than disappearing from the focus order.
  • subtitle is part of the accessible name, so keep it meaningful rather than using it for decoration.
  • Keep labels short, verb-led, and specific about what the next press does.
  • An accessible outcome label is required for every icon-only action. Add a focused tooltip when the glyph is unfamiliar.
  • Let the button size itself; shorten copy instead of constraining the control.
  • Use enforceMaxWidth when the label comes from data outside application control.
  • Use avatar mode when a person or entity is the identity of the action.
  • Use disabled for unavailable actions instead of silently dropping the handler.
  • Keep a focused button mounted when its command changes state. Updating it in place preserves focus.

Avoid

  • Avoid stretching the button through grid or flex layout rules; preserve its component-owned width and flex sizing.
  • Avoid placing peer buttons in an uncoordinated layout. ButtonGroup and ButtonRail provide the expected spacing, focus travel, and overflow behavior.
  • Avoid animating scale or opacity on a wrapper because the component already owns focus and press motion.
  • Avoid combining an icon and avatar, and treat showIconActiveIndicator as a supplement to an accessible label rather than a replacement for one.
  • Avoid customizing internal icon margins, glyph size, or generated class names; these details are not public customization surfaces.
  • Avoid animateActionTransition for ordinary declarative state updates. It is intended for imperative action changes.