Design Systems skill for Codex
A shared UI library teams actually adopt. Codex starts by counting the duplicated buttons, dialogs and selects already in the repo, then builds components on headless primitives with semantic tokens, consistent variant and size props, Storybook coverage and accessibility checks. It versions with Changesets, retires old APIs through lint rules and codemods, and tracks adoption with simple import counts.
curl -fsSL codexskills.dev/install.sh \ | sh -s -- design-systems Installs to ~/.agents/skills/design-systems/ for every project on this machine.
curl -fsSL codexskills.dev/cli.mjs -o codex-skills.mjsnode codex-skills.mjs install design-systems The CLI also searches, updates and removes skills. Run it with no arguments to pick from a list. Needs Node 20.12+.
curl -fsSL codexskills.dev/install.sh \ | sh -s -- --project design-systems Installs to .agents/skills/design-systems/ in the current repository. Commit it and everyone who runs Codex there gets it.
curl -fsSLO codexskills.dev/downloads/design-systems.zipunzip -o design-systems.zip -d ~/.agents/skills/ The archive holds the skill folder with its generated agents/openai.yaml. Unzip into .agents/skills/ to scope it to a repo.
& ([scriptblock]::Create((irm codexskills.dev/install.ps1))) design-systems PowerShell 5.1 or 7. Installs to %USERPROFILE%\.agents\skills\. Add -Project for the current repo.
104-line POSIX script. No sudo, no telemetry. Read install.sh
Use it in Codex
Start a prompt with $design-systems, or pick it from /skills.
You can also just describe the task: Codex reads the description below and loads the playbook when your request matches.
What Codex reads before loading it
Builds and governs a component design system - inventory of existing UI, layered architecture from tokens to primitives to components, component API conventions (variants, slots, asChild), headless foundations such as Radix or React Aria, Storybook docs, Changesets versioning, deprecation with codemods and lint rules, and adoption metrics. Use when starting a shared UI library, consolidating duplicate buttons and modals, writing component guidelines, planning a breaking change to a shared component, or setting up contribution rules.
Use it when
- Starting a shared component package for several apps or teams
- Consolidating duplicated UI components across a codebase
- Defining component API standards and a contribution or RFC process
- Rolling out a breaking change to a shared component
Use something else when
- Only naming and building tokens or theme variables: use design-tokens
- Setting up shadcn/ui itself in an app: use shadcn-ui
How to check the result
Each v1 component has stories for every variant and state, passes axe and keyboard checks, and at least one real product usage has been migrated to it.
The mistake it prevents
Building a large catalog before anyone adopts it, instead of shipping the five most-duplicated components and migrating real screens.
A design system is working when product teams reach for it first because it is faster than building their own, every component has one documented API and accessible behavior, and breaking changes arrive with a migration path. A Figma library with no code, or a package nobody imports, is not a system.
Before building
- Read
AGENTS.md, the workspace layout (pnpm-workspace.yaml,turbo.json,nx.json) and how styling is done. The system must fit the existing stack, not replace it. - Inventory before designing. Count duplicates; they set the priority list:
rg -l --glob '*.{tsx,jsx,vue,svelte}' -i 'function \w*(Button|Modal|Dialog|Select|Tooltip)\b' src | sort
rg -c '<button' --glob '*.{tsx,jsx}' src | sort -t: -k2 -nr | head
rg -o "from ['\"][^'\"]*(ui|components)/[A-Za-z]+['\"]" -N src | sort | uniq -c | sort -nr | head -30
- Screenshot every variant of the top five duplicated components side by side. The inventory board is the strongest argument for the system and the spec for v1.
Architecture
| Layer | Contains | Changes how often |
|---|---|---|
| Tokens | Color, space, type, radius, motion, elevation (see design-tokens) | Rarely; themed per brand/mode |
| Primitives | Box/Stack, Text, Icon, VisuallyHidden, Slot | Almost never |
| Components | Button, Input, Select, Dialog, Tabs, Toast, Table | Minor releases |
| Patterns | Form layouts, empty states, page headers, filters | Often; can live in apps |
Rules: components consume only semantic tokens, never raw values; patterns compose components and never restyle their internals; apps may build patterns but not fork components.
Foundations: build vs adopt
| Need | Default |
|---|---|
| React, full styling control, accessibility handled | Headless: Radix Primitives or React Aria Components, styled with your tokens |
| React + Tailwind, want owned source | shadcn/ui as a starting point, copied into the package and then governed like your own code |
| Multi-framework or no framework | Web components (Lit) wrapping a headless core, or CSS-only components where behavior is trivial |
| Speed over identity, internal tools | Adopt a styled library and theme it via tokens; do not build |
Never hand-roll a combobox, menu, dialog focus trap or date picker when a tested headless implementation exists; that is where accessibility bugs live.
Component API conventions
Write these down once and apply them to every component:
variantfor visual style (primary | secondary | ghost | danger),sizefor scale (sm | md | lg). No booleans likeisPrimary,large.- Composition over configuration:
<Dialog><Dialog.Title/></Dialog>or slots, not fifteen props such astitleText,titleIcon,titleClassName. - Pass through native props and refs (
...props, forwarded ref or React 19 ref prop). asChild(Radix Slot) or a render prop for polymorphism, rather than anasprop that breaks types.- Controlled and uncontrolled:
value/onValueChangeplusdefaultValue. classNameaccepted and merged last; styling escape hatches documented, not hidden.
import { cva, type VariantProps } from "class-variance-authority";
import { Slot } from "@radix-ui/react-slot";
const button = cva("inline-flex items-center justify-center gap-2 font-medium rounded-[var(--radius-control)] focus-visible:outline-2 focus-visible:outline-offset-2 disabled:opacity-50 disabled:pointer-events-none", {
variants: {
variant: {
primary: "bg-[var(--color-action)] text-[color:var(--color-on-action)] hover:bg-[var(--color-action-hover)]",
secondary: "border border-[var(--color-border)] bg-transparent hover:bg-[var(--color-surface-hover)]",
danger: "bg-[var(--color-danger)] text-[color:var(--color-on-danger)]",
},
size: { sm: "h-8 px-3 text-sm", md: "h-10 px-4", lg: "h-12 px-6 text-lg" },
},
defaultVariants: { variant: "primary", size: "md" },
});
type ButtonProps = React.ComponentProps<"button"> &
VariantProps<typeof button> & { asChild?: boolean };
export function Button({ asChild, variant, size, className, ...props }: ButtonProps) {
const Comp = asChild ? Slot : "button";
return <Comp className={button({ variant, size, className })} {...props} />;
}
Workflow for a new or changed component
- [ ] 1. Inventory existing usages and variants in the codebase
- [ ] 2. Write the component spec (template below) and get it reviewed
- [ ] 3. Build on a headless primitive with semantic tokens only
- [ ] 4. Stories for every variant, size and state; interaction tests for behavior
- [ ] 5. Accessibility check: keyboard path, axe, screen reader spot-check
- [ ] 6. Visual regression baseline
- [ ] 7. Changeset with the right semver bump
- [ ] 8. Migrate at least one real product usage before calling it done
Component spec template
# <Component>
**Purpose:** <one sentence; when to use, when NOT to use and what to use instead>
**Anatomy:** <parts and slots>
**API:** <props table: name, type, default, notes>
**Variants / sizes:** <list with intended use>
**States:** default, hover, focus-visible, active, disabled, loading, invalid
**Keyboard:** <keys and behavior, following the ARIA APG pattern>
**Content rules:** <label length, truncation, icon usage>
**Replaces:** <file paths of local duplicates to migrate>
Versioning and release
Use Changesets in a monorepo package:
npx changeset # describe the change, pick patch/minor/major per package
npx changeset version # bump versions, write CHANGELOG.md
npx changeset publish # publish to the registry (ask the user before publishing)
| Change | Bump |
|---|---|
| New component, new optional prop, new variant | minor |
| Bug fix, visual fix within spec | patch |
| Removed or renamed prop, changed default, changed DOM structure apps may target | major |
| Token value change that shifts layout or contrast noticeably | minor with release note; major if contrast or sizing contracts break |
Publishing to a registry needs network and credentials; never run changeset publish or
npm publish without explicit user confirmation.
Deprecation path
- Mark the old API with
/** @deprecated Use <Button variant="ghost"> instead. */so IDEs strike it through. - Add a lint rule so new usages fail CI:
// eslint.config.js
rules: {
"no-restricted-imports": ["error", { paths: [{
name: "@/components/OldButton",
message: "Use Button from @acme/ui (see docs/migrations/button.md).",
}]}],
}
- Ship a codemod for mechanical renames and run it in a dry run first:
npx jscodeshift -t codemods/button-v2.ts src --parser=tsx --dry --print. - Remove in the next major only after usage reaches zero.
Governance
- Contribution model: anyone can propose (RFC issue with the spec template), the system team reviews API and accessibility, contributors can build with pairing.
- A component enters the system when two or more products need it; one-offs stay local.
- Weekly or per-release adoption metric:
rg -l "from ['\"]@acme/ui['\"]" apps | wc -l # files using the system
rg -l "<button|styled\.button|className=.*btn" apps | wc -l # files still hand-rolling
- Track in a table: component, adoption count, open bugs, local duplicates remaining.
Gotchas
- Shipping components that import app code or global CSS; the package must be leaf-level.
classNameoverrides that depend on specificity order; use cascade layers or a merge helper (tailwind-merge) so app overrides win predictably.- Changing DOM structure in a minor: apps target internals in tests and CSS. Treat as major.
- Documentation that drifts: generate props tables from types (Storybook autodocs) instead of hand-writing them.
- Building every component before anyone uses one. Ship the top five by duplication count.
Output contract
When asked to set up or audit a system, return:
**Inventory:** <component>: <n local implementations> (files)
**Proposed layers and package:** <path, build tool, styling approach>
**v1 components (by duplication):** 1. ... 5.
**Conventions doc:** <path>
**Release/versioning:** <Changesets config path>
**Governance:** <RFC template path, review owners>
**Next migrations:** <files to migrate first> What the zip contains
SKILL.md plus the interface file below. Nothing runs on install.
agents/openai.yaml
Gives the Codex app a display name, a one-line description and a starter prompt for this skill.
interface:
display_name: "Design Systems"
short_description: "A shared UI library teams actually adopt"
default_prompt: "$design-systems We have six different modal components, consolidate them into one in packages/ui" About this skill
Written and maintained by the Codex Skills editors to the same editorial standard as every skill here: a selective description, a real workflow, known pitfalls and a way to verify the work. First published , last updated . MIT licensed. Something wrong or out of date? Tell us.