diff --git a/.specs/checkbox.md b/.specs/checkbox.md
index 3f991f52..3ff9fb8d 100644
--- a/.specs/checkbox.md
+++ b/.specs/checkbox.md
@@ -2,36 +2,55 @@
name: checkbox
category: inputs
structure: monolithic
-status: implemented
-spec_version: 2
-checksum: d0a27b8ca2563f33c09954c4657f76af2b4edab6e87bb5367d0ab69d366a2acb
+status: approved
+spec_version: 3
+checksum: 2fd0e74289221cb07d676ea9cecad5c547ffedc51452a69e0e9e552b05ab2cb3
created: 2026-05-22
-last_updated: 2026-05-22
+last_updated: 2026-06-24
---
+
# Checkbox — Component Spec
## Purpose
-Collects or edits user input. Migrated from the existing implementation at `packages/webkit/src/components/webkit/inputs/checkbox/`.
+Square checkbox control only — the box, the native input, and the check glyph, with no label or description chrome. Use it when you compose your own label, or reach for `FieldCheckbox` / `FieldCheckboxBlock` when you want built-in text. Supports a single boolean (`binary`) or collecting a `value` into an array model.
+
+## Usage
+
+```vue
+
+
+
+
+
+```
## Props
| Prop | Type | Default | Required | JSDoc |
|---|---|---|---|---|
-| `modelValue` | `string` | `'undefined'` | no | model Value. |
-| `value` | `string` | `'undefined'` | no | value. |
-| `binary` | `boolean` | `false` | no | binary. |
+| `modelValue` | `unknown` | `undefined` | no | Bound value: a boolean in `binary` mode, otherwise the array/value the control reflects. |
+| `value` | `unknown` | `undefined` | no | This checkbox's value, added to or removed from the model array when not `binary`. |
+| `binary` | `boolean` | `false` | no | Toggles a single boolean instead of collecting `value` into an array. |
| `disabled` | `boolean` | `false` | no | Disables interaction and applies disabled tokens. |
-| `readonly` | `boolean` | `false` | no | readonly. |
-| `inputId` | `string` | `'undefined'` | no | input Id. |
+| `readonly` | `boolean` | `false` | no | Prevents changes while the control stays focusable. |
+| `inputId` | `string` | `undefined` | no | Forwarded to the native input `id`; pair with an external label's `for`. |
| `name` | `string` | `undefined` | no | HTML name for form submission. |
-| `tabindex` | `number` | `'undefined'` | no | tabindex. |
+| `tabindex` | `number \| undefined` | `undefined` | no | Tab order for the native input. |
## Events
| Event | Payload | Notes |
|---|---|---|
-| `update:modelValue` | `unknown` | — |
+| `update:modelValue` | `unknown` | Emitted on toggle; payload is the next boolean (`binary`) or the updated value array. |
## Slots
@@ -39,25 +58,32 @@ Collects or edits user input. Migrated from the existing implementation at `pack
## States
-- Visual states: `default`, `hover`, `focus-visible`, `active`, `disabled`
+- Visual states: `default`, `hover`, `focus-visible`, `active`, `disabled`, `readonly`, `checked`
+- `data-checked` mirrors the checked state
- `data-disabled` mirrors the `disabled` prop
+- `data-readonly` mirrors the `readonly` prop
## Motion & Animations
-| Trigger | Animation / Transition | Token | Reduced-motion fallback |
+| Trigger | Animation / Transition | Token (see `.claude/docs/DESIGN.md` § Animations) | Reduced-motion fallback |
|---|---|---|---|
-| state change | `transition-colors duration-150 ease-out` | inline | `motion-reduce:transition-none` |
+| hover / active state change | `transition-opacity duration-fast-02 ease-productive-entrance` on the hover/active ghost overlays | semantic (matches catalog) | `motion-reduce:transition-none` |
## Tokens
| Region | Token (DESIGN.md) |
|---|---|
-| typography | .text-body-sm |
| surface | `var(--bg-surface)` |
-| text | `var(--text-default)` |
-| spacing | `var(--spacing-3)` |
-| shape | `var(--shape-elements)` |
+| border | `var(--border-default)` |
+| checked surface | `var(--primary)` |
+| checked glyph | `var(--primary-contrast)` |
+| shape | `var(--shape-button)` |
+| hover overlay | `var(--bg-hover)` |
+| active overlay | `var(--bg-active)` |
+| disabled surface | `var(--bg-disabled)` |
+| disabled glyph | `var(--text-disabled)` |
| ring | `var(--ring-color)` |
+| ring offset | `var(--bg-canvas)` |
## Theme gaps
@@ -67,17 +93,17 @@ Collects or edits user input. Migrated from the existing implementation at `pack
## Accessibility (WCAG 2.1 AA)
-- Visible focus: `focus-visible:ring-2 focus-visible:ring-[var(--ring-color)] focus-visible:ring-offset-2 focus-visible:ring-offset-[var(--bg-canvas)]`
-- Keyboard map: `Tab` focuses; `Enter`/`Space` activates; `Escape` closes overlays where applicable.
-- ARIA: root uses appropriate roles (`button`, `dialog`, `status`, etc.) per sub-component.
-- Contrast ≥4.5:1 (text) / ≥3:1 (large + icons), including disabled state.
-- `motion-reduce:transition-none motion-reduce:transform-none` on animated states.
-- Touch target ≥40×40 px where the control is interactive.
+- Visible focus: descendant native input drives the ring — `has-[:focus-visible]:ring-2 has-[:focus-visible]:ring-[var(--ring-color)] has-[:focus-visible]:ring-offset-1 has-[:focus-visible]:ring-offset-[var(--bg-canvas)]`
+- Keyboard map: `Tab` focuses the native input; `Space` toggles checked state (native checkbox behavior).
+- ARIA: renders a real `` with `aria-checked`; control-only, so associate copy via `inputId` + an external label `for`, or use `FieldCheckbox`.
+- Contrast ≥4.5:1 (text) / ≥3:1 (large + icons), including the disabled state.
+- `motion-reduce:transition-none` on the animated ghost overlays.
+- Touch target: the box is 1.125rem (18px) — justified deviation for a control-only primitive; the larger ≥40×40 px hit area is provided by the field wrappers (`FieldCheckbox` / `FieldCheckboxBlock`) or the consumer's label.
## Stories (Storybook)
- Default
-- Disabled
+- Disabled — demonstrates the `disabled` prop (checked and unchecked).
## Constraints — DO NOT
@@ -94,7 +120,8 @@ Collects or edits user input. Migrated from the existing implementation at `pack
- Do not inherit artifacts as-is from another design system, Figma file, library, or pre-existing `CONTRACT.md` / `README.md`. Rewrite to our conventions. See `.claude/rules/migration.md`.
- Do not add Figma references to Storybook stories. No `parameters.design`, no `parameters.figma`, no Figma URLs in `docs.description.*`, no `@storybook/addon-designs` import. The Figma link is owned by `.figma.ts` (Code Connect). See `.claude/docs/COMPONENT_REQUIREMENTS.md`.
- Do not use `parameters.actions.argTypesRegex` (deprecated in Storybook 8 and silently misroutes Vue 3 emits) or `parameters.actions.handles` (DOM-only). Declare every event explicitly in `argTypes` with a camelCase `on` key and `{ action: '' }`. Do not use the legacy CSF2 `Name.args = {...}` form — always object-style CSF3.
-- Do not add bespoke Storybook stories beyond Default + per `kind` + per `size` + Disabled, unless the spec's "Stories (Storybook)" section explicitly justifies the addition.
+- Do not add bespoke Storybook stories beyond Default + Types + Sizes + state stories (`Loading`, `Disabled`) for the props the component actually declares, unless the spec's "Stories (Storybook)" section explicitly justifies the addition. Do not split Types/Sizes into one-story-per-variant — the composite stories are the canonical pattern.
+- Do not duplicate the `## Usage` block from the spec inside the Storybook story body. The block is injected once into `parameters.docs.description.component` by the storybook-write skill; copy it nowhere else.
- Do not edit `.claude/docs/DESIGN.md`, `.claude/docs/COMPONENT_REQUIREMENTS.md`, or `.claude/docs/PRIMEVUE_ABSTRACTION.md`.
- Do not edit the root `package.json` or `.github/workflows/*`.
- Do not change `structure` after `status: approved`. To change structure, bump `spec_version` and re-author the spec.
diff --git a/apps/storybook/src/stories/components/inputs/Checkbox.stories.js b/apps/storybook/src/stories/components/inputs/Checkbox.stories.js
index a29e3f65..013409dc 100644
--- a/apps/storybook/src/stories/components/inputs/Checkbox.stories.js
+++ b/apps/storybook/src/stories/components/inputs/Checkbox.stories.js
@@ -1,14 +1,13 @@
-import { ref } from 'vue'
-
import Checkbox from '@aziontech/webkit/checkbox'
+import { ref, watch } from 'vue'
/** @type {import('@storybook/vue3').Meta} */
const meta = {
- title: 'Components/Inputs/Checkbox',
+ title: 'Components/Inputs/Checkbox',
component: Checkbox,
tags: ['autodocs'],
parameters: {
- layout: 'padded',
+ layout: 'centered',
backgrounds: { default: 'dark' },
a11y: {
config: {
@@ -20,25 +19,44 @@ const meta = {
},
docs: {
description: {
- component:
- 'Control only — the square checkbox input with no label or description. Pair with an external label or use FieldCheckbox / FieldCheckboxBlock for built-in text.'
+ component: [
+ 'Square checkbox control only — the box, the native input, and the check glyph, with no label or description chrome. Use it when you compose your own label, or reach for `FieldCheckbox` / `FieldCheckboxBlock` when you want built-in text. Supports a single boolean (`binary`) or collecting a `value` into an array model.',
+ '',
+ '## Usage',
+ '',
+ '```vue',
+ '',
+ '',
+ '',
+ ' ',
+ '',
+ '```'
+ ].join('\n')
}
}
},
argTypes: {
modelValue: {
control: 'boolean',
- description: 'model Value.',
+ description: 'Bound value: a boolean in binary mode, otherwise the array/value the control reflects.',
table: { type: { summary: 'unknown' }, category: 'props' }
},
value: {
control: false,
- description: 'value.',
+ description: "This checkbox's value, added to or removed from the model array when not binary.",
table: { type: { summary: 'unknown' }, category: 'props' }
},
binary: {
control: 'boolean',
- description: 'binary.',
+ description: 'Toggles a single boolean instead of collecting value into an array.',
table: { type: { summary: 'boolean' }, defaultValue: { summary: 'false' }, category: 'props' }
},
disabled: {
@@ -48,12 +66,12 @@ const meta = {
},
readonly: {
control: 'boolean',
- description: 'readonly.',
+ description: 'Prevents changes while the control stays focusable.',
table: { type: { summary: 'boolean' }, defaultValue: { summary: 'false' }, category: 'props' }
},
inputId: {
control: 'text',
- description: 'input Id.',
+ description: 'Forwarded to the native input id; pair with an external label for attribute.',
table: { type: { summary: 'string' }, category: 'props' }
},
name: {
@@ -63,76 +81,68 @@ const meta = {
},
tabindex: {
control: 'number',
- description: 'tabindex.',
- table: { type: { summary: 'number' }, category: 'props' }
+ description: 'Tab order for the native input.',
+ table: { type: { summary: 'number | undefined' }, category: 'props' }
},
'onUpdate:modelValue': {
action: 'update:modelValue',
- description: 'Emitted when the value changes.',
+ description: 'Emitted on toggle; payload is the next boolean (binary) or the updated value array.',
table: { type: { summary: 'unknown' }, category: 'events' }
}
+ },
+ args: {
+ binary: true,
+ disabled: false,
+ readonly: false
}
}
export default meta
-export const Default = {
- render: () => ({
- components: { Checkbox },
- setup() {
- const value = ref(false)
- return { value }
- },
- template: `
-
- `
- }),
- parameters: {
- docs: {
- description: {
- story: 'Binary control only — associate copy via an external label or FieldCheckbox.'
+const Template = (args) => ({
+ components: { Checkbox },
+ setup() {
+ const value = ref(args.modelValue ?? false)
+
+ watch(
+ () => args.modelValue,
+ (next) => {
+ value.value = next ?? false
}
+ )
+
+ const onUpdate = (next) => {
+ value.value = next
+ args['onUpdate:modelValue']?.(next)
}
+
+ return { args, value, onUpdate }
+ },
+ template: ''
+})
+
+/** @type {import('@storybook/vue3').StoryObj} */
+export const Default = {
+ render: Template,
+ parameters: {
+ docs: { description: { story: 'Binary control only — associate copy via an external label or FieldCheckbox.' } }
}
}
export const Disabled = {
- render: () => ({
+ render: (args) => ({
components: { Checkbox },
setup() {
- const checked = ref(true)
- const unchecked = ref(false)
- return { checked, unchecked }
+ return { args }
},
template: `
-
-
+
+
`
}),
parameters: {
- docs: {
- description: {
- story: 'Disabled checked and unchecked controls without label chrome.'
- }
- }
+ docs: { description: { story: 'Disabled control in both checked and unchecked states.' } }
}
}
diff --git a/packages/webkit/src/components/inputs/checkbox/checkbox.vue b/packages/webkit/src/components/inputs/checkbox/checkbox.vue
index 57b5a067..69a1e919 100644
--- a/packages/webkit/src/components/inputs/checkbox/checkbox.vue
+++ b/packages/webkit/src/components/inputs/checkbox/checkbox.vue
@@ -1,30 +1,27 @@