Skip to content

button

The button role declares that an element performs an action when activated.

Why it matters

Without the role, a button scaffolds as a generic container. It cannot be tabbed to, Enter and Space do nothing, and the generated props contain no onClick — so a consumer cannot attach a handler through the component’s own interface. A disabled state renders as an aria-disabled attribute the element cannot enforce, so a greyed-out button still receives clicks.

Emission

Scaffold

Element<button type="button">
Accepted element typescontainer, glyph
Accepted partslabel, description, indicator

The role element becomes the button, and its descendants render inside it. Descendants that would otherwise be <div> become <span>, because a button may only contain phrasing content — classes and layout are unchanged.

Where the role lands on a glyph, the glyph is re-hosted inside the button rather than becoming it, which adds one level to the markup.

Contract

PropTypeTierGenerated body
onClick?(e: MouseEvent) => voidAlwaysCalls the prop, nothing more
type?'button' | 'submit' | 'reset'Always
onFocus? / onBlur?(e: FocusEvent) => voidOn requestForwarded to the element

onClick is generated empty on purpose. A click on a button means whatever the consumer decides, and the design file has no way to say what that is, so the transform calls the prop and does nothing else.

This is where button differs from togglebutton, which owns a state the click changes and so gets real generated logic.

States

StateWhat the button doesClassify in states?
disabledNative disabled — unfocusable and unclickable, enforced by the platformRecommended
hoverNative hoverRecommended, if the library styles it
activeNative pressed-downRecommended, if the library styles it
focus / focus-visibleNative focus ringOptional — prefer the platform default

A button transforms best when the Figma variant props for these states are classified in the states convention, which is what tells the transform that a given prop carries a given state. Unclassified props still work; they emit as data-* attributes for styling, as they do today.

Focus is the exception worth calling out. Browsers and mobile platforms ship a focus indicator that already meets contrast requirements and matches what users of that platform expect. Specifying one from Figma usually replaces a good default with a worse one, so leave focus unclassified unless the library deliberately overrides it.

Accessible name

A button needs a name. It takes one from a label part, or from text it already contains.

Where the button has no text at all — an icon-only button is the common case — the name comes from a prop nominated as the accessible-name source, and the transform emits it as the platform’s label. Where no name source resolves, the transform warns: a correctly-marked button that announces nothing is worse than the container it replaced.

Platforms

EmitsBehavior a user gets
Web<button type="button">Tab-focusable, Enter and Space activate, disabled blocks interaction
iOSButtonVoiceOver announces “Button”, it becomes a rotor stop, and Full Keyboard Access can reach it
AndroidButton with Role.ButtonTalkBack announces “Button”, double-tap activates, and it joins the accessibility focus order

Before and after

Without the role:

<div
className="button"
data-element="root"
aria-disabled={p.disabled ? 'true' : undefined}
>
{/* … */}
</div>

With the role:

<button
type="button"
className="button"
data-element="root"
disabled={p.disabled}
onClick={p.onClick}
>
{/* … */}
</button>

See also