# The Button contract, with every rule from its checklist.
# The page explains why each one exists: https://opencomponents.dev/raw/docs/components/button.md

component: Button
summary: Runs an action in place. Renders a link when given an href.
standard: https://opencomponents.dev/docs/components/button
element: button # a, with href
role: button # link, with href
props:
  variant: { type: [solid, outline, soft, subtle, ghost, link], default: solid }
  color: { type: [primary, secondary, neutral, success, info, warning, error], default: neutral }
  size: { type: [xs, sm, md, lg, xl], default: md }
  label: { type: string }
  iconOnly: { type: boolean, default: false }
  type: { type: [button, submit, reset], default: button }
  href: { type: string }
  disabled: { type: boolean, default: false }
  focusableWhenDisabled: { type: boolean, default: false }
  loading: { type: boolean, default: false }
  loadingLabel: { type: string, default: Loading }
slots:
  default: The label.
  leading: An icon before the label, and the icon of an icon-only button.
  trailing: An icon after the label.
events:
  click: MouseEvent. Never emitted while disabled or loading.
states:
  hover: ":hover, in @media (hover: hover)"
  active: ":active"
  focus: ":focus-visible"
  disabled: "[disabled], or [aria-disabled=true] when focusableWhenDisabled"
  loading: "[aria-disabled=true][data-loading]"
  pressed: "[aria-pressed=true]"
  expanded: "[aria-expanded=true]"
keyboard:
  Enter: Activates it. Links too.
  Space: Activates it, on release. Buttons only.
parts: [leading, label, trailing, spinner] # data-slot, in this order
tokens:
  theme: ["--color--{color}", "--color--{color}-contrast", "--color--{color}-text", "--color--focus"]
  variables: [--button--height, --button--padding, --button--gap, --button--font-size, --button--icon--size, --button--radius]
  layer: components # in Tailwind's order: theme, base, components, utilities
