PLW Primitives Foundation 02
Palette
Type
Shape
Accent

Primitive · action

Button

Four variants, three sizes. Primary carries the brand accent and elevation; secondary and ghost step down; destructive reads from the danger token. Radius, shadow, and weight all bend with the active shape.

AVariants × sizes
Primary
Secondary
Ghost
Destructive
With leading dot / icon slot
As a link

Accessibility

  • Renders a native <button>; use <a> only when it navigates.
  • Focus ring uses --color-ring at --border-heavy, never the accent alone, so it stays visible on low-contrast accents.
  • Disabled sets aria-disabled + reduced opacity; hit target stays ≥ 44px at md/lg.
  • Label text contrast is guaranteed by the tuned --color-accent-fg per palette.

Token contract

--color-accent--color-accent-fg--color-surface--color-fg--color-border--color-danger--color-ring--font-body--text-xs/sm/base--space-2…6--radius-md--shadow-md--border-default
// Button.tsx — variants/sizes read tokens; no hardcoded color or size.
import { cva, type VariantProps } from "class-variance-authority";

const button = cva(
  "inline-flex items-center justify-center gap-[var(--space-2)] font-[var(--font-body)] font-semibold
   rounded-[var(--radius-md)] border-[length:var(--border-default)] border-transparent
   transition focus-visible:outline focus-visible:outline-[length:var(--border-heavy)]
   focus-visible:outline-offset-2 focus-visible:outline-[color:var(--color-ring)]
   disabled:opacity-50 disabled:pointer-events-none",
  {
    variants: {
      variant: {
        primary:     "bg-[var(--color-accent)] text-[var(--color-accent-fg)] shadow-[var(--shadow-md)]",
        secondary:   "bg-[var(--color-surface)] text-[var(--color-fg)] border-[color:var(--color-border)]",
        ghost:       "bg-transparent text-[var(--color-fg)]",
        destructive: "bg-[var(--color-danger)] text-white shadow-[var(--shadow-md)]",
      },
      size: {
        sm: "text-[var(--text-xs)] px-[var(--space-3)] py-[var(--space-2)]",
        md: "text-[var(--text-sm)] px-[var(--space-5)] py-[var(--space-3)]",
        lg: "text-[var(--text-base)] px-[var(--space-6)] py-[var(--space-4)]",
      },
    },
    defaultVariants: { variant: "primary", size: "md" },
  }
);

type Props = React.ButtonHTMLAttributes<HTMLButtonElement> & VariantProps<typeof button>;

export function Button({ variant, size, className, ...props }: Props) {
  return <button className={button({ variant, size, className })} {...props} />;
}

Primitive · status & metadata

Badge & Tag

Badges signal status (solid, soft, and per-status tints from the success/warning/danger tokens). Tags are quieter metadata chips in the mono font, optionally removable.

ABadge tones
Featured New Draft Active Pending Overdue Archived

est. 1994 licensed commercial litigation

Accessibility

  • Status is never color-only: pair with the label text and an optional dot.
  • Soft tints are generated with color-mix against --color-bg, so they hold AA in every palette.
  • Removable tag's × is a real button with an aria-label.

Token contract

--color-accent--color-success--color-warning--color-danger--color-bg--color-fg--color-muted--radius-pill--radius-sm--font-mono
// Badge.tsx
const tones = {
  accent:  "bg-[var(--color-accent)] text-[var(--color-accent-fg)]",
  soft:    "bg-[color-mix(in_srgb,var(--color-accent)_16%,var(--color-bg))] text-[color-mix(in_srgb,var(--color-accent)_70%,var(--color-fg))]",
  success: "bg-[color-mix(in_srgb,var(--color-success)_16%,var(--color-bg))] text-[color-mix(in_srgb,var(--color-success)_72%,var(--color-fg))]",
  warning: "bg-[color-mix(in_srgb,var(--color-warning)_18%,var(--color-bg))] text-[color-mix(in_srgb,var(--color-warning)_72%,var(--color-fg))]",
  danger:  "bg-[color-mix(in_srgb,var(--color-danger)_16%,var(--color-bg))] text-[color-mix(in_srgb,var(--color-danger)_72%,var(--color-fg))]",
};
export const Badge = ({ tone = "accent", ...p }) =>
  <span className={`inline-flex items-center gap-[var(--space-1)] uppercase tracking-wide text-[var(--text-xs)] font-bold
    px-[var(--space-3)] py-[var(--space-1)] rounded-[var(--radius-pill)] ${tones[tone]}`} {...p} />;

Primitive · structure

Divider

Horizontal hairline, a heavy rule, a labelled divider for sectioning, and a vertical rule for inline groups. Width follows --border-thin / --border-default.

AAll forms

Thin


Heavy


Or continue with
Mon–Fri 9–5 By appointment

Accessibility

  • Decorative rules use <hr>; a labelled divider keeps its text in the flow for context.
  • Vertical divider is a presentational <span> with a border, carrying no semantics.

Token contract

