Skip to main content
EricRoseDev Design System

EricRoseDev Design System

A CSS-first design system. Colors → Tokens → Primitives → Atoms → Components → Layouts

Colors

palette

The raw palette from src/tokens/. Components don't use these colors directly: the color roles under Tokens map them to what each one is for. docs/palette.js chooses what this section shows.

Typography

type

Fonts, sizes and spacing from src/tokens/type.css, and how plain HTML text looks before any class is added (src/base/reset.css). Sizes follow a 1.25 scale and change with the density mode.

Fonts

Each font token, with its font stack: the first font that's available is the one you see.

Headings

Headings use the display font and tight line height, one step down the scale per level. Use them in order (no skipping from h2 to h4) and one h1 per page; pick a level for the outline, not the size.

Sample headings, levels 1 to 6, shown visually only.

Text sizes and spacing

Body text uses a roomy line height, so long paragraphs stay easy to follow from the end of one line to the start of the next.

--eds-line-height-body

Headings use a tight line height, so wrapped titles hold together

--eds-line-height-tight

Paragraphs of running text should stay within --eds-measure (65 characters wide): long lines make it hard to find the start of the next one. .eds-prose applies it for you, along with spacing between paragraphs, lists and headings. This paragraph is set to the measure, so you can see the width it gives.

max-width: var(--eds-measure)

Inline text

Strong importance, emphasis, a link, inline code, Ctrl + K, highlighted, small print, CSS, H2O and E = mc2, removed and added text.

Use these for what the text means, not how it looks: <strong> for importance, <em> for stress, <mark> for relevance (like search matches). Links stay underlined, so they don't rely on color alone.

Icons

library

Icons come from the system's icon library, chosen in src/icons.css: Font Awesome Pro by default. It's loaded separately from the framework CSS (@ericrosedev/eds/icons), so the bundled CSS never contains licensed icon files. .eds-icon sizes and aligns any icon, and icons follow the size and color of the text around them. To say what an icon means rather than which glyph it is, use an icon role. Mark decorative icons aria-hidden="true", and give an icon-only button or link a text name.

Font Awesome

Font Awesome Pro is loaded by default, with every style and feature it documents at fontawesome.com. A few of its icons:

  • fa-house
  • fa-star
  • fa-heart
  • fa-bell
  • fa-user
  • fa-gear
  • fa-magnifying-glass
  • fa-envelope
  • fa-calendar
  • fa-camera
  • fa-cloud
  • fa-bolt
  • fa-bookmark
  • fa-flag
  • fa-lock
  • fa-paper-plane

Custom SVG

For icons the library doesn't have, like a logo mark, the system keeps an SVG sprite in src/icons.svg (exported as @ericrosedev/eds/icons.svg). Each icon is a <symbol>, drawn with <use> inside an <svg class="eds-icon">, so it sizes and colors itself like the font icons. The sprite belongs to each system, and sync never overwrites it.

  • #icon-spark
  • #icon-wave
  • #icon-target
  • #icon-stack
<svg class="eds-icon" aria-hidden="true">
  <use href="/icons.svg#icon-spark"></use>
</svg>

Page CSS can't select the shapes inside a <use>, but inherited properties reach them. Each symbol draws its main parts in currentColor, which follows color, and leaves its secondary parts unfilled, so they take the fill of the <svg>. .eds-icon sets fill: currentColor, so icons are one color until you set a fill. Set both for two colors, and change them on :hover or :focus-visible like any other style. Hover these:

  • #icon-spark
  • #icon-wave
  • #icon-target
  • #icon-stack
.brand-mark {
  color: var(--eds-color-text);   /* main parts */
  fill: var(--eds-color-accent);  /* secondary parts */
  transition: color var(--eds-duration-fast), fill var(--eds-duration-fast);
}

a:is(:hover, :focus-visible) .brand-mark {
  color: var(--eds-color-accent);
  fill: var(--eds-color-text-muted);
}

New symbols follow the same rules: the viewBox goes on the <symbol>, ids start with icon-, and shapes use currentColor or no fill, never a fixed color. The sprite must be served from the same origin as the page: browsers don't load a <use> from another site.

Swapping the library

