Project card
The whole card is one link target. Hover it.
A CSS-first design system. Colors → Tokens → Primitives → Atoms → Components → Layouts
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.
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.
Each font token, with its font stack: the first font that's available is the one you see.
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.
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)
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 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 Pro is loaded by default, with every style and feature it documents at fontawesome.com. A few of its icons:
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.
<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:
.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.
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).
Named values only, with no selectors. Re-theming means changing tokens and nothing else.
What each color is for. They follow the current theme.
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.
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.
Content-agnostic layout behavior. Strip every color and font and they still do their job.
.eds-gfTrue center (default): the 🎯 stays centered even with unequal sides.
.eds-gf--flow: the original space-between feel, where the center floats between the sides.
The look of a single element. Same structure in every brand; the tokens decide how it looks.
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.
| Theme | Default | Outline | Icon | SVG Icon | Icon + Text | No Ripple | Small | Large |
|---|---|---|---|---|---|---|---|---|
--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 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
| Theme | Medium | Medium Clickable | Small | Small Clickable | Large | Large Clickable |
|---|---|---|---|---|---|---|
--primary | Primary | Primary | Primary | |||
--secondary | Secondary | Secondary | Secondary | |||
--success | Success | Success | Success | |||
--danger | Danger | Danger | Danger | |||
--warning | Warning | Warning | Warning | |||
--info | Info | Info | Info | |||
--light | Light | Light | Light | |||
--dark | Dark | Dark | Dark | |||
--link | Link | Link | Link | |||
--bare |
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.
"All channels" is indeterminate (some, not all, are on): a state only script can set, with input.indeterminate = true.
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.
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.
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.
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>
The whole card is one link target. Hover it.
The default alert uses the accent tone.
Status alerts get a tinted background: the system's status surface role if it has one, otherwise a tint of the status color.
Your changes are live.
Default themes may break contrast; strict mode never does.
Something went wrong.
.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
A tooltip names or explains a control while it's hovered or focused. Hover these icons, or Tab to them:
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.
.eds-backdrop is an animated background built from stackable layers: gradient, shapes, grain and pattern. Try every option in the backdrop playground.
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.
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.
A component may only use tokens and lower levels.
@import "@ericrosedev/eds/css";
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.
.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
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
.d-flex .justify-content-between .align-items-center
.d-flex .flex-column .align-items-center .gap-2
.d-flex .flex-wrap .gap-3
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:
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.
.reveal slides elements in as they scroll into view, in pure CSS. It switches off in reduced motion.
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.
.text-default .text-muted .text-accent .text-info .text-success .text-warning .text-danger
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.
.fw-light
.fw-normal
.fw-medium
.fw-semibold
.fw-bold
.fw-bolder
.fw-lighter
.fst-italic
.fst-normal
.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-body
.font-display
.font-mono
.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
.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