A design system you can touch.
Every component here shares one set of tokens. Change the theme, mode, corner style, density or state, and all of them follow at once. Each one comes with a rule for when to use it and markup you can copy.
Three rules every component follows
- The accent color always means needs attention, never decoration.
- Every interactive state is reachable by keyboard alone.
- Labels say what happens, not how the system works underneath.
How it was built
I built Workbench by directing Claude, then reviewed each round the way a stranger would: a hiring manager with ten seconds, an accessibility auditor, a developer deciding whether to build on it. Those reviews caught real problems, like a selection card with radio semantics but no keyboard support, and five dark-mode colors below WCAG contrast.
The speed came from the tool. Deciding what to fix, in what order, and when a suggested fix was wrong came from six years of shipping product design. The audit above lets you check the result yourself.
Buttons
Use primary for the one action you want taken on a screen. Avoid more than one primary button in the same view.
Variants
Sizes
Loading
Link and group
<button class="btn btn-primary">Save changes</button> <button class="btn btn-secondary">Cancel</button> <button class="btn btn-outline">Learn more</button> <button class="btn btn-ghost">Skip</button> <button class="btn btn-destructive">Delete project</button> <button class="btn btn-success">Approve request</button> <button class="btn btn-primary btn-xs">Extra small</button> <button class="btn btn-primary btn-sm">Small</button> <button class="btn btn-primary btn-lg">Large</button> <button class="btn btn-primary btn-xl">Extra large</button> <button class="btn btn-primary btn-loading" disabled aria-busy="true"> <span class="spinner"></span>Saving… </button> <button class="btn-link">View full changelog</button> <div class="btn-group" role="group" aria-label="Range"> <button class="btn btn-secondary btn-sm" aria-pressed="false">Day</button> <button class="btn btn-secondary btn-sm" aria-pressed="true">Week</button> </div>
Input
Use for a single line of free text. Avoid for choices from a fixed list. Use a select instead.
Basic
Search and password
Prefix, suffix and stepper
Textarea
<label for="email">Work email</label> <input id="email" type="email" inputmode="email" autocomplete="email" placeholder="name@company.com" aria-describedby="email-hint"> <span class="hint" id="email-hint">Enter a work email, like name@company.com</span> <div class="input-wrap"> <span class="input-icon-l"><svg>...</svg></span> <input class="has-icon-l has-icon-r" type="search" placeholder="Search projects"> <button class="input-icon-r" aria-label="Clear search"><svg>...</svg></button> </div> <div class="input-affix"> <span class="affix pre">$</span> <input type="text" inputmode="decimal" value="49.00"> <span class="affix suf">USD</span> </div> <div class="stepper"> <button aria-label="Decrease seats">−</button> <input type="number" value="4" min="0"> <button aria-label="Increase seats">+</button> </div> <textarea class="ta" placeholder="Add context for the team"></textarea>
Select
Use when there are 4 to 12 known options. Avoid past about 12, where search beats scanning.
Multi-select with chips
<label for="plan">Plan</label> <select id="plan"> <option>Starter</option> <option selected>Pro</option> <option>Team</option> </select> <div class="chip-input"> <span class="tag-chip">Design<button aria-label="Remove Design"><svg>...</svg></button></span> <input type="text" placeholder="Add a tag, press Enter"> </div>
Toggle
Use for a setting that takes effect immediately. Avoid when the change needs a separate save step. Use a checkbox instead.
<label class="toggle-row">
<span class="switch">
<input type="checkbox" role="switch" checked>
<span class="track"></span><span class="knob"></span>
</span>
Email notifications
</label>Segmented control
Use for 2 to 4 views where every option should stay visible. Avoid past 4 options. That is a tab bar or a select.
<div class="segmented" role="group" aria-label="View"> <button class="active" aria-pressed="true">List</button> <button aria-pressed="false">Board</button> <button aria-pressed="false">Timeline</button> </div>
Selection card
Use when each option needs room to explain itself, like a plan picker. Avoid for more than 5 options, or options that are just a short label. Use radio rows instead.
Starter
$0 per month
Pro
$29 per month
Team
$79 per month
<div class="select-cards" role="radiogroup" aria-label="Plan">
<div class="select-card active" role="radio" aria-checked="true" tabindex="0">
<span class="sc-check"><svg>...</svg></span>
<p class="sc-title">Pro</p>
<p class="sc-price">$29 per month</p>
</div>
</div>
<!-- Arrow keys move selection, Enter and Space select -->Badges
Use to show status at a glance in a list or table row. Avoid more than one badge on the same item.
Sizes and dot style
<span class="badge badge-success">Active</span> <span class="badge badge-pending">Pending review</span> <span class="badge badge-error">Failed sync</span> <span class="badge badge-success badge-sm">Active</span> <span class="badge badge-success badge-lg">Active</span> <span class="badge badge-success badge-dot">Live</span>
Alert
Use for information that affects the whole page or a whole action. Avoid for a single-field error. Use the field hint instead.
Weekly digest paused
You won't get the Monday summary until you turn it back on.
Billing needs attention
Your card was declined. Update your payment method to keep your plan active.
<div class="alert warn" role="alert">
<svg aria-hidden="true">...</svg>
<div>
<h3>Billing needs attention</h3>
<p>Your card was declined. Update your payment method to keep your plan active.</p>
</div>
</div>Tabs
Use to switch between views of the same object. Avoid for sequential steps. Use a stepper instead.
<div class="tabs" role="tablist" aria-label="Project"> <button class="tab-btn active" role="tab" aria-selected="true">Overview</button> <button class="tab-btn" role="tab" aria-selected="false" tabindex="-1">Activity</button> </div> <!-- Left and right arrows move between tabs -->
Card
Use to group a preview with its metadata in a grid. Avoid nesting a card inside another card.
Grid card
Q3 brand refresh
Updated Aug 4, 3 collaborators
Horizontal card
Onboarding flow v2
In review, assigned to Ana
Media card
Redesigning checkout
6 min read
Pricing card
Pro
$29 per month
- Unlimited projects
- Priority support
<div class="sample-card"> <div class="thumb"></div> <h3>Q3 brand refresh</h3> <p>Updated Aug 4, 3 collaborators</p> </div> <div class="card-horizontal"> <div class="thumb"></div> <div class="ch-body"><h3>Onboarding flow v2</h3><p>In review, assigned to Ana</p></div> </div> <div class="card-media"> <div class="cm-cover"><span class="cm-badge">Case study</span></div> <div class="cm-body"><h3>Redesigning checkout</h3><p>6 min read</p></div> </div> <div class="card-pricing featured"> <p class="cp-tier">Pro</p> <p class="cp-price">$29<span> per month</span></p> <ul><li><svg>...</svg>Unlimited projects</li></ul> <button class="btn btn-primary">Choose Pro</button> </div>
Stat card
Use for one key number on a dashboard. Avoid more than 4 to 6 on a screen, or none of them read as key.
Monthly recurring revenue
$8,420 +12%
Churned seats
14 −3%
<div class="stat-card"> <p class="stat-label">Monthly recurring revenue</p> <p class="stat-value">$8,420 <span class="stat-trend up">+12%</span></p> </div>
List row
Use for scannable rows in a settings or team list. Avoid when rows need more than 2 or 3 data points. Use a table instead.
Selectable and reorderable
<div class="list-row">
<div class="list-avatar" aria-hidden="true">JD</div>
<div class="list-meta">
<p class="list-name">Jordan Diaz</p>
<p class="list-sub">jordan@company.com</p>
</div>
<span class="badge badge-success">Active</span>
</div>
<div class="list-row">
<span class="drag-handle" aria-hidden="true"><svg>...</svg></span>
<input type="checkbox" checked aria-label="Homepage hero copy">
<div class="list-meta"><p class="list-name">Homepage hero copy</p></div>
</div>Table
Use when rows need to be compared column by column. Avoid on narrow screens without horizontal scroll. Tables don't reflow.
| Name | Status | Owner | Updated | |
|---|---|---|---|---|
| Onboarding flow v2 | Active | Ana Mora | Aug 4 | |
| Pricing experiment | Pending | Jordan Diaz | Aug 2 | |
| Q2 retro notes | Failed sync | Ravi Kapoor | Jul 29 |
<div class="table-wrap">
<table class="ds-table">
<thead>
<tr>
<th scope="col"><input type="checkbox" aria-label="Select all rows"></th>
<th scope="col">Name</th><th scope="col">Status</th>
</tr>
</thead>
<tbody>
<tr>
<td><input type="checkbox" checked aria-label="Select Onboarding flow v2"></td>
<td class="t-name">Onboarding flow v2</td>
<td><span class="badge badge-success">Active</span></td>
</tr>
</tbody>
</table>
</div>Accordion
Use for FAQs or optional detail someone can choose to open. Avoid for content people need on every visit. It only adds a click.
Everything stays intact for 30 days in case you change your mind. After that it is permanently deleted.
Yes. Guests can join individual projects without a full seat.
50% off with a valid .edu email, applied automatically at checkout.
<div class="accordion-item">
<h3>
<button class="accordion-trigger" aria-expanded="true" aria-controls="panel-1" id="trigger-1">
Question text
<span class="chev" aria-hidden="true"><svg>...</svg></span>
</button>
</h3>
<div class="accordion-panel open" id="panel-1" role="region" aria-labelledby="trigger-1">
<div class="accordion-panel-inner"><p>Answer text</p></div>
</div>
</div>
<!-- Closed panels get the inert attribute so their content can't be tabbed into -->Toast
Use to confirm that an action succeeded or failed without blocking the page. Avoid for anything the person must act on. That belongs in a modal or an inline banner.
<div class="toast-region" aria-live="polite">
<div class="toast success">
<svg aria-hidden="true">...</svg>
<div class="toast-body">
<p class="t-title">Changes saved</p>
<p class="t-desc">Everyone on the project can see them now.</p>
</div>
<button class="toast-close" aria-label="Dismiss notification"><svg>...</svg></button>
</div>
</div>Bottom tab bar
Use as the main navigation of a mobile app, with 3 to 5 destinations. Avoid on tablet and desktop widths, where a sidebar like the one on this page does the job.
Home
This frame is fixed at 320px, the narrowest phone width still in common use, so the tab bar sits against real content and the safe-area edge.
<nav class="tab-bar" aria-label="App"> <button class="tab-bar-item active" aria-current="page"><svg>...</svg>Home</button> <button class="tab-bar-item"><svg>...</svg>Search</button> </nav> /* padding-bottom: max(8px, env(safe-area-inset-bottom)) keeps labels clear of the home indicator on notched phones */
Bottom sheet
Use for a short list of actions on a touch screen, where the thumb reaches the bottom more easily than a small menu. Avoid on tablet and desktop widths. Use the dropdown menu instead.
Project settings
The sheet rises from the bottom of this frame, the way it would on a phone.
Project actions
<div class="sheet-overlay"></div> <div class="bottom-sheet" role="dialog" aria-modal="true" aria-labelledby="sheet-title"> <div class="sheet-handle" aria-hidden="true"></div> <h3 id="sheet-title">Project actions</h3> <button class="sheet-action">Duplicate project</button> <button class="sheet-action danger">Delete project</button> </div> <!-- Escape and a tap on the overlay close it, focus returns to the trigger -->
Avatar
Use initials by default and a photo when one exists. Avoid the status dot unless presence matters in that view.
<span class="avatar avatar-xs">JD</span> <span class="avatar avatar-sm">JD</span> <span class="avatar">AM</span> <span class="avatar avatar-lg">RK</span> <span class="avatar avatar-xl">NM</span> <span class="avatar-wrap"> <span class="avatar">SC</span> <span class="avatar-status online" role="img" aria-label="Online"></span> </span>
Progress bar
Use when progress is measurable and the total is known. Avoid for waits of unknown length. Use a spinner instead.
<div class="progress-label"><span id="pl-upload">Uploading assets</span><span>68%</span></div> <div class="progress" role="progressbar" aria-labelledby="pl-upload" aria-valuenow="68" aria-valuemin="0" aria-valuemax="100"> <div class="progress-fill" style="width:68%"></div> </div>
Checkbox and radio
Use checkboxes when more than one option can be true, radios when exactly one can. Avoid a single checkbox for an on or off setting. Use a toggle instead.
<label class="check-row"> <input type="checkbox" checked> <span class="check-box" aria-hidden="true"></span> Email me weekly summaries </label> <label class="check-row"> <input type="radio" name="billing"> <span class="check-box radio" aria-hidden="true"></span> Monthly billing </label>
Tooltip
Use to label an icon-only control. Avoid for instructions someone needs before acting. Those belong inline.
<span class="tip-wrap">
<button class="btn btn-secondary btn-icon" aria-label="Copy link" aria-describedby="tip-copy">
<svg aria-hidden="true">...</svg>
</button>
<span class="tip" role="tooltip" id="tip-copy">Copies the shareable link</span>
</span>Modal
Use to confirm a destructive or hard-to-reverse action. Avoid for anything the person can undo with one click.
<div class="modal-overlay">
<div class="modal-box" role="dialog" aria-modal="true" aria-labelledby="modal-title" tabindex="-1">
<h2 id="modal-title">Delete this project?</h2>
<p>It will be removed for every collaborator. This can't be undone.</p>
<div class="modal-actions">
<button class="btn btn-secondary">Cancel</button>
<button class="btn btn-destructive">Delete project</button>
</div>
</div>
</div>Empty state
Use to say what's missing and what to do next. Avoid a bare "No results" with no way forward. That's a dead end, not a state.
No projects yet
Projects you create show up here, along with anyone you invite to them.
<div class="empty-state"> <svg aria-hidden="true">...</svg> <h3>No projects yet</h3> <p>Projects you create show up here, along with anyone you invite to them.</p> <button class="btn btn-primary btn-sm">Create a project</button> </div>
Breadcrumb
Use to show where a page sits in a nested hierarchy. Avoid on a flat, one-level site. It adds a control with nothing to navigate.
<nav class="breadcrumb" aria-label="Breadcrumb"> <a href="/">Home</a> <span class="sep" aria-hidden="true"><svg>...</svg></span> <a href="/projects">Projects</a> <span class="sep" aria-hidden="true"><svg>...</svg></span> <span class="current" aria-current="page">Q3 brand refresh</span> </nav>
Pagination
Use to move through a known, countable set of pages. Avoid for an endless feed. Use a "Load more" button instead.
<nav class="pagination" aria-label="Pagination"> <button class="page-btn nav" aria-label="Previous page"><svg>...</svg></button> <button class="page-btn active" aria-current="page">1</button> <button class="page-btn">2</button> <span class="page-ellipsis" aria-hidden="true">…</span> <button class="page-btn nav" aria-label="Next page"><svg>...</svg></button> </nav>
Dropdown menu
Use for a short list of actions on a single item. Avoid past about 8 items. That needs its own page or a select.
<div class="dropdown">
<button class="btn btn-secondary dropdown-trigger" aria-haspopup="menu" aria-expanded="false"
aria-controls="menu">Actions <svg aria-hidden="true">...</svg></button>
<div class="dropdown-panel" id="menu" role="menu" hidden>
<button class="dropdown-item" role="menuitem">Duplicate<kbd>⌘D</kbd></button>
<div class="dropdown-sep" role="separator"></div>
<button class="dropdown-item danger" role="menuitem">Delete project</button>
</div>
</div>Skeleton loader
Use to hold the exact shape of a row or card while its content loads. Avoid for waits under 300ms. A flash of skeleton looks more broken than a blank beat.
<div class="skeleton-block" role="status" aria-label="Loading content">
<div class="skel-row">
<span class="skel circle"></span>
<div class="skel-lines"><span class="skel l1"></span><span class="skel l2"></span></div>
</div>
<span class="skel media"></span>
</div>Slider
Use for a value picked from a range, where the feel matters. Avoid when the exact number matters more. Use a number input instead.
<div class="slider-field">
<span class="slider-label">
<label for="volume">Playback volume</label>
<b><output for="volume">68%</output></b>
</span>
<input class="slider-track" id="volume" type="range" min="0" max="100" value="68">
</div>Tokens
Primitives Raw values with no meaning attached
Violet ramp
violet-100violet-400violet-600violet-700Neutral ramp
ink-05ink-20ink-60ink-100Semantic What components reference
Color, paired the way it's used
action-primaryaction-warnstatus-positivestatus-negativedestructiveSpacing
Radius
Touch target
Type
Sizing
Icons
Breakpoint
Only the semantic layer changes between light and dark. Every color that sits behind white text has its own -fill token, pinned to a value that clears 4.5:1 in both modes, so the plain color can lighten in dark mode for readable text without breaking the buttons that use it. The contrast badges above measure the real pairing and update when you change theme, mode or style.
Figma export
Use to hand off a whole style direction at once. Avoid mixing kits on one screen.
Style kit tokens
Paste one of these blocks into your project to switch the whole system. Color stays the same across all three on purpose. Only radius and shadow change.
Component vectors
Every value is a literal color and pixel, so it survives the trip into Figma. Copy an SVG and paste it onto a Figma canvas with Cmd or Ctrl+V. It arrives as editable vector layers, not a flat image. Download saves the same file.