src/icons.css belongs to each system, and sync never overwrites it. To use another library, replace its imports with that library's CSS (or remove them, to use only the SVG sprite), update the icon markup, and point the icon role tokens in src/tokens/icons.css at the new library's glyphs. .eds-icon keeps sizing and alignment the same. Font Awesome Pro is an optional peer dependency: each app installs it with its own license, from Font Awesome's registry (see the README).

Tokens

values

Named values only, with no selectors. Re-theming means changing tokens and nothing else.

Color roles

What each color is for. They follow the current theme.

Icon roles

What each icon means. Markup uses the role (.eds-icon--success), and src/tokens/icons.css decides which glyph that is, so a brand changes an icon in one place. Pair a role with any style: fa-solid, fa-regular, fa-light, fa-duotone.

Space

Speeds

How long motion takes, from src/tokens/motion.css. Transition speeds time a change of state, like a hover or a list opening; fades keep them in reduced motion. Animation speeds time one cycle of a looping animation, which only plays when motion is welcome. Easing shapes the movement: the brand's curves are tokens that each point at one from the easing library (Utilities › Easing). The dots below move only when motion is welcome; otherwise they jump.

Primitives

layout

Content-agnostic layout behavior. Strip every color and font and they still do their job.

Golden flexbox .eds-gf

True center (default): the 🎯 stays centered even with unequal sides.

↖️ a much wider left slot
⬆️
↗️
⬅️
🎯
➡️
⬇️ (no left slot)
↘️

.eds-gf--flow: the original space-between feel, where the center floats between the sides.

↖️ a much wider left slot
⬆️
↗️
⬅️
🎯
➡️
↙️
⬇️
↘️

Stack, cluster, grid

stack
stack
stack
cluster
cluster
wraps
as needed
grid
grid
grid
grid

Atoms

one element

The look of a single element. Same structure in every brand; the tokens decide how it looks.

Buttons

Pick a theme (.eds-btn--primary and so on), then a style (--outline), a shape (--icon, for one icon) and a size (--sm, --lg). Icon-only buttons need a name: aria-label on the button. The light and dark outline styles are meant for a background of the opposite tone.

ThemeDefaultOutlineIconSVG IconIcon + TextNo RippleSmallLarge
--primary
--secondary
--success
--danger
--warning
--info
--light
--dark
--link
--bare

With no theme a button is neutral; --ghost drops its border and background until hovered. Disable a button with the disabled attribute; for a link styled as a button, use aria-disabled="true".

Pressing a button sends a ripple out from where you pressed it (from the middle for Enter or Space), from src/js/ripple.js. It's on by default: turn it off for one button with .eds-btn--no-ripple, or for a page or part of one with data-ripple="off". When motion is reduced the ripple doesn't grow; it fades, so the press still shows.

Badges

Badges take the same themes as buttons, in three sizes (.eds-badge--sm, medium by default, --lg). Make one an <a> or a <button> and it becomes clickable, with a hover state, the pointer cursor and the focus ring. With no theme a badge is an outlined pill: Default

ThemeMediumMedium ClickableSmallSmall ClickableLargeLarge Clickable
--primaryPrimaryPrimaryPrimary
--secondarySecondarySecondarySecondary
--successSuccessSuccessSuccess
--dangerDangerDangerDanger
--warningWarningWarningWarning
--infoInfoInfoInfo
--lightLightLightLight
--darkDarkDarkDark
--linkLinkLinkLink
--bareBareBareBare

Form controls

Every control is the native element, styled: it keeps its keyboard handling, form submission and screen reader support. .eds-input covers text, number, text area and select; .eds-input-group adds icons inside a field; .eds-check and .eds-switch draw checkboxes, radio buttons and switches; .eds-choice is their label, a 44px-tall target.

A select whose list is styled too, with icons in its options, in browsers with the customizable select (Chrome, Edge, Safari 27). Elsewhere it's a plain select.

Checkboxes
Radio buttons
Toggle switches

"All channels" is indeterminate (some, not all, are on): a state only script can set, with input.indeterminate = true.

Validation with HTML and CSS only

The rules are HTML attributes: required, type="email" and "url", pattern, minlength, min and max. The browser checks them, and CSS shows the result with :user-invalid and :user-valid, which wait until you've changed a field and left it, or tried to submit. Submitting with a problem focuses the first one and shows every error. Format hints sit above each field, where autocomplete lists and phone keyboards can't cover them.

