Skip to content
Back to skills

Component Design Systems

ASecurity

A design system has three token layers, and conflating them is what makes systems

  • 2 stars
  • 0 votes
  • 0 copies
  • 2 views
  • Added September 10, 2026
developmenttypescriptgoreactnodeexpressapi

Works with

  • cli
  • api

Security analysis

A100/100

Scanned September 10, 2026

npx -y skills add snoodleboot-io/prompticorn --skill component-design-systems --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Component Design Systems?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Component Design Systems
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/snoodleboot-io-component-design-systems/badge)](https://www.skillsdirectory.com/skills/snoodleboot-io-component-design-systems)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
name: component-design-systems
description: "A design system has three token layers, and conflating them is what makes systems"
---

# Component Design Systems (Verbose)

## Core Patterns

### Token Layers

A design system has three token layers, and conflating them is what makes systems
impossible to retheme.

```css
/* 1. Primitive — raw values, no meaning. Never referenced by components. */
:root {
  --red-600: #b42318;
  --gray-100: #f2f4f7;
  --size-3: 0.75rem;
}

/* 2. Semantic — meaning, no context. This is what components consume. */
:root {
  --color-bg-danger: var(--red-600);
  --color-bg-subtle: var(--gray-100);
  --space-inline-sm: var(--size-3);
}

/* 3. Component — only when a component genuinely deviates. */
:root {
  --button-height-sm: 2rem;
}
```

Dark mode then costs one block, because only layer 2 rebinds:

```css
[data-theme="dark"] {
  --color-bg-subtle: var(--gray-800);
}
```

The non-obvious part: **tokens are the durable contract, component APIs are not.**
Over five years you will migrate class components to hooks, CSS-in-JS to CSS
modules, and possibly frameworks entirely. `--color-bg-danger` survives every one
of those, and it is simultaneously the interface to design tools, iOS, Android,
and email templates. So govern token names strictly — renaming one is a breaking
change with a deprecation window — and let component internals churn freely.

### Compound Components

Prop-driven components fail at a predictable point: when a consumer needs a layout
you did not anticipate.

```tsx
const SelectCtx = createContext<Ctx | null>(null);

function useSelectCtx() {
  const ctx = useContext(SelectCtx);
  if (!ctx) throw new Error("Select.* must be rendered inside <Select>");
  return ctx;
}

export function Select({ value, onChange, children }: SelectProps) {
  const [open, setOpen] = useState(false);
  const ctx = useMemo(() => ({ value, onChange, open, setOpen }), [value, onChange, open]);
  return <SelectCtx.Provider value={ctx}>{children}</SelectCtx.Provider>;
}

Select.Trigger = function Trigger({ children }: { children: ReactNode }) {
  const { open, setOpen } = useSelectCtx();
  return <button aria-expanded={open} onClick={() => setOpen(!open)}>{children}</button>;
};

Select.Option = function Option({ value, children }: OptionProps) {
  const { onChange, setOpen } = useSelectCtx();
  return (
    <li role="option" onClick={() => { onChange(value); setOpen(false); }}>
      {children}
    </li>
  );
};
```

Throwing from `useSelectCtx` turns a silent misuse into an immediate, obvious error.

| | Prop-driven | Compound |
|---|---|---|
| Novel layout | Needs a new `renderX` prop | Consumer just rearranges |
| API surface | Grows with every request | Stable |
| Discoverability | High — autocomplete lists props | Lower; needs docs |
| Best for | Closed sets (Button, Badge) | Slotted UI (Select, Dialog, Card) |

Use props for leaf components with a small fixed shape. Reach for composition the
moment a component has *slots*.

### Polymorphic and Forwarding Components

```tsx
type ButtonProps<C extends ElementType> = {
  as?: C;
  variant?: "primary" | "ghost" | "danger";
} & Omit<ComponentPropsWithoutRef<C>, "as">;

export const Button = forwardRef(function Button<C extends ElementType = "button">(
  { as, variant = "primary", className, ...rest }: ButtonProps<C>,
  ref: ComponentPropsWithRef<C>["ref"]
) {
  const Tag = as ?? "button";
  return <Tag ref={ref} className={cx(styles.base, styles[variant], className)} {...rest} />;
});
```

The `as` prop lets a link look like a button without duplicating styles — and
without shipping a `<button onClick={navigate}>` that breaks middle-click,
ctrl-click, and screen-reader link navigation.

### Variants as Data

```ts
export const badge = cva("inline-flex items-center rounded-full font-medium", {
  variants: {
    tone: {
      neutral: "bg-[--color-bg-subtle] text-[--color-fg-default]",
      danger:  "bg-[--color-bg-danger-subtle] text-[--color-fg-danger]",
    },
    size: { sm: "h-5 px-2 text-xs", md: "h-6 px-2.5 text-sm" },
  },
  compoundVariants: [{ tone: "danger", size: "sm", class: "font-semibold" }],
  defaultVariants: { tone: "neutral", size: "md" },
});
```

Because the variant map is a plain object, you can enumerate it in a story and
snapshot every combination. That catches the "danger + small looks broken" case
that no unit test would think to assert.

### Accessibility as a Library Guarantee

The system's value proposition is that accessibility is solved *once*. Concretely:

- Focus is visible via `:focus-visible`; never removed with a bare `outline: none`.
- Interactive targets meet WCAG 2.2 target size (24×24 CSS px minimum).
- Icon-only controls require an `aria-label` — make it a required prop in TypeScript.
- Text/background token pairs are contrast-checked in CI, so a token change cannot
  silently drop below 4.5:1.

For anything with a focus-management contract — dialogs, menus, comboboxes,
tooltips — wrap a headless primitive rather than hand-rolling. Roving tabindex,
typeahead, scroll locking, and returning focus to the trigger are each easy to get
90% right and very visible when wrong.

## Common Anti-Patterns

❌ **Components reading primitive tokens directly** — `background: var(--red-600)`
means dark mode requires touching every component.
✅ Components read semantic tokens; only semantic tokens read primitives.

❌ **Boolean props multiplying** — `isCompact`, `isInline`, `hasIcon`,
`isDestructive` create 16 combinations, most untested and several nonsensical.
✅ One enum per axis, with invalid combinations unrepresentable.

❌ **Not forwarding `ref` and `className`** — the component cannot be a popover
trigger, cannot be measured, cannot be nudged 4px by a consumer.
✅ `forwardRef` plus a `{...rest}` spread and merged `className` everywhere.

❌ **Hard-coding external spacing** — `margin-bottom: 16px` on a Card makes it
wrong in every layout that isn't the one you designed against.
✅ Components own internal padding; the parent owns spacing between children (`gap`).

❌ **A "Button" that renders `<div onClick>`** — no keyboard activation, no role,
no focusability.
✅ Real semantic elements, with `as` for the link/button swap.

❌ **Docs written after adoption** — the undocumented loading state gets
reimplemented in four product teams, differently.
✅ A story per state, shipped in the same PR as the component.

❌ **Accepting a one-off variant to unblock a team** — `variant="specialDashboard"`
is permanent and nobody will ever dare delete it.
✅ Give them an escape hatch (`className`, composition); promote to a variant only
after the pattern appears in three places.

## Component Design System Checklist

- [ ] Primitive / semantic / component token layers separated
- [ ] Components reference only semantic tokens
- [ ] Dark theme achieved by rebinding semantic tokens alone
- [ ] Token renames go through a deprecation window
- [ ] Every component forwards `ref` and merges `className`
- [ ] Rest props spread onto the underlying element
- [ ] Variants expressed as a data map, not JSX ternaries
- [ ] Every variant × size combination rendered in a story
- [ ] Composition used wherever consumers need layout control
- [ ] Context hooks throw when used outside their provider
- [ ] Interactive components built on a headless primitive or audited for keyboard nav
- [ ] `:focus-visible` styling present; no bare `outline: none`
- [ ] Icon-only controls require an accessible name at the type level
- [ ] Token contrast pairs verified in CI
- [ ] Components carry no external margin
- [ ] Each component documents its states plus one anti-example

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…