# yaml-language-server: $schema=https://opencomponents.dev/schemas/contract.json
# The Checkbox contract, with every rule from its checklist.
# The page explains why each one exists: https://opencomponents.dev/raw/docs/components/checkbox.md

component: Checkbox
summary: Says yes or no to one thing, like agreeing to the terms. The label it wraps names it.
standard: https://opencomponents.dev/docs/components/checkbox
element: input # type="checkbox", inside the label that names it, which takes class and style
role: checkbox
props:
  size: { type: [xs, sm, md, lg, xl], default: md }
  checked: { type: boolean, default: false } # when it's bound. Without it, the box keeps its own
  indeterminate: { type: boolean, default: false } # set as a property. The first click clears it, and a bound value with it
  disabled: { type: boolean, default: false }
  readonly: { type: boolean, default: false } # sets aria-readonly, since HTML ignores readonly on a checkbox, and cancels the click
slots:
  default: The label, which names the checkbox. A press on it toggles the box, unless it's read-only.
events:
  change: Event, from the <input>. Fires when people check or uncheck it.
states:
  hover: ":hover, in @media (hover: hover)"
  focus: "> input:focus-visible"
  checked: "> input:checked"
  indeterminate: "> input:indeterminate"
  disabled: ":has(> input:disabled), which a <fieldset disabled> sets too"
  read-only: "> input[aria-readonly=true], never :read-only"
  invalid: "> input[aria-invalid=true], never :invalid"
keyboard:
  Tab: Moves focus to the box.
  Space: Checks or unchecks it, and clears the indeterminate state, unless it's read-only.
  Enter: Never toggles it.
parts: [box, indicator, label] # data-slot, in this order. The box is the <input>
tokens:
  theme: ["--color--neutral-text", "--color--primary", "--color--primary-contrast", "--color--error-text", "--color--focus"]
  variables: [--checkbox--box--size, --checkbox--box--radius, --checkbox--box--border-color, --checkbox--box--fill, --checkbox--indicator--color, --checkbox--gap, --checkbox--font-size, --checkbox--hit-area--size]
  layer: components # in Tailwind's order: theme, base, components, utilities