Enter an email address, like name@example.com.

3 to 16 lowercase letters, numbers or underscores.

Use 3 to 16 lowercase letters, numbers or underscores.

At least 8 characters.

Use at least 8 characters.

Enter an age from 13 to 120.

Starting with https://

Enter a full address, starting with https://

Plan

Choose a plan.

Agree to the terms to continue.

CSS can't tell screen readers that a field is invalid, or read an error out when it appears. That part needs a little script: this page loads src/js/field-validation.js, which keeps aria-invalid and the error's description in step with what you see.

Disabled

Disable one control with the disabled attribute, or a whole group at once with <fieldset disabled>, as here. Disabled controls are skipped by Tab and left out when the form is sent.

Disabled controls

Keys, dividers and skip links

Press ⌘ K to search: .eds-kbd draws a key.


Skip link. Every page starts with .eds-skip-link, which jumps past the header to the main content. It appears in the top corner when you press Tab at the top of the page. Point it at the page's <h1>, marked .eds-skip-target with tabindex="-1":

<a class="eds-skip-link" href="#content">Skip to main content</a>
<header>…</header>
<main><h1 id="content" class="eds-skip-target" tabindex="-1">Page title</h1>…</main>

Components

built from atoms

A field: label + input + hint. Leave it empty or type "nope", then tab away.

Enter a valid email address.

That handle is taken.

Note

The default alert uses the accent tone.

Did you know

Status alerts get a tinted background: the system's status surface role if it has one, otherwise a tint of the status color.

Saved

Your changes are live.

Heads up

Default themes may break contrast; strict mode never does.

Toast

.eds-toast shows a short status message in a corner of the screen, above everything else, from src/js/toast.js. Toasts stack in one "Notifications" region, are announced to screen readers without taking focus, and stay until closed unless you give them a timeout (which pauses while the pointer or focus is on the toast). Use them for news like "Saved"; anything that needs action belongs in an alert or a dialog.

import { toast, setToastPosition } from '@ericrosedev/eds/toast';

toast('Your changes are live.', { title: 'Saved', tone: 'success' });
toast('Link copied', { tone: 'dark', timeout: 5000 });
setToastPosition('top-end'); // bottom-end (default), bottom-start, top-start, top-end

Tooltips

component

A tooltip names or explains a control while it's hovered or focused. Hover these icons, or Tab to them:

Settings
Notifications: 3 unread
Delete

The tip is a popover, so it sits above everything, placed with CSS anchor positioning: above the icon, flipping below or to the side near the edge of the screen. src/js/tooltip.js opens it after a short hover or on keyboard focus, keeps it open while the pointer is on the tip itself, closes it with Escape, and wires it up for screen readers: the settings tip repeats the button's name, so it adds nothing; the notifications tip describes its button; the delete button has no name of its own, so it takes the tip's.

<button class="eds-btn eds-btn--icon" aria-label="Settings" data-tooltip="tip-settings">
  <i class="eds-icon eds-icon--settings fa-solid" aria-hidden="true"></i>
</button>
<div class="eds-tooltip" id="tip-settings" popover="manual">Settings</div>

Use tooltips for hints people can afford to miss. Touch screens have no hover, so a control has to make sense without its tooltip.

Backdrop

component

.eds-backdrop is an animated background built from stackable layers: gradient, shapes, grain and pattern. Try every option in the backdrop playground.

Layouts

page regions

This whole page is .eds-page: a hero above the fold, a sticky section nav beside the content, and a footer. On narrow screens the nav becomes a sticky bar. The header at the top is .eds-site-header, built on the golden flexbox. Below is .eds-prose.

Prose layout

Long-form text gets a readable measure and vertical rhythm, with no classes needed on the inner elements. Inline code and links are styled too.

  • Works on Markdown output
  • Headings get extra space above
A component may only use tokens and lower levels.
@import "@ericrosedev/eds/css";

Utilities

helpers

Single-purpose classes for one-off adjustments. They sit in the top layer, so they win over component styles without !important. Reach for a primitive or component first; use these for the exceptions.

Display

.d-none hides an element from everyone, screen readers included. To hide something visually but keep it readable, use .sr-only.

