components.*Components
Live interactive demos with token annotations. Every component is assembled from the Design Better token set — no hardcoded values outside this file.
Actions
01 · Buttons
Sizes — Primary
color.brand.signalcolor.brand.signal-brightcolor.text.primarycolor.bgmotion.duration.fastInputs & selection
02 · Form controls
Text input
Please enter a valid email address.
Select menu
Textarea
Checkbox · Radio · Toggle
Email notifications
Download automatically
color.brand.signalcolor.border.subtlecolor.surface.raisedcolor.text.primarycolor.text.dimcolor.error03 · Date pickers
Preset ranges — segment control
Selected: 30D
In context — dashboard toolbar
Subscriber growth
Input + calendar popover
October 2026
S
M
T
W
T
F
S
Calendar — anatomy reference
October 2026
S
M
T
W
T
F
S
▶Spec notes
—── Segment control (preset ranges) ──
—Pattern: connected segment control — not individual pills. Buttons flush with 1px gap (hairline divider via wrapper bg).
—Active segment: background #FF4725 · text #0A0A0A · weight 600 — never white text on signal
—Inactive segment: background var(--surface-1) · text var(--text-2) — use var(--surface-2) inside a surface-1 container
—Hover (inactive only): background var(--surface-2) · text var(--text-1) · 120ms standard easing
—Labels: mono · uppercase · 0.08em tracking · 12px standalone / 11px compact toolbar
—No border-radius anywhere — not on the group, not on individual segments
—Preset values: Today · 7D · 30D · 90D · 1Y · All — keep this set unless the product needs different granularity
—Never use for multi-select — use Filter pills (s-18) instead
—── Calendar picker (single date) ──
—Trigger input: surface-1 bg · hairline border → signal orange on open · mono 14px · calendar icon left · border-radius 0
—Popover: surface-2 bg · hairline border · 4px below trigger · 296px wide · no border-radius anywhere
—Month header: body 16px / 600 · text-1 · ↑↓ nav buttons · text-2 → text-1 hover
—Day headers: S M T W T F S · mono 11px · uppercase · 0.1em tracking · text-3
—Day cells: mono 13px · 2px gap grid · no border-radius — never round cells, not even today or selected
—Today (unselected): transparent bg · signal orange text · weight 600
—Selected: signal orange bg · ink text (#0A0A0A) · weight 700 — never white on signal
—Adjacent month days: text-3 · clicking navigates to that month
—Footer: Clear (left) · Today (right) · body 14px / 500 · signal orange · hover #e03d1e
color.brand.signalcolor.surface.raisedcolor.surface.elevatedcolor.border.hairlinefont.monofont.bodymotion.duration.fast04 · Filter pills & menus
Pills, for one or two dimensions
Active filters
RoleDesignerStageEarly careerDesign Systems
Topic selection (pick up to 3)
01 selected · pick up to 3
Menus, for three or more dimensions or any multi-select
Topic
- Branding
- Career
- Design systems
- Leadership
- Research
- Typography
▶Spec notes
—Key:value pill — label: 10px · uppercase · 0.1em tracking · text-3 / value: 13px · semibold · text-1
—Value-only pill — 13px · 0.08em tracking · uppercase — used for standalone topics/tags
—Toggle variant — selected: signal orange border + 10% signal bg · deselected: divider border
—Counter: signal orange number + muted text · mono · 11px — reinforces selection constraint
—All filter-pill variants share: 100px border-radius, the one sanctioned exception to square corners · 1px border · 5px 12px padding
—DROPDOWN VARIANT, for filtering across several dimensions at once. Pills are the default and stop working past about two dimensions or a dozen values: they wrap into a wall and push the content below the fold. Past that, use one dropdown per dimension on a single line.
—Dropdown row: one select per dimension · the blank option means 'no filter' and stays out of the URL · a dimension needing several values at once renders as a searchable checkbox popover rather than a select.
—Either variant writes the query string and lets the server filter, so a filtered view is shareable and survives a reload. A filter held only in component state is a filter nobody can send to a colleague.
—Choosing between them: count FILTER dimensions, not values. One or two read better as pills because the options are visible without a click. Three or more, or any dimension needing multi-select, goes to menus.
—A sort control is not a filter dimension (see patterns.sortAndFilter), so do not count it. But its presence alone sends the row to menus. Sort has ordered options, exactly one always active and no off state, so it cannot be a pill: a pill row needs a deselected state to mean anything. Two filters plus a sort as pills strands one dropdown beside the row, and a mixed row reads as two controls that do not belong together.
color.brand.signalcolor.text.primarycolor.text.dimcolor.text.faintcolor.border.subtleborderRadius.pill05 · Selection cards
Options
Pricing tiers
selected border: 1px solid var(--signal-text); background: var(--surface-2);
unselected border: 1px solid var(--divider-strong); /* nothing reserved, nothing shifts */
transition background-color, border-color 120ms cubic-bezier(0.4, 0, 0.2, 1);
▶Spec notes
—Grid: auto-fill minmax(280px, 1fr) · 12px gap between cards. A real gap, not the painted grid-gap technique: each card owns its border so the selected one can recolour it, and a hairline two cards share cannot change colour for one of them.
—Default: surface-1 · 1px solid var(--divider-strong), the box-edge role.
—Selected: surface-2 · the same 1px border turns var(--signal-text). Two cues, surface and border. Nothing is reserved and nothing shifts, because the border was already there.
—Use var(--signal-text), never raw #FF4725, for the border. Measured on the selected surface: --signal-text is 4.54:1 in the light theme and 4.91:1 in the dark, clearing the 3:1 a state indicator needs. Raw signal is 2.81:1 in the light theme and fails.
—Not an outline. On a square card a 2px signal outline is indistinguishable from the keyboard focus ring. The focus ring sits 2px OUTSIDE the card with a gap, so an inset border-colour change stays distinct from it, which is the property the outline lacked.
—Not a 3px left stripe. Earlier versions used one with a transparent 3px reserved on every unselected card. The border-colour change does the same job with no reserve, matches what designbetter.com already ships, and keeps the left stripe meaning status and emphasis (alerts, toasts, pull quotes) rather than also meaning 'selected'.
—Index: mono · 11px · text-3 · top-left, giving each option a numbered identity.
—Radio: 18px circle · 2px border · filled signal orange with an ink centre dot when selected. A circle is a shape, not a softened corner.
—Transition: background-color and border-color · 120ms standard easing.
—PRICING TIERS use the same treatment. The tiers are a choice of one, so mark them up as a radiogroup of radios with aria-checked, and arrow keys move between them.
—Change the key figure, such as a price, to var(--signal-text) on selection: one reinforcing signal touch. Raw #FF4725 here is text at 2.81:1 in the light theme and fails even as large type.
—Selection is not hover. Do not combine the hover lift with a selected card, or a chosen card looks permanently mid-interaction.
—Never mark selection with box-shadow alone: it disappears in high-contrast mode and at low opacity on a dark ground.
color.brand.signalcolor.surface.raisedcolor.surface.elevatedcolor.border.subtlemotion.duration.fastNavigation
06 · Tabs, breadcrumb & pagination
Tabs
Breadcrumb
Pagination
…
▶Spec notes
—Tabs: body font · 14px · 10px 16px padding · active is weight 500 + text-1, inactive weight 400 + text-3
—Tabs: the active marker is a 2px signal underline. Inactive tabs carry 2px solid transparent so switching never shifts the row, and the strip sits on margin-bottom -1px so its underline covers the container's 1px divider rather than stacking on it.
—Tabs: transition colour and border-colour only, 120ms. Never animate the indicator's position; this is a row of buttons, not a sliding pill.
—Breadcrumb: body font · 13px · 6px gap · trail in text-3, current page in text-2 and not a link. The separator is a chevron at 12px in text-3, never a slash.
—Pagination: 6px 12px cells · 1px divider border · border-radius 0 · 13px. The page number is mono so the control does not resize as the digits change width.
—All three are navigation, so they belong in a <nav>, and the current tab or page carries aria-current. Colour alone does not announce position.
—Breadcrumbs are for depth, tabs are for switching a view in place. A breadcrumb trail that does not reflect real URL hierarchy is decoration and should be cut.
color.brand.signalcolor.border.subtlecolor.text.primarycolor.text.faintcolor.surface.raised07 · Frosted nav
▶Spec notes
—position: sticky; top: 0; backdrop-filter: saturate(180%) blur(12px); -webkit-backdrop-filter: ...
—Background: rgba(10,10,10,0.85) dark · rgba(245,242,236,0.85) light — 0.85 opacity minimum
—CTA: square corners, like every other button in the system · signal orange bg · ink text, never white, which measures 3.40:1 on signal and fails AA · 6px 14px padding
—Nav links: body font · 14px · 120ms transition on colour.
—COLOUR IS FIXED, NOT THEMED. The bar paints its own dark ground in every theme, so its foreground cannot use --text-2, --divider or any other theme-dependent token: on the light theme those resolve dark, and the links go dark-on-dark. Use literal light values here — rgba(242,240,236,0.72) for links, rgba(255,255,255,0.12) for the bottom edge. This is the one place in the system where a literal beats a token, and the reason is that the surface is theme-independent while the token is not.
—THE SMELL, and it is greppable: one rule block that sets its own background with a LITERAL while taking its foreground from a TOKEN. Those two facts together are the bug, whatever it renders as. It hides because it can render correctly by accident: on designbetter.com the header was a sibling of <main> rather than a child, so it never entered the light scope and inherited the dark set. Right by DOM placement, not by construction, and a visual check passes it. Wrap the surface in the light theme to prove it, and if two ink surfaces on one site solve this two different ways, check both. Stated once for every fixed ground in techniques.fixedGround.
—The wordmark is the bar: 28px tall in a 56px bar. It was 20px and read as a footer credit rather than as the masthead.
color.brand.signalcolor.brand.inkcolor.border.subtlemotion.duration.fastmotion.easing.standard08 · App shell & sidebar
App sidebar
◈Dashboard
◎Content
◻Audience
◇Revenue
◁Settings
AW
Active state, square corners
◈Dashboard
◎Content hover
◻Audience
active bg
#FF4725active text
#0A0A0Ahover bg
rgba(255,255,255,0.08)default text
rgba(255,255,255,0.65)border-radius
0 — alwaysLogo placement
src
/Logos/SVG/DesignBetterWhite.svgheight
28pxfilter
brightness(0) invert(1)opacity
0.9zone padding
0 16px 16pxzone border-bottom
1px solid rgba(255,255,255,0.06)▶Spec notes
—Sidebar background: #111111 · 1px right border rgba(255,255,255,0.06) — slightly lifted from app bg (#0A0A0A)
—Logo: <img src='/Logos/SVG/DesignBetterWhite.svg'> · height 28px · filter: brightness(0) invert(1) · opacity: 0.9
—Logo zone: padding 0 16px 16px · border-bottom 1px solid rgba(255,255,255,0.06) — always the SVG asset, never plain text
—Nav items: padding 7–8px 10px · gap 2px between items · full-width rows
—Active state: background #FF4725 · text #0A0A0A · font-weight 500 · border-radius 0 — no rounding, ever
—Hover state: background rgba(255,255,255,0.08) · text rgba(255,255,255,0.9), brightened from 0.65 · 120ms transition · border-radius 0. Literal values, never var(--surface-2): the panel is dark in every theme and the token resolves to cream on the light theme, putting white text on a cream row. See techniques.fixedGround.
—Default state: transparent background · text rgba(255,255,255,0.65)
—User footer: border-top rgba(255,255,255,0.06) · avatar circle (border-radius 50% is correct for avatar only) · name body 12px/500 · role body 11px text-3
—Do NOT use border-radius on nav items, active states, or any status indicator in the sidebar — pills are for avatars only
color.brand.signalcolor.surface.basecolor.border.subtlefont.bodyfont.monomotion.duration.normal09 · Scroll rail
Guests
- Frank Chimero
- Jessica Hische
- Paula Scher
- Michael Bierut
- Debbie Millman
- Aaron Draplin
▶Spec notes
—The way a collection is presented when there is more of it than fits. Seven call sites on designbetter.com alone, which is every surface showing more than about four of anything.
—Rail: flex · 16px gap · overflow-x auto · scroll-snap-type x mandatory · scroll-snap-align start ON THE LIST ITEMS THEMSELVES, not on the card inside them · 8px padding-bottom so a focus ring is not clipped · scrollbar hidden, because the arrows are the affordance and a native bar under the art reads as a second one.
—That emphasis is load-bearing. A rail can declare snap-type and give it nothing to snap to: put scroll-snap-align on the card class and the item laying out the row computes to none, so the rail scrolls completely free while the stylesheet looks correct. It was shipped that way on every rail on designbetter.com and found only when this line was checked against it.
—Arrows: 38px square, 32px in the compact variant · 1px divider-strong border, the box-edge role · transparent fill · invert to text-1 ground on hover · 8px apart · square corners.
—Arrow placement has two cases. By default they sit at the right edge of the section head. When the rail has its own buttons beneath it, the arrows move down and join that row instead: two sets of controls bracketing the rail leaves an awkward gap over the artwork.
—Each arrow scrolls by one item, not by one viewport. A viewport step skips items at narrow widths, which is where the rail matters most.
—MEASURE that step from the first item's width plus the computed column gap, at press time. Do not pass it in. Every call site that configures a pixel step gets it right once and then drifts as the card changes: on designbetter.com six callers had hard-coded 356, 460, 340, 256, 336 and 1, and the guests rail was jumping two cards per press. Measuring is three lines and cannot rot.
—Mark it up as a real list. An anchor cannot take role="listitem", so a rail that carries the role anyway has it dropped by the browser and announces nothing. Give the list an aria-label naming the collection, and the arrows labels that name it too — "More guests", not "Next".
—With JavaScript off, or before hydration, the rail still scrolls: it is an overflow container. Only the arrows are inert, so never make them the sole way to reach the content.
—Do not use a rail for anything the reader must see all of. It hides most of its content by design, so a pricing comparison or a set of options belongs in a grid.
color.border.strongcolor.text.primarycolor.surface.raisedmotion.duration.fastmotion.easing.standardspacing.4Content
10 · Content cards
color.surface.raisedcolor.surface.hovercolor.video-fg/bgcolor.podcast-fg/bgfont.displayfont.editorialmotion.duration.normal11 · Key-value card
American Giant
Bonus episode + always-on mid-roll campaign
Goal
Drive brand awareness and trust for premium made-in-America apparel.
Format
1-hour bonus episode with the founder, followed by a host-read mid-roll campaign with a discount code across the full catalog.
Outcome
Strong audience response on social and direct listener feedback to the hosts.
"Thank you for making time to offer an episode that honored the manufactured and tactile roots in my love of design."
Carlo Blackmore · listener
▶Spec notes
—Eyebrow: signal-text · mono · 11px · uppercase · 0.12em tracking
—Key: the system's eyebrow, stacked ABOVE its value, never beside it · mono · 11px · weight 400 · 0.12em tracking · uppercase · text-3 · 8px to the value below (label-to-field).
—A fixed key column was the old answer and it reads as sloppy at any real content length: the longest label sets a width every row then has to live with, short labels hang in space, and the values take a ragged left edge that fights the title above them. Stacked, every line in the card starts at the same x.
—Tracking does more than size to make a label read as a label rather than as small body copy. 0.12em at 11px separates cleanly from 15px body; 0.08em at 13px does not.
—24px between pairs (item-to-item), 8px inside one. The gap between pairs must be visibly larger than the gap inside a pair, or the card reads as six loose lines instead of three labelled facts.
—Value column: body font · 15px · text-2 · 1.5 line-height
—Blockquote: border-left 3px #FF4725 · editorial italic · 16px · text-1 — no outer border on card
—Hover: surface-2 bg + translateY(-2px) · 240ms standard easing
font.displayfont.editorialfont.monocolor.brand.signalcolor.surface.raisedmotion.duration.normal12 · Editorial step numeral
▶Spec notes
—Step label: body font · 11px · uppercase · 0.14em tracking · signal orange — names the dimension being answered
—Numeral: display font (F37 Lineca) · clamp(80px, 12vw, 120px) · weight 800 · -0.02em tracking · text-1
—Counter: body · 13px · text-3 · positioned absolute top-right of numeral block
—Grid: auto-fill minmax(160px, 1fr) · 1px gap divider technique — stays 4-up on wide, 2-up mid, 1-up mobile
—Used as wayfinding in multi-step flows — not decorative; always pairs with a question or content section
font.displaycolor.brand.signalcolor.text.primarycolor.text.faintcolor.surface.raised13 · Quotes & attribution
Pull quote
Standard, left rule
“Design is not just what it looks like and feels like. Design is how it works.”
Steve Jobs · Co-founder, Apple
Signal panel, editorial feature
“I’ve always been interested in this intersection of science and engineering and art and the humanities.”
Andrew Evans · Founder, The Magic Patio
Testimonial
“Design Better changed how I think about craft. Every episode leaves me with something actionable.”
SR
Sara Rodriguez
Design Lead · Stripe
Byline
AW
Aarron Walter
Co-founder, Design Better · May 18, 2026
▶Spec notes
—Pull quote: editorial italic · 22px · 1.45 line-height · text-1 · 3px signal left rule · 24px padding-left · max-width 640px. Never roman weight. See spacing.componentSpace for the air it needs.
—Attribution: the system eyebrow · mono · 11px · 0.12em · uppercase · text-3 · on its own line below the quote, never inside the quotation marks, and never with a dash prefix: the voice guide bans em dashes, and the line break already does the separating. Name, then title, joined with a middle dot.
—Signal panel variant, for a high-impact editorial moment only: signal orange ground · display font, not italic · 24px · ink text · attribution in ink at 55%. Never white on signal, which measures 3.40:1 and fails AA.
—Quote marks are typographic, curly and part of the text. No oversized decorative glyph behind it, and never a quotation mark as an image.
—Testimonial: editorial italic · 16px · 1.65 line-height, with a 40px avatar, name at weight 600 and role in text-3 at 12px. Square card, round avatar: a circle is a shape, not a softened corner.
—Byline: body font · 14px name in text-1, 12px meta in text-3, 12px gap, avatar at 28px.
—Use the pull quote to lift a line out of a piece the reader is already in. Use the testimonial when the speaker's identity is the evidence. They look alike and argue differently, so do not swap one for the other to vary the rhythm.
—Attribute every quote to a named person with their role. An unattributed quote on a page that sells something reads as invented.
—This section absorbed the separate Pullquote section, which documented the same component at a different size. Three sources had given three sizes (28px, 22px and '18 to 22px'); 22px is the one the dedicated spec and the page pattern agreed on.
font.editorialcolor.brand.signalfont.displayfont.monocolor.text.dimcolor.text.faintLabels & identity
14 · Badges & tags
Content type
PodcastVideoNewsletterBook
Status
NewFeaturedFree
Tags (click × to dismiss)
Design leadershipSystems thinkingProduct designCareer growth
Count badge
31299
color.content-type.podcastcolor.content-type.videocolor.content-type.newslettercolor.content-type.bookcolor.brand.signalborderRadius.pill15 · Avatar & identity
Initials — sizes
AW
AW
AW
EW
JL
MK
Presence indicators
AW
SR
JL
Avatar stack
AW
SR
JL
MK
System photo avatars
/avatars/avatar-01.png → avatar-12.png
color.brand.signalcolor.presence.onlinecolor.presence.awaycolor.presence.offlinecolor.bg.ink16 · Logo grid & row
Grid, when the marks are the content
Design Better subscribers work at

Source: 2025 listener survey · 250,000+ subscriber list analysis
Grid, inverted on a dark band
Design Better subscribers work at

Source: 2025 listener survey · 250,000+ subscriber list analysis
Row, when the marks are evidence for the text above
▶Spec notes
—Text wordmarks when SVG unavailable: display font · weight 500 · 13px · text-3 → text-1 on hover
—Image logos: filter: brightness(0) invert(0.95) dark · brightness(0) light — never full-color
—The single-tone rule binds on BOTH themes even when the consumer only renders one today. A mark drawn in currentColor looks correct on light and goes black-on-black the day a dark toggle ships, and nothing will catch it in between because there is nothing to see.
—INVERTED: the tint follows the SURFACE the grid sits on, not the page theme. A logo row on a black band inside a light page needs white marks while the theme still says light, so pass the surface explicitly (surface="dark") rather than reading the theme. Keying it to the theme renders black marks on a black band, which is the frosted-nav bug in a different component.
—Grid: auto-fill · minmax(100px, 1fr) · gap 4px vertical / 16px horizontal
—Label: mono · 11px · uppercase · 0.12em tracking · text-3 · centered
—A max-width plus object-fit: contain silently overrides the height you set. Hit the width cap and contain scales the mark down, height with it — a wordmark whose CSS says 20px can draw at 13px. Devtools still reports 20px on the box, so this is slow to find. Give wide marks a larger max-width rather than fighting it with height.
—A mark only sizes predictably once its viewBox is trimmed to the ink. Untrimmed padding inside the file makes the CSS height a container height rather than the glyph height, and four marks set to one number read at four different optical sizes. Trim at the source, then the CSS height IS the optical height.
—Even trimmed, one height will not serve every mark. An all-caps wordmark, a lowercase wordmark, a stacked lockup and an icon each need their own optical height. Store it per mark — this is expected, not a workaround.
—ROW VARIANT, the more common of the two. A single wrapping line rather than a grid: used for a partner wall and for the short mark row under a sign-up form. Same optical-height rule, same trimmed viewBox requirement.
—Row: pass each mark's optical height and width as custom properties on the item rather than styling by selector, so one row can scale every mark by a fraction for a smaller placement without a second set of rules.
—Use the grid when the marks are the content and the reader scans them. Use the row when they are evidence for the text above and the reader skims.
font.displayfont.monocolor.text.dimcolor.text.faintmotion.duration.normalData
17 · Tables & dividers
Table
| Episode | Guest | Date | Duration |
|---|---|---|---|
| Frank Chimero on design problems | Frank Chimero | May 2026 | 48 min |
| The craft of simplicity with Jason Fried | Jason Fried | Apr 2026 | 52 min |
| Systems thinking at Figma | Rasmus Andersson | Apr 2026 | 41 min |
Divider variants
or continue with
▶Spec notes
—Header: body · 11px · 500 · uppercase · 0.08em tracking · text-3 · borderBottom divider
—Body cell (text): body font · 15px · text-1 (primary column) or text-2 (secondary) · 12px 14px padding
—Body cell (numeric): mono · 13px · text-2 · right-aligned — always mono for scannable columns
—Negative numbers: content.video.fg (#E06060) — not red, not text-1
—Alternating rows: surface-alt on even rows — only when density requires it
—Row hover: surface-1 background · 120ms transition — add on interactive tables
—Overflow: overflowX auto on the wrapper — never truncate numbers, truncate text with ellipsis
color.border.subtlecolor.surface.raisedcolor.text.primarycolor.text.dimcolor.text.faintfont.mono18 · Stat blocks
Single stat
Episodes published
Single stat — with suffix
.M+
Total downloads
Stat group
▶Spec notes
—Figure: display font · 700 · 72px desktop, 48px below 600px · line-height 1 · letter-spacing -0.03em
—Suffix and prefix: half the figure size · suffix in signal orange, prefix in text-1 at 60% · neither rolls
—Odometer: a 0-9 reel per digit, 1.6s cubic-bezier(0.16, 1, 0.3, 1), each digit starting 55ms after the one to its right, so the number settles right to left. Rolls once, on scroll into view.
—FIGURE WIDTHS: F37 Lineca's digits are proportional and the face ships no tabular (tnum) feature, so font-variant-numeric cannot fix this. The spread is 26.4%: a 1 is 0.514em where a 0 is 0.698em. A naive odometer gives every digit a window as wide as the widest glyph in its reel, so a 1 carries a sixth of an em of dead air and '10.8' reads as '1 0.8'.
—The fix is a per-digit window: hold the widest width while the reel is moving so nothing is clipped on its way past, then animate to the landing digit's own advance on the same curve and delay as the vertical roll, so the number settles horizontally and vertically at once. The advance table is read from the .otf, not measured off a screenshot.
—This misreads as bad kerning rather than as a layout bug, so it sends people into the typeface instead of the component. If you set this odometer in a face with tabular figures, none of the above applies: use them and skip the width table.
—Reduced motion: render the number already landed. Never animate it.
—The stat group paints its hairlines with the grid-gap technique: container takes the divider colour, gap 1px, cells take the surface colour. Add background-clip: padding-box to the container, or its background also paints under its own border and a translucent border composites over a copy of itself — outer rules come out visibly darker than inner ones. A short last row shows the container colour through the empty cells, so fill them or end the grid.
—Numbers at or above 10,000 abbreviate to K, at or above 1,000,000 to M. The unit renders at suffix size and does not pass through the odometer.
font.displaycolor.text.primarycolor.text.dimcolor.brand.signalcolor.surface.raisedmotion.easing.standard19 · KPI card
Dashboard grid — 4 cards
Revenue YTD
$142,800
↑ 18% vs last year
Expenses YTD
$38,540
64% of annual budget
Net Income
$104,260
72.9% margin
Goal progress
71%
$201K annual target
Negative value treatment
Net change
+$8,200
Month over month
Refunds
-$1,340
3 transactions
Overdue
-$4,000
Invoice #0041
Runway
14 mo
At current burn rate
▶Spec notes
—Label: mono · 11px · 500 · uppercase · 0.10em tracking · text-3 — never display font
—Value: mono · 26px · 700 · text-1 · -0.02em letter-spacing · line-height 1
—Negative values: content.video.fg (#E06060) — not arbitrary red, not text-1
—Sub-label: body · 12px · text-3 — trend, context, or timeframe only; one clause max
—Card surface: surface-1 · 0 border-radius · borderTop 2px divider-color — use signal orange top border for featured metric
—Grid gap: 2px between cards reads as a hairline grid on a dark bg — preferred over border on each card
—Never put currency symbols in the label — put them in the value string
font.monocolor.text.primarycolor.content.video.fgcolor.surface.raisedcolor.border.subtle20 · Data visualization
Sequential chart palette — always apply in this order
S1Signal
Primary series · revenue · key metric#FF4725S2Podcast
Second series · categorical#9D90F8S3Newsletter
Third series · positive trend#4DB87AS4Video
Fourth series · negative / warning#E06060S5Amber
Fifth series · caution / neutral#C8973AS6Teal
Sixth series — extended use only#0D8880Chart type selection
Bar — compare discrete categories
0
25
50
J
F
M
A
M
J
Max 6 categories · always baseline 0 · border-radius 0
Line / area — trends over time, multi-series
Podcast
Newsletter
Max 4 series · area fill at 12–18% opacity · legend below or right
Axis, tooltip & legend spec
Jun 2026
84.2kPodcast
63.7kNewsletter
Tooltip — border-radius: 0
Tooltip bg
var(--surface-2)Tooltip border
1px solid rgba(255,255,255,0.08)Tooltip radius
0Axis ticks
mono 9–10px · rgba(242,240,236,0.38)Gridlines
1px dashed rgba(255,255,255,0.06)Baseline
1px solid rgba(255,255,255,0.12)Bar radius
0 — never round data barsArea opacity
12–18% of series colorLegend: ≤3 series
right-aligned · 16px swatchLegend: 4+ series
below chart · wrap rowAccessibility
always pair color with label textAxis baseline
always start at 0 (bar & area)▶Spec notes
—Use the sequential palette in order — never skip colors or invent new ones for a 6-or-fewer series chart
—Signal (#FF4725) is always series 1 — it is the primary or total metric
—Bar charts: border-radius 0 — rounding data bars undermines precision reading
—Gridlines: 1px dashed rgba(255,255,255,0.06) — subtle, never compete with data
—Axis ticks: mono 10px · text-3 (0.38 opacity) — same token as stat sub-labels
—Tooltips: surface-2 bg · rgba(255,255,255,0.08) border · mono font for values · 0 border-radius
—Chart background: surface-1 or transparent — never surface-2 (too much contrast lift)
—Area fills: 18–25% opacity of the series color — matches the content-type bg token pattern
color.brand.signalcolor.content.podcast.fgcolor.content.newsletter.fgcolor.content.video.fgcolor.content.book.fgcolor.surface.raisedFeedback & state
21 · Alerts
Static alert
New episodes available
Three new episodes have been added to your playlist since your last visit.
Subscription confirmed
You're now subscribed to Design Better. Your first issue arrives Tuesday.
Your trial ends soon
Your free trial expires in 3 days. Upgrade to keep full access.
Payment failed
We couldn't process your card. Please update your payment method.
Flash alert — one-shot attention animation
ℹ
Your export is ready. Download will start in a moment.
✓
Episode published. Changes are live.
▲
Draft has unsaved changes. Save before leaving.
✕
Upload failed. File size exceeds 50 MB limit.
Info
Success
Warning
Error
▶Spec notes
—── Static alert ──
—Background: content-type bg token (podcast-bg, newsletter-bg, book-bg, video-bg) — never a custom tint.
—Left edge: no explicit border — background alone carries the variant signal.
—Icon: Central Icons, 18px, variant accent color. Title: body 16px / 600, accent color. Message: body 15px / text-2, paddingLeft 26px to align with title text.
—No border-radius — ever.
—── Flash alert ──
—One-shot keyframe on mount — fires once, never loops. Duration: 900ms, easing: ease-out.
—Animation peaks at 18% (≈162ms) — background and inset border flare to ~22% opacity, then decay to the 6% resting tint.
—Use CSS class 'db-flash-alert' + data-variant attribute. Never set animation properties inline.
—Left accent bar: 2px solid, variant color — no border-radius.
—To replay the animation: remount the component (change the key prop).
—Do not show more than 3 stacked flash alerts at once. If a fourth arrives, replace the oldest.
—Do not use flash alerts for persistent banners, marketing messages, or onboarding tips.
color.podcast-fg/bgcolor.newsletter-fg/bgcolor.book-fg/bgcolor.video-fg/bgmotion.duration.normal22 · Toasts
Changes saved
Your settings have been updated.
Connection lost
Check your network and try again.
Approaching limit
You've used 90% of your monthly quota.
Update available
A new version is ready to install.
desktop → position: fixed · top: 24px · right: 24px · z-index: 9000
mobile → position: fixed · bottom: 24px · left/right: 16px · full-width
auto-dismiss → 4 s (success / info) · never auto-dismiss errors
▶Spec notes
—Structure: left 3px accent stripe · semantic fill bg · border-radius 0 · padding 12px 14px
—Title: body · 14px · weight 700 · variant accent color · margin-bottom 2px
—Body: body · 14px · text-2 · line-height 1.4 — one sentence max
—Dismiss: × button top-right · text-3 default · text-1 hover · no border · no bg
—Success (newsletter-fg/bg) · Error (video-fg/bg) · Warning (book-fg/bg) · Info (podcast-fg/bg)
—Never use signal orange (#FF4725) as a toast accent — it conflicts with primary CTA semantics
—Desktop position: fixed · top: 24px · right: 24px · z-index: 9000 · max-width: 380px
—Mobile position: fixed · bottom: 24px · left/right: 16px · full-width — never top-right on mobile
—Auto-dismiss: 4 s for success and info · Errors and warnings persist until manually dismissed
—Stack newest on top with 8px gap · maximum 3 visible · oldest exits automatically
—Enter: translateY(-8px) opacity 0 → 0 opacity 1 · 240ms decelerate easing
color.content.newsletter.fgcolor.content.video.fgcolor.content.book.fgcolor.content.podcast.fgmotion.duration.normalmotion.easing.decelerate23 · Loading & progress
Progress bar
Progress30%
Progress75%
Labeled — goal progress
Revenue goal$142K of $201K
71% — 5 months remainingInline — list row context
Partnerships64%$91,400
Advertising18%$25,700
Workshops11%$15,700
Substack7%$9,900
Spinners and skeletons
Spinner — sm · md · lg
sm · 16px
md · 28px
lg · 44px
Skeleton — card placeholder
▶Spec notes
—── Progress bar ──
—Track: 4px height (default) · 6px (goal) · 3px (inline row) · divider color · no border-radius
—Fill: signal orange — use content-type color when the bar maps to a typed data series
—Label: body 13px text-2 left · mono 12px text-3 right · gap 6px below label, above bar
—Goal variant: show 'X of Y' in text-3 at half opacity after 'of' — never show raw fraction on its own
—Inline variant: 3px height · no labels · sits inside a list row between name and value columns
—Animate fill on mount: width transition 400ms standard easing
—── Spinner ──
—Track ring: var(--divider-strong) · Active arc: #FF4725 hardcoded (not var — animation contexts need a static value)
—sm 16px / 1.5px border · md 28px / 2px · lg 44px / 2.5px — always border-radius: 50%
—Keyframe: @keyframes db-spin { to { transform: rotate(360deg); } } · clockwise only
—Never embed a spinner inside a primary button — disable the button and show the spinner adjacently
—── Skeleton ──
—Skeleton blocks: var(--surface-1) on var(--surface-2) card bg — one step of surface contrast
—border-radius 0 for text-line shapes and for image or cover placeholders alike
—Keyframe: @keyframes db-skeleton-pulse { 0%, 100% { opacity: 1 } 50% { opacity: 0.35 } } · 1.4s ease-in-out
—Stagger animationDelay by 0.12–0.15s per sibling block — avoids all blocks pulsing in lockstep
—Remove skeleton immediately when data arrives — never fade out; the content replaces it instantly
—This section absorbed the separate Loading states section. Both demoed spinners and skeletons, and each spec documented only half of what its demo showed.
color.brand.signalcolor.surface.raisedcolor.border.subtlemotion.duration.normalmotion.easing.standard24 · Empty state
Default — first-run / zero state
No episodes yet
Episodes you publish will appear here. Start by recording your first conversation.
Search / filter — no results
No results for “Braun”
Try different keywords or clear filters
Inline — table or list context
TransactionCategoryDateAmount
No transactions match your filters.
▶Spec notes
—Default (zero state): icon + headline + body + optional CTA — centered, 56px vertical padding
—Icon container: 44×44px · 1px divider border · text-3 color — use a Central Icons outline icon
—Headline: display font · 18px · 600 · text-1 — name what's missing, not what to do
—Body: body font · 14px · text-3 · max-width 280px · 1.5 line-height — one sentence, action-oriented
—CTA: primary button if creating is the next step · ghost/secondary if navigating elsewhere
—Search/filter variant: no icon · display 16px text-2 headline · body 13px text-3 with inline signal link to clear
—Inline (table) variant: single centered body line · 36px vertical padding · borderBottom divider — no icon, no CTA
—Never say 'Nothing here yet' — say what specifically is absent and what to do about it
font.displayfont.bodycolor.text.primarycolor.text.faintcolor.border.subtlecolor.brand.signalOverlays & marketing
25 · Disclosure & overlay
Accordion
We publish new episodes on a bi-weekly schedule, with bonus content and newsletter issues filling the weeks in between. Paid members get early access.
Tooltip (static demo)
Opens in new tab
Dropdown menu (static open state)
▶Spec notes
—Accordion: 16px 0 padding on the trigger · 15px at weight 500 · 1px divider between rows, with the top and bottom edges ruled so the set reads as one block.
—Accordion: the trigger is a <button> inside the heading, with aria-expanded and aria-controls. A div with an onClick is not reachable by keyboard and is the usual way this ships broken.
—Accordion: the chevron rotates 180 degrees on open, 240ms standard easing. Animating height is optional; animating it badly is worse than not animating it.
—Tooltip: 8px 12px padding · 12px body font · inverted fill, so text takes var(--bg) on a text-1 ground · square corners · 8px offset from its trigger.
—Tooltip: supplementary only. Anything a user must read to complete the task belongs in visible copy, because a tooltip is unreachable on touch and invisible to someone who never hovers.
—Dropdown menu: surface-1 panel, 1px divider border, square corners, items at 14px with surface-2 on hover. Opens on click, never on hover, and closes on Escape and on outside click.
—Everything here hides content behind an interaction, so decide what the crawler and the printed page see. An accordion's content should be in the DOM; a tooltip's does not need to be.
color.surface.raisedcolor.border.subtlecolor.border.strongcolor.text.primarycolor.text.dimcolor.error26 · Membership band
Pitch band, to convert
You’re missing the best of Design Better.
Membership is where the conversations keep going after the episode ends.
Most members expense it. Here is a letter for your manager.
Join Design Better- Members SlackA working community of senior designers, not a broadcast channel.
- Design ClubAnnual subscribersA fortnightly call where members bring real work and get real feedback.
- Every episodeThe full back catalogue, including members-only conversations.
- The RoundupThe week in design and technology, read so you do not have to.
- The booksEvery Design Better handbook, in full.
- Early accessAnnual subscribersNew formats and events before they open to everyone.
Members and subscribers work at
Perks band, on a page that has already pitched
- Members SlackA working community of senior designers, not a broadcast channel.
- Design ClubAnnual subscribersA fortnightly call where members bring real work and get real feedback.
- Every episodeThe full back catalogue, including members-only conversations.
- The RoundupThe week in design and technology, read so you do not have to.
- The booksEvery Design Better handbook, in full.
- Early accessAnnual subscribersNew formats and events before they open to everyone.
▶Spec notes
—Two variants of one component, both shipped on designbetter.com: the PITCH band (home page and the foot of archives) and the PERKS band (/join). Same perks grid, different job.
—PITCH band: a fixed dark ground (ink, or a scene image with ink panels) · two columns that stack below about 640px · a pitch column on the left · a 2-column perks grid on the right · a proof row under both columns. The perks are evidence for the pitch.
—PERKS band: a fixed light tint (peach on designbetter.com) · perks only, no pitch column · a 3-column grid · no button · no proof row. It summarises what is included, on a page that has already made the case.
—Pitch column: display heading 36px · lead 18px · body 15px carrying at most one inline secondary link · the button.
—At most one button, and how many depends on the variant: exactly one in the pitch band, none in the perks band. Never two: the pitch band exists to convert, and a second option beside it only gives the reader a cheaper thing to click. Put the secondary path in the body as an inline link.
—The pitch band's heading may change per placement; the treatment may not. The foot of an archive can be blunter ('You're missing the best of Design Better') because a reader who scrolled a page of free work has already shown interest.
—Perks: the painted grid-gap technique · each cell an 18px icon and a title at weight 700, 18px in the 2-column grid and 22px in the 3-column grid · an optional badge on its OWN line · 14px body.
—Icon colour follows the ground: signal orange on the dark pitch band (5.83:1), ink on the light perks band. Signal on peach is 2.10:1, below the 3:1 that a meaningful icon needs, so it never carries the signal colour there.
—A badge beside a variable-length title fits next to some titles and wraps under others, and the cells stop agreeing on where it lives. Inline placement only works when every title in the set is about the same length, so default to its own line.
—Proof row (pitch band only): an eyebrow and a logo row at reduced scale, under both columns rather than inside one, so it reads as footing for the whole claim rather than another item in the list.
—Both grounds are fixed, one dark and one light, so every text colour, rule and hover on either is a literal value. See techniques.fixedGround.
—Replaced a CTA card and feature rows that no property used: a search of seven properties found that card's shape used for alerts, quotes and a price card, never as a call to action.
color.brand.inkcolor.brand.signalfont.displayfont.monospacing.10spacing.18



