Identifies this entity as a component.
Machine-readable identifier (e.g., 'button', 'dialog', 'form-field', 'data-table'). MUST be unique within the documentation group.
Pattern: ^[a-z][a-z0-9-]*$
Human-readable name for display in docs (e.g., 'Button', 'Dialog', 'Form Field', 'Data Table').
Modular metadata entries. Each entry is a typed object with a `kind` discriminator. Types include status, since, tags, category, aliases, summary, thumbnail, preview, extends, and links.
All structured docs for this component as an ordered array of typed document block objects. Accepts component-specific types (anatomy, api, variants, states, design-specifications) and general types (guideline, purpose, accessibility, examples). Each document block entry has a `type` property that determines its shape. Ordering is significant: tools SHOULD preserve it for display.
Agent-optimized context for this component — intent, constraints, disambiguation, anti-patterns, examples, and keywords for efficient agentic consumption.
References:
entityMetadata, componentDocumentBlock, agents, extensions
{
"kind": "component",
"name": "button",
"displayName": "Button",
"metadata": [
{
"kind": "description",
"value": "An interactive element that triggers an action when activated. Buttons communicate what will happen when the user interacts with them and are the primary mechanism for initiating actions within a surface."
},
{
"kind": "status",
"status": "stable",
"platformStatus": {
"react": {
"status": "stable",
"since": "1.0.0"
},
"web-component": {
"status": "experimental",
"since": "3.2.0",
"description": "Available as a Web Component wrapper. Native shadow DOM implementation planned for v4."
},
"ios": {
"status": "stable",
"since": "2.1.0"
},
"android": {
"status": "draft",
"description": "Compose implementation in progress. Expected in v4.0."
},
"figma": {
"status": "stable",
"since": "1.0.0"
}
}
},
{
"kind": "since",
"value": "1.0.0"
},
{
"kind": "tags",
"items": [
"action",
"form",
"interactive"
]
},
{
"kind": "category",
"value": "action"
},
{
"kind": "aliases",
"items": [
"btn",
"action-button",
"CTA"
]
},
{
"kind": "summary",
"value": "An interactive element that triggers an action when activated."
},
{
"kind": "links",
"items": [
{
"kind": "source",
"url": "https://code.acme.com/design-system/src/packages/components/src/button/button.tsx",
"label": "React component source"
},
{
"kind": "source",
"url": "https://code.acme.com/design-system/src/packages/components/src/button/button.test.tsx",
"label": "Unit tests"
},
{
"kind": "design",
"url": "https://design-tool.acme.com/file/abc123?node-id=1234:5678",
"label": "Design file — component"
},
{
"kind": "design",
"url": "https://design-tool.acme.com/file/abc123?node-id=1234:9999",
"label": "Design file — variants"
},
{
"kind": "storybook",
"url": "https://storybook.acme.com/?path=/docs/components-button--docs",
"label": "Storybook docs"
},
{
"kind": "package",
"url": "https://www.npmjs.com/package/@acme/components",
"label": "npm package"
},
{
"kind": "alternative",
"url": "https://design.acme.com/components/link",
"label": "link (component)"
},
{
"kind": "child",
"url": "https://design.acme.com/components/icon-button",
"label": "icon-button (component)"
},
{
"kind": "parent",
"url": "https://design.acme.com/components/button-group",
"label": "button-group (component)"
}
]
}
],
"documentBlocks": [
{
"kind": "anatomy",
"description": "The Button is composed of a container, a text label, and an optional leading or trailing icon.",
"parts": [
{
"name": "container",
"displayName": "Container",
"description": "The outer boundary of the button. Receives background color, border, border radius, and padding. Defines the clickable area.",
"required": true,
"tokens": {
"background": "button-background",
"border-color": "button-border-color",
"border-width": "button-border-width",
"border-radius": "button-border-radius",
"padding-horizontal": "button-padding-horizontal",
"padding-vertical": "button-padding-vertical"
}
},
{
"name": "label",
"displayName": "Label",
"description": "The text content of the button. Communicates the action that will occur on activation.",
"required": true,
"tokens": {
"font-family": "button-font-family",
"font-size": "button-font-size",
"font-weight": "button-font-weight",
"line-height": "button-line-height",
"text-color": "button-text-color"
}
},
{
"name": "icon",
"displayName": "Icon",
"description": "An optional icon displayed before (leading) or after (trailing) the label. Reinforces the label's meaning visually.",
"required": false,
"tokens": {
"size": "button-icon-size",
"color": "button-icon-color",
"gap": "button-icon-gap"
}
},
{
"name": "focus-ring",
"displayName": "Focus Ring",
"description": "A visible outline rendered when the button receives keyboard focus. Not displayed on mouse interaction.",
"required": true,
"tokens": {
"color": "button-focus-ring-color",
"width": "button-focus-ring-width",
"offset": "button-focus-ring-offset"
}
}
],
"preview": [
{
"title": "Anatomy diagram",
"presentation": {
"kind": "image",
"url": "https://design.acme.com/assets/button-anatomy.png",
"alt": "An annotated diagram of a primary button with numbered callouts: 1. Container, 2. Label, 3. Icon (optional), 4. Focus ring (shown in dashed outline)."
}
}
],
"agents": {
"intent": "Describe the visual parts that compose a Button so agents can map design tokens to the correct sub-elements.",
"keywords": [
"parts",
"container",
"label",
"icon",
"focus ring",
"structure"
]
}
},
{
"kind": "api",
"properties": [
{
"name": "variant",
"type": "'primary' | 'secondary' | 'ghost' | 'danger'",
"schema": {
"type": "string",
"enum": [
"primary",
"secondary",
"ghost",
"danger"
],
"default": "primary"
},
"description": "The visual style of the button. Determines background color, text color, and border treatment.",
"required": false,
"defaultValue": "primary",
"since": "1.0.0"
},
{
"name": "size",
"type": "'small' | 'medium' | 'large'",
"schema": {
"type": "string",
"enum": [
"small",
"medium",
"large"
],
"default": "medium"
},
"description": "The size of the button. Affects padding, font size, icon size, and minimum target area.",
"required": false,
"defaultValue": "medium",
"since": "1.0.0"
},
{
"name": "disabled",
"type": "boolean",
"schema": {
"type": "boolean",
"default": false
},
"description": "When true, the button is non-interactive. The cursor changes to not-allowed, and the button is visually dimmed to 40% opacity.",
"required": false,
"defaultValue": false,
"since": "1.0.0"
},
{
"name": "loading",
"type": "boolean",
"schema": {
"type": "boolean",
"default": false
},
"description": "When true, the label is replaced with a spinner and the button is non-interactive. The button retains its dimensions to prevent layout shift.",
"required": false,
"defaultValue": false,
"since": "2.1.0"
},
{
"name": "fullWidth",
"type": "boolean",
"schema": {
"type": "boolean",
"default": false
},
"description": "When true, the button expands to fill the width of its parent container.",
"required": false,
"defaultValue": false,
"since": "1.2.0"
},
{
"name": "iconStart",
"type": "IconComponent",
"description": "An icon component rendered before the label. When provided without a label, an aria-label is required.",
"required": false,
"since": "2.0.0"
},
{
"name": "iconEnd",
"type": "IconComponent",
"description": "An icon component rendered after the label.",
"required": false,
"since": "2.0.0"
},
{
"name": "type",
"type": "'button' | 'submit' | 'reset'",
"schema": {
"type": "string",
"enum": [
"button",
"submit",
"reset"
],
"default": "button"
},
"description": "The HTML button type attribute. Controls form submission behavior.",
"required": false,
"defaultValue": "button",
"since": "1.0.0"
}
],
"events": [
{
"name": "onClick",
"description": "Fires when the button is activated via mouse click, touch tap, Enter key, or Space key. Does not fire when the button is disabled or loading.",
"payload": "(event: MouseEvent) => void",
"since": "1.0.0"
},
{
"name": "onFocus",
"description": "Fires when the button receives focus.",
"payload": "(event: FocusEvent) => void",
"since": "1.0.0"
},
{
"name": "onBlur",
"description": "Fires when the button loses focus.",
"payload": "(event: FocusEvent) => void",
"since": "1.0.0"
}
],
"slots": [
{
"name": "default",
"description": "The button's text label.",
"acceptedContent": "Plain text or a text node. Do not nest interactive elements, headings, or block-level elements."
}
],
"cssCustomProperties": [
{
"name": "--button-background",
"description": "The background color of the button container.",
"type": "color",
"since": "1.0.0",
"defaultValue": "var(--color-action-primary)"
},
{
"name": "--button-text-color",
"description": "The color of the label text.",
"type": "color",
"since": "1.0.0",
"defaultValue": "var(--color-text-on-action)"
},
{
"name": "--button-border-radius",
"description": "The border radius of the button container.",
"type": "dimension",
"since": "1.0.0",
"defaultValue": "var(--radius-medium)"
}
],
"dataAttributes": [
{
"name": "data-state",
"description": "Reflects the current interactive state of the button. Useful for styling with attribute selectors.",
"values": [
"default",
"hover",
"active",
"focus",
"disabled",
"loading"
]
},
{
"name": "data-variant",
"description": "Reflects the current variant. Useful for parent-level conditional styling.",
"values": [
"primary",
"secondary",
"ghost",
"danger"
]
}
],
"agents": {
"intent": "Define the code-level interface of the Button component — properties, events, slots, and CSS hooks.",
"constraints": [
{
"rule": "Always set the type attribute explicitly; never rely on the browser default.",
"level": "should"
},
{
"rule": "When iconStart is used without a label, an aria-label must be provided.",
"level": "must"
}
],
"keywords": [
"props",
"events",
"slots",
"CSS custom properties",
"interface",
"API"
]
}
},
{
"kind": "events",
"items": [
{
"name": "onClick",
"description": "Fires when the button is activated via mouse click, touch tap, Enter key, or Space key. Does not fire when the button is disabled or loading.",
"payload": "(event: MouseEvent) => void",
"bubbles": true,
"cancelable": false,
"since": "1.0.0"
},
{
"name": "onFocus",
"description": "Fires when the button receives focus via keyboard tab, programmatic focus, or mouse click. Use to show contextual help or tooltips.",
"payload": "(event: FocusEvent) => void",
"bubbles": false,
"cancelable": false,
"since": "1.0.0"
},
{
"name": "onBlur",
"description": "Fires when the button loses focus. Use to dismiss contextual help or validate inline state.",
"payload": "(event: FocusEvent) => void",
"bubbles": false,
"cancelable": false,
"since": "1.0.0"
}
],
"agents": {
"intent": "Document the events emitted by the Button so agents can wire up correct event handlers.",
"constraints": [
{
"rule": "onClick does not fire when disabled or loading — do not attach workaround handlers.",
"level": "must-not"
}
],
"keywords": [
"events",
"onClick",
"onFocus",
"onBlur",
"handlers",
"callbacks"
]
}
},
{
"kind": "variants",
"items": [
{
"kind": "enum",
"name": "emphasis",
"displayName": "Emphasis",
"description": "Controls the visual weight of the button. Determines background fill, border treatment, and text color to establish a visual hierarchy among actions on a surface.",
"values": [
{
"name": "primary",
"displayName": "Primary",
"description": "High-emphasis — the main action on the surface. Uses a solid, filled background. Limit to one primary button per surface.",
"purpose": {
"kind": "purpose",
"useCases": [
{
"description": "When the action is the most important on the surface — the one the user is most likely to take (e.g., Save, Submit, Confirm).",
"kind": "positive"
},
{
"description": "When a surface already has a primary button. Adding a second dilutes visual hierarchy.",
"kind": "negative",
"alternative": {
"name": "secondary",
"rationale": "Secondary emphasis maintains importance without competing with the existing primary action."
}
}
]
}
},
{
"name": "secondary",
"displayName": "Secondary",
"description": "Medium-emphasis — important but not the primary action. Uses a visible border and transparent background.",
"purpose": {
"kind": "purpose",
"useCases": [
{
"description": "When the action is important but secondary to a primary action on the same surface (e.g., Cancel alongside Save).",
"kind": "positive"
}
]
}
},
{
"name": "ghost",
"displayName": "Ghost",
"description": "Low-emphasis — tertiary actions, toolbar actions, or dense layouts. No background or border in the default state.",
"purpose": {
"kind": "purpose",
"useCases": [
{
"description": "When the action is tertiary or supplementary — helpful but not essential to the user's primary task.",
"kind": "positive"
},
{
"description": "When the action is the only action on the surface and needs to be clearly discoverable.",
"kind": "negative",
"alternative": {
"name": "secondary",
"rationale": "A ghost button on its own can be overlooked. Secondary emphasis provides enough visual presence to be discoverable."
}
}
]
}
},
{
"name": "danger",
"displayName": "Danger",
"description": "High-emphasis destructive — signals an irreversible action. Uses the danger color. Pair with a confirmation dialog.",
"purpose": {
"kind": "purpose",
"useCases": [
{
"description": "When the action is destructive or irreversible — deleting a record, revoking access, removing a team member.",
"kind": "positive"
},
{
"description": "When the action is not destructive, even if it feels important or urgent.",
"kind": "negative",
"alternative": {
"name": "primary",
"rationale": "The danger color is a strong signal reserved for destruction. Using it for non-destructive actions dilutes its meaning."
}
}
]
}
}
]
},
{
"kind": "enum",
"name": "size",
"displayName": "Size",
"description": "Controls the physical dimensions of the button — padding, font size, icon size, and minimum touch target area.",
"values": [
{
"name": "sm",
"displayName": "Small",
"description": "Compact size for toolbars and dense layouts. 32px height."
},
{
"name": "md",
"displayName": "Medium",
"description": "Default size for most contexts. 40px height."
},
{
"name": "lg",
"displayName": "Large",
"description": "Touch-optimized size for mobile-first surfaces. 48px height."
}
]
}
],
"agents": {
"intent": "List the visual and behavioral dimensions along which the Button varies — emphasis and size.",
"constraints": [
{
"rule": "Limit each surface to one primary variant.",
"level": "must"
},
{
"rule": "Use danger variant only for destructive or irreversible actions.",
"level": "must"
}
],
"disambiguation": [
{
"entity": "states",
"distinction": "Variants are static design dimensions chosen at author time; states are dynamic conditions that change at runtime in response to user interaction."
}
],
"keywords": [
"emphasis",
"primary",
"secondary",
"ghost",
"danger",
"size",
"small",
"medium",
"large"
]
}
},
{
"kind": "states",
"items": [
{
"name": "default",
"displayName": "Default",
"description": "The button's resting state when no interaction is occurring."
},
{
"name": "hover",
"displayName": "Hover",
"description": "Triggered when the user's pointer moves over the button. The background darkens by 8% to indicate interactivity. Not applicable on touch devices.",
"tokens": {
"button-background": "color-action-primary-hover"
}
},
{
"name": "active",
"displayName": "Active / Pressed",
"description": "Triggered while the button is being pressed (mousedown or touch start). The background darkens by 16% from the default to indicate activation.",
"tokens": {
"button-background": "color-action-primary-active"
}
},
{
"name": "focus",
"displayName": "Focus",
"description": "Triggered when the button receives keyboard focus. A 2px focus ring appears with a 2px offset from the container edge.",
"tokens": {
"button-focus-ring-color": "color-focus-ring",
"button-focus-ring-width": "border-width-focus",
"button-focus-ring-offset": "space-focus-offset"
}
},
{
"name": "disabled",
"displayName": "Disabled",
"description": "The button is non-interactive. Opacity is reduced to 0.4. Pointer events are disabled. The button remains in the tab order when using aria-disabled instead of the HTML disabled attribute."
},
{
"name": "loading",
"displayName": "Loading",
"description": "The button label is replaced by a spinner animation. The button is non-interactive. The button maintains its dimensions from the default state to prevent layout shift."
}
],
"agents": {
"intent": "Document the interactive states a Button can enter at runtime and the token overrides each state applies.",
"disambiguation": [
{
"entity": "variants",
"distinction": "States are runtime conditions triggered by user interaction (hover, focus, disabled); variants are static design choices made at author time."
}
],
"keywords": [
"hover",
"active",
"focus",
"disabled",
"loading",
"pressed",
"interactive"
]
}
},
{
"kind": "design-specifications",
"properties": {
"background": "button-bg",
"text-color": "button-text",
"border-color": "button-border",
"border-width": "1px",
"border-radius": "button-radius",
"padding-horizontal": "space-4",
"padding-vertical": "space-2",
"font-family": "font-family-body",
"font-size": "14px",
"font-weight": "500",
"line-height": "20px",
"icon-size": "16px",
"icon-color": "inherit",
"icon-gap": "8px",
"focus-ring-color": "color-focus",
"focus-ring-width": "2px",
"focus-ring-offset": "2px",
"min-height": "40px",
"min-width": "64px",
"opacity": "1"
},
"spacing": {
"internal": {
"container-horizontal": "space-4",
"container-vertical": "space-2",
"icon-to-label": "space-2"
},
"external": {
"button-to-button": "space-3",
"button-group-gap": "space-3"
}
},
"sizing": {
"minWidth": "64px",
"minHeight": "40px"
},
"typography": {
"label": {
"fontSize": "14px",
"fontWeight": "500",
"lineHeight": "20px",
"typeToken": "$body-compact-01"
}
},
"responsive": [
{
"breakpoint": "small",
"description": "In narrow containers (below 320px), buttons expand to full width automatically to maintain a usable tap target."
},
{
"breakpoint": "medium",
"description": "Buttons display at their intrinsic width. Button groups display inline."
}
],
"variants": [
{
"name": "primary",
"description": "High-emphasis filled button. Solid background with light text.",
"properties": {
"background": "button-primary-bg",
"text-color": "button-primary-text",
"border-color": "transparent",
"border-width": "0px",
"icon-color": "button-primary-text"
}
},
{
"name": "secondary",
"description": "Medium-emphasis outlined button. Transparent background with visible border.",
"properties": {
"background": "transparent",
"text-color": "button-secondary-text",
"border-color": "button-secondary-border",
"border-width": "1px",
"icon-color": "button-secondary-text"
}
},
{
"name": "ghost",
"description": "Low-emphasis button with no background or border.",
"properties": {
"background": "transparent",
"text-color": "button-ghost-text",
"border-color": "transparent",
"border-width": "0px",
"icon-color": "button-ghost-text"
}
},
{
"name": "danger",
"description": "High-emphasis destructive button.",
"properties": {
"background": "button-danger-bg",
"text-color": "button-danger-text",
"border-color": "transparent",
"border-width": "0px",
"icon-color": "button-danger-text"
}
}
],
"sizes": [
{
"name": "small",
"description": "Compact size — 32px height.",
"properties": {
"min-height": "32px",
"font-size": "12px",
"line-height": "16px",
"icon-size": "14px",
"icon-gap": "4px",
"padding-horizontal": "space-3",
"padding-vertical": "space-1"
},
"spacing": {
"internal": {
"container-horizontal": "space-3",
"container-vertical": "space-1",
"icon-to-label": "space-1"
}
},
"sizing": {
"minWidth": "44px",
"minHeight": "32px"
},
"typography": {
"label": {
"fontSize": "12px",
"fontWeight": "500",
"lineHeight": "16px",
"typeToken": "$label-01"
}
}
},
{
"name": "large",
"description": "Touch-optimized — 48px height.",
"properties": {
"min-height": "48px",
"font-size": "16px",
"line-height": "24px",
"icon-size": "20px",
"icon-gap": "space-3",
"padding-horizontal": "space-5",
"padding-vertical": "space-3"
},
"spacing": {
"internal": {
"container-horizontal": "space-5",
"container-vertical": "space-3",
"icon-to-label": "space-3"
}
},
"sizing": {
"minWidth": "80px",
"minHeight": "48px"
},
"typography": {
"label": {
"fontSize": "16px",
"fontWeight": "500",
"lineHeight": "24px",
"typeToken": "$body-compact-02"
}
}
}
],
"states": [
{
"name": "hover",
"description": "Pointer over the button. Background shifts to indicate interactivity.",
"properties": {
"background": "button-hover-bg"
}
},
{
"name": "active",
"description": "Button is being pressed.",
"properties": {
"background": "button-active-bg"
}
},
{
"name": "focus",
"description": "Keyboard focus. A 2px focus ring appears with a 2px offset.",
"properties": {
"focus-ring-color": "color-focus",
"focus-ring-width": "2px",
"focus-ring-offset": "2px"
}
},
{
"name": "disabled",
"description": "Non-interactive. Opacity reduced, pointer-events disabled. Uses aria-disabled.",
"properties": {
"opacity": "0.4"
}
},
{
"name": "loading",
"description": "Label replaced by spinner. Non-interactive but retains dimensions. Spinner respects prefers-reduced-motion."
}
],
"variantStates": [
{
"variant": "primary",
"state": "hover",
"properties": {
"background": "button-primary-hover-bg"
}
},
{
"variant": "primary",
"state": "active",
"properties": {
"background": "button-primary-active-bg"
}
},
{
"variant": "danger",
"state": "hover",
"properties": {
"background": "button-danger-hover-bg"
}
},
{
"variant": "danger",
"state": "active",
"properties": {
"background": "button-danger-active-bg"
}
}
],
"agents": {
"intent": "Provide concrete token values, spacing, sizing, and typography specs for implementing the Button across all variants and sizes.",
"keywords": [
"tokens",
"spacing",
"sizing",
"typography",
"responsive",
"measurements",
"implementation"
]
}
},
{
"kind": "purpose",
"useCases": [
{
"description": "When the user needs to trigger an action such as submitting a form, saving data, opening a dialog, or confirming a decision.",
"kind": "positive"
},
{
"description": "When a destructive or irreversible action needs to be initiated, such as deleting a record or revoking access. Pair with a confirmation dialog.",
"kind": "positive"
},
{
"description": "When the action navigates the user to a different page or URL.",
"kind": "negative",
"alternative": {
"name": "link",
"rationale": "Links carry native navigation semantics. Screen readers announce them as links, and browsers support standard navigation behaviors such as open-in-new-tab."
}
},
{
"description": "When the user needs to select one option from a set of mutually exclusive choices.",
"kind": "negative",
"alternative": {
"name": "radio-group",
"rationale": "Radio groups communicate exclusivity through their semantic role. A set of buttons styled to look like a selector does not convey mutual exclusivity to assistive technology."
}
},
{
"description": "When the only content is an icon with no visible text label.",
"kind": "negative",
"alternative": {
"name": "icon-button",
"rationale": "Icon buttons enforce an aria-label requirement and apply size adjustments for icon-only touch targets. A standard button with its label removed may fail accessibility requirements silently."
}
}
],
"agents": {
"intent": "Clarify when to use Button versus similar interactive elements.",
"disambiguation": [
{
"entity": "link",
"distinction": "Button triggers an in-page action; Link navigates to a URL."
},
{
"entity": "icon-button",
"distinction": "Button always has a visible text label; IconButton is icon-only and requires aria-label."
}
],
"keywords": [
"when to use",
"use case",
"action",
"submit",
"navigation"
]
}
},
{
"kind": "guideline",
"items": [
{
"guidance": "Limit each surface to one primary button.",
"rationale": "Multiple primary buttons dilute visual hierarchy. When everything is emphasized, nothing is. A single primary button directs the user to the most important action.",
"kind": "required",
"category": "visual-design"
},
{
"guidance": "Place the primary button on the right side of a button group in left-to-right layouts.",
"rationale": "Users scan in the direction of the layout's reading order. Placing the primary action at the natural endpoint aligns with the completion point of reading.",
"kind": "encouraged",
"category": "visual-design"
},
{
"guidance": "Do not use a Button when the action navigates the user to a different page or URL. Use a Link component instead.",
"rationale": "Buttons and links have different semantic roles. Buttons trigger actions (submit, open, close). Links navigate. Screen reader users rely on element role to anticipate behavior.",
"kind": "prohibited",
"category": "visual-design"
},
{
"guidance": "Use the danger variant exclusively for destructive or irreversible actions. Pair danger buttons with a confirmation dialog.",
"rationale": "Red is a strong signal. If danger styling is used for non-destructive actions, it dilutes the warning signal and conditions users to ignore it.",
"kind": "required",
"category": "visual-design"
},
{
"guidance": "Use the loading state instead of disabling the button during asynchronous operations.",
"rationale": "A disabled button gives no feedback that an action is in progress. The loading state communicates that the action was registered and the system is working.",
"kind": "encouraged",
"category": "interaction"
},
{
"guidance": "Do not wrap a button's label text across multiple lines.",
"rationale": "Multi-line button labels are harder to scan and create inconsistent button heights in groups. If the label is too long, rewrite it to be shorter.",
"kind": "prohibited",
"category": "visual-design"
},
{
"guidance": "Maintain a minimum tap target of 44x44 CSS pixels for all button sizes.",
"rationale": "The WCAG 2.5.8 target size criterion requires a minimum 24x24px target, with 44x44px recommended. Touch devices require larger targets to prevent mis-taps.",
"kind": "required",
"category": "accessibility",
"criteria": [
{
"url": "https://www.w3.org/TR/WCAG22/#target-size-minimum"
}
]
},
{
"guidance": "Use a verb or verb phrase that describes the action the button performs. Two words maximum.",
"rationale": "Action-oriented labels set clear expectations about what will happen on activation. Short labels prevent truncation on narrow viewports.",
"kind": "required",
"category": "content",
"target": "label"
},
{
"guidance": "Use sentence case capitalization.",
"rationale": "Sentence case is easier to read than title case or all caps. It also localizes more predictably across languages where capitalization rules differ.",
"kind": "required",
"category": "content",
"target": "label"
},
{
"guidance": "When using an icon-only button (no visible label), provide an aria-label that describes the action.",
"rationale": "Screen readers announce button content as the accessible name. Without visible text, there is no accessible name. The aria-label provides one.",
"kind": "required",
"category": "accessibility",
"criteria": [
{
"url": "https://www.w3.org/TR/WCAG22/#name-role-value"
}
]
},
{
"guidance": "Use the native <button> element. Do not recreate button behavior on a <div> or <span>.",
"rationale": "Native buttons provide built-in keyboard interaction (Enter, Space), focus management, and form submission behavior. Recreating this on a non-semantic element is error-prone.",
"kind": "required",
"category": "accessibility",
"criteria": [
{
"url": "https://www.w3.org/TR/WCAG22/#name-role-value"
}
]
},
{
"guidance": "Prefer aria-disabled=\"true\" over the HTML disabled attribute when the button should remain discoverable by screen reader users.",
"rationale": "The HTML disabled attribute removes the button from the tab order, making it invisible to keyboard users. aria-disabled keeps the button focusable and announceable.",
"kind": "encouraged",
"category": "accessibility",
"criteria": [
{
"url": "https://www.w3.org/TR/WCAG22/#keyboard"
}
]
},
{
"guidance": "The focus ring must be visible in all color modes (light, dark, high contrast).",
"rationale": "Keyboard users depend on the focus indicator to track their position. If the focus ring is invisible against the background, navigation becomes impossible.",
"kind": "required",
"category": "accessibility",
"criteria": [
{
"url": "https://www.w3.org/TR/WCAG22/#focus-visible"
}
]
},
{
"guidance": "Button label text must meet a minimum 4.5:1 contrast ratio against the button background.",
"rationale": "Text contrast ensures readability for users with low vision.",
"kind": "required",
"category": "accessibility",
"criteria": [
{
"url": "https://www.w3.org/TR/WCAG22/#contrast-minimum"
}
]
}
],
"agents": {
"intent": "Enforce correct Button usage rules in generated UI code and design decisions.",
"constraints": [
{
"rule": "Never place two primary buttons on the same surface.",
"level": "must-not"
},
{
"rule": "Always pair a danger button with a confirmation dialog.",
"level": "must"
}
],
"keywords": [
"rules",
"best practices",
"visual design",
"accessibility",
"content"
]
}
},
{
"kind": "accessibility",
"wcagLevel": "AA",
"keyboardInteraction": [
{
"key": "Enter",
"action": "Activates the button."
},
{
"key": "Space",
"action": "Activates the button."
},
{
"key": "Tab",
"action": "Moves focus to the next focusable element in the tab order."
},
{
"key": "Shift+Tab",
"action": "Moves focus to the previous focusable element in the tab order."
}
],
"ariaAttributes": [
{
"attribute": "role",
"value": "button",
"description": "Applied automatically by the <button> element. Only set explicitly when using the 'as' prop to render a non-button element.",
"required": false
},
{
"attribute": "aria-disabled",
"value": "true | false",
"description": "Set to 'true' when the button is non-interactive. Preferred over the HTML disabled attribute when the button should remain focusable for screen reader discoverability.",
"required": false
},
{
"attribute": "aria-label",
"value": "string",
"description": "Provides an accessible name for icon-only buttons that lack visible text. Not needed when a visible label is present.",
"required": false
},
{
"attribute": "aria-busy",
"value": "true | false",
"description": "Set to 'true' when the button is in the loading state. Communicates to assistive technology that the button's action is in progress.",
"required": false
}
],
"screenReaderBehavior": "Announced as '[label], button'. When disabled via aria-disabled, announced as '[label], button, dimmed' (VoiceOver) or '[label], button, unavailable' (NVDA/JAWS). When loading, announced as '[label], button, busy'.",
"focusManagement": "The button participates in the normal tab order. It does not trap or redirect focus. When the button triggers a modal or popover, focus is moved to the opened element — this is the responsibility of the modal/popover component, not the button.",
"colorContrast": [
{
"foreground": "color-text-on-action",
"background": "color-action-primary",
"contrastRatio": 7.2,
"level": "AAA",
"context": "Label text on primary button background in light mode."
},
{
"foreground": "color-action-primary",
"background": "color-background-default",
"contrastRatio": 4.8,
"level": "AA",
"context": "Secondary button border/text against the default page background in light mode."
},
{
"foreground": "color-text-on-action",
"background": "color-action-danger",
"contrastRatio": 6.5,
"level": "AAA",
"context": "Label text on danger button background in light mode."
}
],
"motionConsiderations": "The loading spinner animation respects the prefers-reduced-motion media query. When reduced motion is preferred, the spinner is replaced with a static ellipsis indicator.",
"agents": {
"intent": "Provide keyboard interaction patterns, ARIA requirements, and contrast ratios for implementing an accessible Button.",
"constraints": [
{
"rule": "Use aria-disabled instead of the HTML disabled attribute when the button must remain in the tab order.",
"level": "should"
},
{
"rule": "Focus ring must be visible with a minimum 3:1 contrast ratio against the surrounding background.",
"level": "must"
}
],
"keywords": [
"WCAG",
"keyboard",
"ARIA",
"screen reader",
"focus",
"contrast",
"a11y"
]
}
}
],
"agents": {
"intent": "Trigger a user-initiated action within the current view without causing page navigation.",
"constraints": [
{
"rule": "Do not use for navigating to a different page or URL.",
"level": "must-not"
},
{
"rule": "Limit each surface to one primary-emphasis button.",
"level": "must"
},
{
"rule": "Always provide an accessible label via visible text or aria-label.",
"level": "must"
}
],
"disambiguation": [
{
"entity": "link",
"distinction": "Use button for in-page actions; use link for navigation to a URL."
},
{
"entity": "icon-button",
"distinction": "Use button when a visible text label is present; use icon-button for icon-only affordances."
}
],
"antiPatterns": [
{
"description": "Using a button to navigate to another page.",
"instead": "Use a link element with href."
},
{
"description": "Placing multiple primary buttons on the same surface.",
"instead": "Use one primary and one or more secondary or tertiary buttons."
}
],
"keywords": [
"action",
"submit",
"click",
"CTA",
"trigger",
"call-to-action",
"interactive"
]
},
"$extensions": {
"com.designTool": {
"componentId": "abc123def456"
}
}
}
Design System Documentation Standard (DSDS) 0.1 — Draft Specification
GitHub