Skip to content

Skills · experience-design

design-system-spec

Write a design system component spec — purpose, anatomy, variants, every state (default, hover, focus, active, disabled, error, loading), the design tokens each part uses by name, behaviour, content rules, accessibility (keyboard, focus order, ARIA role, contrast, target size) cited to WCAG 2.2 criteria, do/don't examples, and the open questions engineering must answer. Use when the user wants a component spec, design system documentation, "spec out this button / modal / text field", a component's states and tokens, or a design-to-engineering hand-off for a component.

Pack experience-designTier standardJob Design the experiencev0.1.0

npx skilldrop-cli install design-system-spec
Other ways to install

With the sibling skills it hands off to:

npx skilldrop-cli install design-system-spec --with-related

The whole pack it belongs to:

npx skilldrop-cli install --pack experience-design

By hand, from a clone of the repo:

cp -R packs/experience-design/skills/design-system-spec ~/.claude/skills/

In Claude Code, with the experience-design plugin installed, invoke it as:

/experience-design:design-system-spec

Try it

A realistic prompt from the skill's acceptance evals:

Write the component spec for Acme's modal dialog. It's used for confirmations (delete project), short forms (rename), and a long terms-and-conditions view that scrolls. Our tokens include color.background.surface, color.background.overlay, color.border.subtle, color.text.default, space.200, space.300, radius.large, font.heading.md, font.body.md, shadow.overlay. Web, built in Vue.

What a good result looks like

Example output

a text field with async validation · the skill's own worked example, source

Input

Spec our text field for the Northwind design system. Used in sign-up (username, which we check for availability as you type), settings forms, and checkout. Today each product has its own and they look different. Our tokens: color.text.default, color.text.subtle, color.text.danger, color.border.default, color.border.hover, color.border.focus, color.border.danger, color.background.input, color.background.disabled, space.100, space.200, radius.small, font.body.md, font.label.md, font.helper.sm. Web, React.

Output

Component spec: Text field

Status: draft · Platform: web (React) · Token source: Northwind tokens, plus two [proposed token]s Assumptions to confirm first: availability checks call an API that answers in under 2 seconds, and fields are single-line (multi-line is a separate textarea component).

Purpose and boundary

  • Use it for: short free-text input of one line: names, usernames, email, card number.
  • Use something else when: the answer is one of a known set (select or radio group), it's a date (date picker), or it's more than one line (textarea).

Anatomy

#PartRequiredNotes
1LabelyesAlways visible, above the input. Never replaced by placeholder.
2Input containeryesBorder and background
3Input textyes
4Helper textoptionalFormat or reason we ask. Replaced by the error message in the error state.
5Error messagein error stateIcon + text
6Status indicatoroptionalSpinner (loading) or check (valid), at the input's end

Variants

AxisValuesWhen to use each
Sizemd (default), smsm only in dense tables and filters; md everywhere else
Widthfull, fixed to expected lengthFixed width for known-length data (postcode, CVC) so the size hints at the answer

No "outlined/filled" style variant: one style across products is the point of the spec.

States