rules:
  - id: checkbox/contrast
    layer: ui
    level: must
    scope: component
    requirement: "The label has a contrast of 4.5:1, and the focus ring has 3:1 against the page"
    basis: "WCAG 1.4.3, 1.4.11 (AA)"
    check: "axe `color-contrast`, contrast checker"
  - id: checkbox/boundary-contrast
    layer: ui
    level: must
    scope: component
    requirement: "The box's border and a checked box's fill have 3:1 against the page, and the check and the dash have 3:1 against the fill"
    basis: "WCAG 1.4.11 (AA)"
    check: "Contrast checker"
  - id: checkbox/focus-visible
    layer: ui
    level: must
    scope: component
    requirement: "Keyboard focus shows an `outline` at least 2px thick around the box, outside its border, on `:focus-visible`"
    basis: "WCAG 2.4.7 (AA), 2.4.13 (AAA)"
    check: "Keyboard"
  - id: checkbox/target-size
    layer: ui
    level: must
    scope: component
    requirement: "The box has a hit area of at least 24 × 24px that toggles it, without counting its label"
    basis: "Open Components, beyond WCAG 2.5.8 (AA)"
    check: "axe `target-size`, emulation"
  - id: checkbox/distinct-marks
    layer: ui
    level: must
    scope: component
    requirement: "Checked shows a check and indeterminate a dash, so the two never rely on colour to tell them apart"
    basis: "WCAG 1.4.1 (A)"
    check: "Visual regression test"
  - id: checkbox/resizable
    layer: ui
    level: must
    scope: component
    requirement: "The box, its gap and its hit area are sized in `rem`, and the label wraps beside the box instead of being clipped"
    basis: "Open Components, beyond WCAG 1.4.4, 1.4.12 (AA)"
    check: "200% zoom"
  - id: checkbox/stable-size
    layer: ui
    level: must
    scope: component
    requirement: "No state changes the checkbox's size or position"
    basis: "Open Components"
    check: "Visual regression test"
  - id: checkbox/state-selectors
    layer: ui
    level: should
    scope: component
    requirement: "States are styled from the `<input>`, with `:checked`, `:indeterminate`, `[aria-readonly]`, `:disabled` and `[aria-invalid]`, and never from `:invalid` or `:read-only`"
    basis: "Open Components"
    check: "Review"
  - id: checkbox/hover-capable
    layer: ui
    level: should
    scope: component
    requirement: "Hover styles only apply inside `@media (hover: hover)`"
    basis: "Open Components"
    check: "Review"
  - id: checkbox/forced-colors
    layer: ui
    level: must
    scope: component
    requirement: "The box, its check or dash, and its focus, read-only and disabled states stay visible in forced colours mode"
    basis: "Open Components"
    check: "Emulation"
  - id: checkbox/more-contrast
    layer: ui
    level: should
    scope: component
    requirement: "The box's border is drawn at full strength when `prefers-contrast: more` is set"
    basis: "Open Components"
    check: "Emulation"
  - id: checkbox/native-element
    layer: ux
    level: must
    scope: component
    requirement: "It's a native `<input type=\"checkbox\">`, never an element with `role=\"checkbox\"`"
    basis: "Open Components, beyond WCAG 2.1.1, 4.1.2 (A)"
    check: "Unit test"
  - id: checkbox/label-toggles
    layer: ux
    level: must
    scope: component
    requirement: "Its `<label>` wraps the box and its text, so the text names it and a press on the text toggles it, unless it's read-only"
    basis: "WCAG 1.3.1, 4.1.2 (A), HTML"
    check: "Unit test"
  - id: checkbox/visible-label
    layer: ux
    level: must
    scope: usage
    requirement: "Every checkbox has visible text, unless its context names it, like a row in a table, in which case it has an `aria-label`"
    basis: "WCAG 1.3.1, 3.3.2, 4.1.2 (A)"
    check: "axe `label`"
  - id: checkbox/label-in-name
    layer: ux
    level: must
    scope: usage
    requirement: "An `aria-label` or `aria-labelledby` includes the visible text, ideally at the start"
    basis: "WCAG 2.5.3 (A)"
    check: "axe `label-content-name-mismatch`"
  - id: checkbox/no-interactive-label
    layer: ux
    level: should
    scope: usage
    requirement: "The label holds no links or buttons, which go in the hint or before the checkbox"
    basis: "Open Components"
    check: "Review"
  - id: checkbox/positive-label
    layer: ux
    level: should
    scope: usage
    requirement: "The label says what checking the box means, in the positive, like \"Email me updates\""
    basis: "Open Components"
    check: "Review"
  - id: checkbox/described
    layer: ux
    level: should
    scope: usage
    requirement: "Hints sit outside the label and are linked with `aria-describedby`"
    basis: "WCAG 1.3.1 (A)"
    check: "Screen reader"
  - id: checkbox/decorative-indicator
    layer: ux
    level: must
    scope: component
    requirement: "The check and the dash are hidden from assistive technologies with `aria-hidden`, so the name is the label alone"
    basis: "WCAG 1.1.1, 4.1.2 (A)"
    check: "Unit test"
  - id: checkbox/keyboard
    layer: ux
    level: must
    scope: component
    requirement: "`Tab` reaches the box, `Space` checks or unchecks it unless it's read-only, and `Enter` never toggles it"
    basis: "WCAG 2.1.1 (A), APG"
    check: "Unit test"
  - id: checkbox/indeterminate
    layer: ux
    level: must
    scope: component
    requirement: "The mixed state is set as the `<input>`'s `indeterminate` property, so it's announced as mixed, and it clears on the first click"
    basis: "WCAG 4.1.2 (A), HTML"
    check: "Unit test"
  - id: checkbox/derived-mixed
    layer: ux
    level: should
    scope: usage
    requirement: "`indeterminate` is worked out from the options it stands for whenever they change"
    basis: "Open Components"
    check: "Review"
  - id: checkbox/no-immediate-action
    layer: ux
    level: should
    scope: usage
    requirement: "Checking a box changes nothing until its form is sent, and a setting that takes effect at once uses a Switch"
    basis: "Open Components"
    check: "Review"
  - id: checkbox/required
    layer: ux
    level: should
    scope: both
    requirement: "A box people have to check has `required`, which lands on the `<input>`, while optional boxes don't"
    basis: "HTML, WCAG 3.3.2 (A)"
    check: "Unit test, review"
  - id: checkbox/error-message
    layer: ux
    level: must
    scope: usage
    requirement: "An invalid checkbox has an error message, in text and linked with `aria-describedby`, that says why it needs checking"
    basis: "WCAG 1.4.1, 3.3.1 (A), 3.3.3 (AA)"
    check: "Screen reader"
  - id: checkbox/invalid-state
    layer: ux
    level: must
    scope: both
    requirement: "A checkbox sets `aria-invalid=\"true\"` while its error message shows, and only looks invalid when it does"
    basis: "WCAG 4.1.2 (A), Open Components"
    check: "Unit test"
  - id: checkbox/read-only
    layer: ux
    level: must
    scope: component
    requirement: "A read-only checkbox sets `aria-readonly=\"true\"`, stays focusable and is sent with its form, while a click, its label or `Space` never change it"
    basis: "WAI-ARIA, Open Components"
    check: "Unit test"
  - id: checkbox/read-only-values
    layer: ux
    level: should
    scope: usage
    requirement: "A choice people need to see but can't change is read-only rather than disabled, and says why next to it"
    basis: "Open Components"
    check: "Review"
  - id: checkbox/disabled-inert
    layer: ux
    level: must
    scope: component
    requirement: "A disabled checkbox, or one inside a `<fieldset disabled>`, can't be focused, toggled or submitted, and looks disabled"
    basis: "HTML"
    check: "Unit test"
  - id: checkbox/explain-disabled
    layer: ux
    level: should
    scope: usage
    requirement: "A disabled checkbox explains why next to it, or is left out until it applies"
    basis: "Open Components"
    check: "Review"
  - id: checkbox/focus-not-obscured
    layer: ux
    level: must
    scope: usage
    requirement: "Sticky headers and footers never cover a focused checkbox, for example thanks to `scroll-padding`"
    basis: "WCAG 2.4.11 (AA), 2.4.12 (AAA)"
    check: "Keyboard"
  - id: checkbox/mirrors-in-rtl
    layer: ux
    level: should
    scope: component
    requirement: "The box sits at the start of the line in either direction, with logical properties and a grid that follows `dir`"
    basis: "Open Components"
    check: "Review"
  - id: checkbox/shared-vocabulary
    layer: dx
    level: must
    scope: component
    requirement: "It uses the shared names: `size`, `disabled`, `readonly`, `checked`, `indeterminate`, the default slot for its label and `change`"
    basis: "Open Components"
    check: "Review"
  - id: checkbox/typed
    layer: dx
    level: must
    scope: component
    requirement: "Props, events and slots are typed, with unions rather than `string`"
    basis: "Open Components"
    check: "Type check"
  - id: checkbox/control-attributes
    layer: dx
    level: must
    scope: component
    requirement: "Attributes, listeners and refs land on the `<input>`, while classes and styles land on the `<label>`"
    basis: "Open Components"
    check: "Unit test"
  - id: checkbox/form-control
    layer: dx
    level: must
    scope: component
    requirement: "Its form sends its `name` with its `value` while it's checked, and nothing while it's unchecked, whether `checked` is bound or not"
    basis: "HTML"
    check: "Unit test"
  - id: checkbox/controllable
    layer: dx
    level: should
    scope: component
    requirement: "`checked` can be bound or left to the browser, a bound box always shows the state you hold, and every other state is a prop or an attribute"
    basis: "Open Components"
    check: "Unit test"
  - id: checkbox/role-and-name
    layer: ax
    level: must
    scope: component
    requirement: "It can be found by its role and label alone, with `getByRole(\"checkbox\", { name })`, and reports checked, unchecked or mixed"
    basis: "WCAG 4.1.2 (A)"
    check: "Unit test"
  - id: checkbox/deterministic-dom
    layer: ax
    level: must
    scope: component
    requirement: "The same props render the same markup, with no generated IDs, and states change properties and attributes, never elements"
    basis: "Open Components"
    check: "Unit test"
  - id: checkbox/data-attributes
    layer: ax
    level: should
    scope: component
    requirement: "The size is mirrored as `data-size`, and parts are marked with `data-slot`"
    basis: "Open Components"
    check: "Unit test"
  - id: checkbox/dev-warnings
    layer: ax
    level: should
    scope: component
    requirement: "It warns during development when a checkbox has no label and no `aria-label`"
    basis: "Open Components"
    check: "Unit test"