.d-none .d-block .d-inline .d-inline-block .d-flex .d-inline-flex .d-grid .d-inline-grid .d-contents

.d-flex
.d-none (you won't see this)
One box is hidden with .d-none

Flexbox and gap

Direction, wrapping, growing and alignment for flex (and grid) containers, plus gaps from the space scale. The -reverse directions change what you see but not the Tab or reading order, so use them sparingly.

.flex-row .flex-column .flex-row-reverse .flex-column-reverse .flex-wrap .flex-nowrap .flex-wrap-reverse .flex-fill .flex-grow-0|1 .flex-shrink-0|1 .align-items-start|center|end|baseline|stretch .align-self-auto|start|center|end|baseline|stretch .justify-content-start|center|end|between|around|evenly .gap-0….gap-8

A
B
C
.d-flex .justify-content-between .align-items-center
A
Longer B
C
.d-flex .flex-column .align-items-center .gap-2
.flex-fill
B
.flex-fill
.d-flex .flex-wrap .gap-3

Focus ring offsets

Move an element's keyboard focus ring out (positive) or in (negative). Use a negative offset where an outside ring would be clipped or would overlap a neighbor, like cards in a tight grid or items in a scroller, and check the ring still contrasts with the element's own background. Sizes come from --eds-focus-offset-sm, -md and -lg (2px, 4px, 8px). Press Tab to move through these:

Easing

A library of timing curves, the same in every system. Each class sets the timing function for an element's transitions and animations: in curves start slowly (things leaving), out curves end slowly (things arriving), in-out both. back, bounce and elastic overshoot, so keep them for small, playful moves. Every curve is also a custom property, like --eds-ease-out-cubic; the brand's own easings (.ease-in, .ease-out, .ease-in-out, .ease-out-2, .ease-bounce) point at these and are shown under Tokens › Speeds. The dots move only when motion is welcome.

Scroll reveal

.reveal slides elements in as they scroll into view, in pure CSS. It switches off in reduced motion.

Tray from the left
Slide up
Tray from the right

Role-based color

A small set of color utilities that use the color roles, so they follow dark mode, strict mode and each system's brand. Markup never names a palette step.

.bg-surface
.bg-raised
.bg-sunken
.bg-accent
.bg-info
.bg-success
.bg-warning
.bg-danger

.text-default .text-muted .text-accent .text-info .text-success .text-warning .text-danger

Text

Weight, style, size, line height, font, alignment, wrapping, decoration and case. These change how text looks; for meaning, use the HTML element (<strong> for importance, <em> for stress). Write text in its normal case and use .text-uppercase for the look, since text typed in capitals can be read out letter by letter. There's no justify: its uneven word spacing makes reading harder.

Weight and style

.fw-light

.fw-normal

.fw-medium

.fw-semibold

.fw-bold

.fw-bolder

.fw-lighter

.fst-italic

.fst-normal

Size and line height

.text-xs

.text-sm

.text-md

.text-lg

.text-xl

.text-2xl

.text-3xl

.text-4xl

.lh-1: lines sit right on top of each other, for big single-line text

.lh-tight: for headings and short wrapped titles

.lh-body: roomy, for running text

Font

.font-body

.font-display

.font-mono

Alignment and wrapping

.text-start

.text-center

.text-end

.text-justify spreads each line to fill the full width, so both edges line up. The last line stays at the start. It hyphenates long words to keep the gaps between words smaller, but those gaps still vary, which makes justified text harder for many people to read, so keep it for wide columns and short passages.

.text-nowrap keeps this on one line

.text-truncate cuts long text off with an ellipsis

.text-break: https://example.com/a/very/long/address/that/would/overflow

.text-balance evens out the lengths of these wrapped lines

.text-pretty avoids leaving one lonely word on the last line

.text-wrap lets text wrap normally again

.line-clamp-2 shows two lines of this text at most, then cuts the rest off with an ellipsis so the card stays the same height

.line-clamp-3 allows three lines before the ellipsis. There are classes for one to six lines, or set your own count on .line-clamp with --eds-line-clamp

Decoration and case

.text-decoration-none

.text-decoration-underline

.text-decoration-line-through

.TEXT-LOWERCASE

.text-uppercase

.text-capitalize text

.text-small-caps for Names and Titles

.text-all-small-caps for NASA and HTML