StateVisual change (by token, by part #)CursorAnnounced by assistive tech
Default2 border color.border.default, background color.background.input; 1 color.text.default; 4 color.text.subtletextLabel, then helper text (via aria-describedby)
Hover2 border color.border.hovertextnothing
Focus2 border color.border.focus plus a focus ring [proposed token: border.width.focus] outside the bordertextSame as default
FilledNo change from defaulttextLabel and value
Disabled2 background color.background.disabled; 3 color.text.subtle; helper text says why it's disablednot-allowedLabel, value, "dimmed" or "unavailable" (from disabled)
Read-only2 border removed, value shown as textdefaultLabel, value, "read only"
Error2 border color.border.danger; 5 shown with icon in color.text.danger; 4 hiddentextLabel, value, "invalid", then the error text
Loading6 spinner at the end of 2; input stays editabletext"Checking availability" in a status region, once per check
Valid (async only)6 check icon, colour color.text.default (not green alone)text"Username available" in the status region

Focus differs from hover by the added ring, not by colour alone.

Tokens

Part #PropertyTokenNotes
1typefont.label.md
1colourcolor.text.default
2backgroundcolor.background.input
2border colourcolor.border.default / .hover / .focus / .dangerby state
2radiusradius.small
2paddingspace.100 vertical, space.200 horizontal
2min height[proposed token: size.target.min]Must meet the 24×24 CSS px target minimum. Recommend larger for touch.
3typefont.body.md
4, 5typefont.helper.sm
1→2, 2→4gapspace.100

Behaviour

  • Async check: starts 400 ms after the last keystroke [assumption]; a newer keystroke cancels the pending check. The result shows only for the current value.
  • Validation timing: format errors show on blur, not on every keystroke. Once a field is in error, it re-validates on input so the error clears as soon as it's fixed.
  • Overflow: text scrolls horizontally inside the input; the label wraps, never truncates.
  • Responsive: full width below the small breakpoint, whatever the width variant.
  • Submit: Enter submits the parent form. On submit with errors, focus moves to the first field in error.

Content rules

  • Label: a noun, 1–4 words, sentence case, no colon, no "Enter your…".
  • Helper text: one sentence, under 80 characters, says the format or why we ask.
  • Error: what's wrong and how to fix it, naming the field. Exact strings come from ux-writing.
  • Placeholder: off by default. If used, only an example value ("sam@northwind.example"), never the label or instructions.

Accessibility

  • Native element / role: <input> with a <label for>. No ARIA role needed.
  • Accessible name: from the visible label, so it satisfies 2.5.3 Label in Name.
  • States → ARIA: aria-describedby → helper text (default) or error message (error); aria-invalid="true" in error; disabled attribute for disabled; readonly for read-only. Required fields use required and a visible "(required)" or "(optional)" convention, picked once per form.
  • Keyboard: standard text input; Tab and Shift+Tab move in and out, so there's no trap.
  • Focus: stays in the field during async checks; after a failed submit, moves to the first invalid field.
  • Contrast and size: label, value, helper and error text at least 4.5:1; the input border and the focus ring at least 3:1 against the background; target at least 24×24 CSS px.
  • Announcements: async results ("Checking availability", "Username available", "Username taken") go to a role="status" region so they're announced without moving focus.
  • WCAG 2.2 criteria: 1.3.1 Info and Relationships, 1.4.1 Use of Color, 1.4.3 Contrast (Minimum), 1.4.11 Non-text Contrast, 2.4.7 Focus Visible, 2.5.3 Label in Name, 2.5.8 Target Size (Minimum), 3.3.1 Error Identification, 3.3.2 Labels or Instructions, 3.3.3 Error Suggestion, 4.1.2 Name, Role, Value, 4.1.3 Status Messages.

Do / don't

✅ Do❌ Don'tWhy
Label above the field, always visiblePlaceholder as the only labelIt disappears on typing, and has weak contrast
Show format errors on blurShow "Invalid email" after the first keystrokeUsers get told off before they've finished
Fixed width for a 3-digit CVCFull-width CVC fieldWidth hints at the expected answer
Error icon + text + red borderRed border aloneColour alone fails users who can't see red
Explain a disabled field in its helper textGrey out with no reasonThe user can't fix what they can't see

Open questions for engineering

QuestionBlocks
Controlled, uncontrolled, or both?The React API and how forms libraries integrate
Who owns the debounce and cancel: the component or the caller?Whether the async state is a prop or internal
Is the 400 ms debounce acceptable for the username API's rate limits?The behaviour timing
Can we add border.width.focus and size.target.min to the token set?Focus and size tokens; otherwise raw values leak in
Does the error message render in the component or via a form-level slot?Where aria-describedby points

Next: once built, run accessibility-audit on the sign-up form that uses it.

How it's checked

Measured against plain Claude Code (2026-10-23): it met 79% of its checks, against 64% without it, and a blind judge preferred the agent without it. How skills are checked

21 assertions across 3 acceptance evals, and 8 trigger queries. Every skill's checks

What the output must do
  • States purpose and boundary, including when to use something other than a modal
  • Anatomy parts are numbered and the same numbers are used in the state and token tables
  • Every applicable state is specified, including focus, and focus is distinguishable from hover without relying on colour alone
  • Styling uses only token names, never hex codes or pixel values; any needed token not in the prompt is tagged [proposed token]
  • Keyboard behaviour covers focus moving into the dialog on open, Tab staying inside, Esc closing, and focus returning to the trigger
  • Specifies a native <dialog> or role=dialog with aria-modal and an accessible name from the heading
  • Every WCAG citation is a real WCAG 2.2 criterion with the correct number and name
  • Ends with open questions for engineering, each naming the decision it blocks
  • Flags that the error text colour #E57373 on the white surface is about 3:1 and fails the 4.5:1 text contrast requirement, despite the designer's note saying it passes, and proposes the darker error token or another fix
  • Flags that 20 × 20 px buttons fall below the WCAG 2.2 AA minimum target size of 24 × 24 CSS px (2.5.8), and does not present 44 px as the AA requirement
  • Does not use 'color.border.focus-strong' as if it exists; notes it is missing from tokens.json and points to an existing token or leaves it as an open question
  • Does not specify press-and-hold auto-repeat as settled behaviour; it follows the clinical safety objection (one tap, one step) or raises the disagreement as a decision to be made
  • States what happens at 0.5 mL and 10 mL and when someone types an out-of-range or off-step value such as 12, 0 or 2.3, answering QA's question
  • Does not invent usage data (such as how often nurses hit 10 mL), token values or research not in the files
  • Stays within about 1,200 words
  • Covers what happens with an invalid or empty email, while the form is sending, after a successful signup, and if sending fails
  • Advises against using the placeholder text as the only label, and explains why in plain terms (it disappears when typing, and screen readers and some users miss it)
  • Covers keyboard use and screen-reader needs, such as a programmatic label and error messages that are announced
  • Written plainly for a marketing person to hand to a developer, without unexplained design-system jargon
  • Does not invent brand colours, fonts, token names, pixel values or a privacy policy link not given; anything needed is left as something to fill in
  • Fits on one screen (roughly 300 words or fewer) as a checklist
When it should and shouldn't activate

Should activate:

  • Write a component spec for our button: variants, states, tokens and accessibility
  • Document the tabs component for our design system so engineering can build it
  • Spec out a toast notification with all its states and keyboard behaviour
  • Our date picker behaves differently in three products — write one spec they can all build to

Should leave to another skill:

  • Audit our sign-up page for WCAG 2.2 AA problems
  • Set up our brand kit with our logo, colours and fonts
  • Write the error messages and helper text for the checkout form
  • Design the navigation structure for our settings area

Hands off to

Related skills

brand-kit, information-architecture

Source

SKILL.md · the whole folder on GitHub.