--color-border--color-muted--border-thin--border-default--space-4/5--font-mono
// Divider.tsx
export function Divider({ label, weight = "thin" }: { label?: string; weight?: "thin" | "heavy" }) {
  if (label) return (
    <div role="separator" className="flex items-center gap-[var(--space-4)] text-[var(--text-xs)] uppercase tracking-wide
      text-[var(--color-muted)] font-[var(--font-mono)] before:flex-1 before:border-t before:border-[var(--color-border)]
      after:flex-1 after:border-t after:border-[var(--color-border)]">{label}</div>
  );
  return <hr className={`border-0 border-t-[length:var(--border-${weight === "heavy" ? "default" : "thin"})]
    border-[color:var(--color-border)] my-[var(--space-5)]`} />;
}

Primitive · form

Input

Text field, textarea, and select, plus help text and an error state driven by the danger token. Labels, focus rings, and required markers are built in.

AFields & states
As it appears on file.
Enter a valid email address.
Drop a file or click to browse. PDF, max 10MB.

Accessibility

  • Every field has a real <label for>; required uses a visible * plus the field's required attr.
  • Errors set aria-invalid and pair the danger border with text, not color alone.
  • Focus ring uses --color-ring; placeholder contrast comes from --color-muted.

Token contract

--color-surface--color-fg--color-muted--color-border--color-ring--color-danger--radius-md--border-default--font-body--text-sm--space-2…4
// Input.tsx
export function Input({ label, required, error, id, ...props }: InputProps) {
  return (
    <div className="flex flex-col gap-[var(--space-2)]">
      <label htmlFor={id} className="text-[var(--text-sm)] font-semibold text-[var(--color-fg)]">
        {label} {required && <span className="text-[var(--color-danger)]">*</span>}
      </label>
      <input id={id} required={required} aria-invalid={!!error}
        className={`w-full font-[var(--font-body)] text-[var(--text-sm)] text-[var(--color-fg)]
          bg-[var(--color-surface)] rounded-[var(--radius-md)] px-[var(--space-4)] py-[var(--space-3)]
          border-[length:var(--border-default)] border-[color:${error ? "var(--color-danger)" : "var(--color-border)"}]
          focus-visible:outline focus-visible:outline-[color:var(--color-ring)]`} {...props} />
      {error && <span className="text-[var(--text-xs)] font-semibold text-[var(--color-danger)]">{error}</span>}
    </div>
  );
}

Primitive · form

Checkbox & Radio

Custom-styled controls that wrap native inputs, so keyboard and screen-reader behavior is free. Checked state fills with the accent; the focus ring tracks the hidden input.

AStates
Checkbox
Radio

Accessibility

  • The real <input> is visually hidden but focusable; the styled box is decorative.
  • Focus ring renders on the box via input:focus-visible + .box, tracking keyboard focus only.
  • Whole label is clickable; radios share a name for arrow-key grouping.

Token contract

--color-accent--color-accent-fg--color-surface--color-border--color-ring--radius-sm--radius-pill--border-default--space-3
// Checkbox.tsx — native input + styled box, label wraps both.
export function Checkbox({ label, hint, ...props }: CheckboxProps) {
  return (
    <label className="inline-flex items-start gap-[var(--space-3)] cursor-pointer text-[var(--text-sm)]">
      <input type="checkbox" className="peer sr-only" {...props} />
      <span className="grid place-items-center w-5 h-5 rounded-[var(--radius-sm)] bg-[var(--color-surface)]
        border-[length:var(--border-default)] border-[color:var(--color-border)]
        peer-checked:bg-[var(--color-accent)] peer-checked:border-[var(--color-accent)]
        peer-focus-visible:outline peer-focus-visible:outline-[color:var(--color-ring)]">
        <CheckIcon className="opacity-0 peer-checked:opacity-100 text-[var(--color-accent-fg)]" />
      </span>
      <span>{label}{hint && <small className="block text-[var(--color-muted)]">{hint}</small>}</span>
    </label>
  );
}

Primitive · utility

Icon wrapper & Focus ring

A consistent container for icons in three tones, and the shared focus-ring utility every interactive primitive borrows so keyboard focus is unmistakable in any palette.

AIcon wrapper · soft / solid / ghost
Focus ring · tab to the control

Accessibility

  • Decorative icons are aria-hidden; an icon-only control needs an aria-label.
  • The focus utility is outline-based (not box-shadow), so it survives high-contrast and forced-colors modes.
  • One ring definition, every primitive: --border-heavy solid --color-ring, offset 2–3px.

Token contract

--color-accent--color-accent-fg--color-bg--color-fg--color-border--color-ring--radius-md--border-default/heavy
// focus-ring.ts — one utility, reused by every interactive primitive.
export const focusRing =
  "focus-visible:outline focus-visible:outline-[length:var(--border-heavy)]
   focus-visible:outline-offset-2 focus-visible:outline-[color:var(--color-ring)]";

// IconWrap.tsx
const tones = {
  soft:  "bg-[color-mix(in_srgb,var(--color-accent)_16%,var(--color-bg))] text-[var(--color-accent)]",
  solid: "bg-[var(--color-accent)] text-[var(--color-accent-fg)]",
  ghost: "border-[length:var(--border-default)] border-[color:var(--color-border)] text-[var(--color-fg)]",
};
export const IconWrap = ({ tone = "soft", children }) =>
  <span className={`grid place-items-center w-10 h-10 rounded-[var(--radius-md)] ${tones[tone]}`}>{children}</span>;