rules:
  - id: button/intent-colors
    layer: ui
    level: must
    scope: both
    requirement: "Colours describe the action's intent rather than a hue, and never carry that meaning on their own"
    check: "Review"
  - id: button/one-primary
    layer: ui
    level: should
    scope: usage
    requirement: "There's one solid primary button per view or group, and the rest are ranked with `variant`"
    check: "Review"
  - id: button/contrast
    layer: ui
    level: must
    scope: component
    requirement: "The label has a contrast of 4.5:1 at rest, on hover and when pressed, in every variant, and the focus ring has 3:1 against the page"
    check: "axe `color-contrast`"
  - id: button/link-underlined
    layer: ui
    level: must
    scope: component
    requirement: "The `link` variant is underlined at rest, so it doesn't rely on colour alone to stand out from the text around it"
    check: "Review"
  - id: button/focus-visible
    layer: ui
    level: must
    scope: component
    requirement: "Keyboard focus shows a 2px `outline`, offset from the button, on `:focus-visible`"
    check: "Keyboard"
  - id: button/target-size
    layer: ui
    level: must
    scope: component
    requirement: "The button is at least 24 × 24px, unless it's a `link` inside a sentence, with a 44 × 44px hit area on touch screens"
    check: "axe `target-size`"
  - id: button/resizable
    layer: ui
    level: must
    scope: component
    requirement: "The button is sized with `min-height` and padding in `rem`, and its label wraps instead of being clipped or truncated"
    check: "200% zoom"
  - id: button/stable-size
    layer: ui
    level: must
    scope: component
    requirement: "No state changes the button's size or position"
    check: "Visual regression test"
  - id: button/state-selectors
    layer: ui
    level: should
    scope: component
    requirement: "States are styled from the attributes that expose them"
    check: "Review"
  - id: button/hover-capable
    layer: ui
    level: should
    scope: component
    requirement: "Hover styles only apply inside `@media (hover: hover)`"
    check: "Review"
  - id: button/forced-colors
    layer: ui
    level: must
    scope: component
    requirement: "The button's boundary, focus, disabled and pressed states stay visible in forced colours mode"
    check: "Emulation"
  - id: button/more-contrast
    layer: ui
    level: should
    scope: component
    requirement: "Borders are drawn at full strength when `prefers-contrast: more` is set"
    check: "Emulation"
  - id: button/reduced-motion
    layer: ui
    level: must
    scope: component
    requirement: "Nothing turns or slides when `prefers-reduced-motion: reduce` is set"
    check: "Emulation"
  - id: button/native-element
    layer: ux
    level: must
    scope: component
    requirement: "It's a native `<button>`, or an `<a href>` for navigation, but never a `<div>` or a `<span>`"
    check: "Unit test"
  - id: button/links-navigate
    layer: ux
    level: must
    scope: usage
    requirement: "Navigation always uses a link, so a button never changes the URL"
    check: "Review"
  - id: button/accessible-name
    layer: ux
    level: must
    scope: both
    requirement: "Every button has a name, and an icon-only button has a visually hidden label"
    check: "axe `button-name`, unit test"
  - id: button/label-in-name
    layer: ux
    level: must
    scope: usage
    requirement: "An `aria-label` or `aria-labelledby` includes the visible label, ideally at the start"
    check: "axe `label-content-name-mismatch`"
  - id: button/decorative-icons
    layer: ux
    level: must
    scope: component
    requirement: "Icons are hidden from assistive technologies with `aria-hidden`, so they don't add to the button's name"
    check: "Unit test"
  - id: button/verb-labels
    layer: ux
    level: should
    scope: usage
    requirement: "Labels start with a verb and name the outcome, and end with `…` when more input follows"
    check: "Review"
  - id: button/keyboard
    layer: ux
    level: must
    scope: component
    requirement: "`Enter` and `Space` activate a button, while `Enter` follows a link"
    check: "Unit test"
  - id: button/activate-on-click
    layer: ux
    level: must
    scope: usage
    requirement: "Actions run on `click`, never on `mousedown`, `pointerdown` or `touchstart`"
    check: "Review"
  - id: button/no-nesting
    layer: ux
    level: must
    scope: usage
    requirement: "There's nothing interactive inside a button, and no button inside a link"
    check: "axe `nested-interactive`"
  - id: button/dom-order
    layer: ux
    level: must
    scope: usage
    requirement: "Buttons come in the same order in the DOM as on screen, without `order` or `row-reverse`"
    check: "Keyboard"
  - id: button/disabled-inert
    layer: ux
    level: must
    scope: component
    requirement: "A disabled or loading button does nothing, so there's no event, no submission and no navigation"
    check: "Unit test"
  - id: button/explain-disabled
    layer: ux
    level: should
    scope: usage
    requirement: "An enabled button with validation is preferred over a disabled one, and a disabled one explains why"
    check: "Review"
  - id: button/keep-focus
    layer: ux
    level: must
    scope: component
    requirement: "A focused button is never natively disabled, which is why loading uses `aria-disabled`"
    check: "Unit test"
  - id: button/loading
    layer: ux
    level: must
    scope: component
    requirement: "Loading keeps the button's size and name, blocks repeated activation and is announced"
    check: "Unit test"
  - id: button/announce-outcome
    layer: ux
    level: should
    scope: usage
    requirement: "The page announces the action's outcome with a status message"
    check: "Screen reader"
  - id: button/toggle-pressed
    layer: ux
    level: must
    scope: both
    requirement: "A toggle button sets `aria-pressed` and keeps its label"
    check: "Unit test"
  - id: button/popup-expanded
    layer: ux
    level: must
    scope: both
    requirement: "A button that opens a popup sets `aria-haspopup` and `aria-expanded`"
    check: "Unit test"
  - id: button/focus-after
    layer: ux
    level: should
    scope: usage
    requirement: "After activation, focus moves as the APG describes, and never falls back to the top of the page"
    check: "Keyboard"
  - id: button/focus-not-obscured
    layer: ux
    level: must
    scope: usage
    requirement: "Sticky headers and footers never cover a focused button, for example thanks to `scroll-padding`"
    check: "Keyboard"
  - id: button/destructive
    layer: ux
    level: should
    scope: usage
    requirement: "A destructive action uses `error`, names what it destroys, and can either be undone or asks for confirmation"
    check: "Review"
  - id: button/mirrors-in-rtl
    layer: ux
    level: should
    scope: both
    requirement: "It uses logical properties, and icons that point along the reading direction flip in right-to-left layouts"
    check: "Review"
  - id: button/shared-vocabulary
    layer: dx
    level: must
    scope: component
    requirement: "It uses the shared names: `variant`, `color`, `size`, `label`, `disabled`, `loading`, `leading`, `trailing` and `click`"
    check: "Review"
  - id: button/typed
    layer: dx
    level: must
    scope: component
    requirement: "Props, events and slots are typed, with unions rather than `string`"
    check: "Type check"
  - id: button/explicit-type
    layer: dx
    level: must
    scope: both
    requirement: "`type` defaults to `\"button\"`, and submit buttons say `type=\"submit\"` explicitly"
    check: "Unit test"
  - id: button/root-element
    layer: dx
    level: must
    scope: component
    requirement: "The root element is the button itself, so attributes, listeners and refs land on it"
    check: "Unit test"
  - id: button/stateless
    layer: dx
    level: should
    scope: component
    requirement: "Every state is a prop or an attribute that the parent controls"
    check: "Review"
  - id: button/role-and-name
    layer: ax
    level: must
    scope: component
    requirement: "It can be found by its role and name alone, with `getByRole(\"button\", { name })`"
    check: "Unit test"
  - id: button/deterministic-dom
    layer: ax
    level: must
    scope: component
    requirement: "The same props render the same markup, and states change attributes, never elements"
    check: "Unit test"
  - id: button/data-attributes
    layer: ax
    level: should
    scope: component
    requirement: "Props are mirrored as `data-*` attributes, and parts are marked with `data-slot`"
    check: "Unit test"
  - id: button/dev-warnings
    layer: ax
    level: should
    scope: component
    requirement: "It warns during development when a button has no accessible name"
    check: "Unit test"
