Theme Engine
createEchoTheme(seed) → { css, values, report }. One OKLCH derivation turns a ~6-value seed into the full semantic-role set. Users touch only the seed; components consume only the roles.
Feel Pick one colour and the whole device relights around it — plastic, glow and screen stay the same material in a new hue.
Preview
One seed in, the whole device relights. Try it live on the theme generator.
Or let the agent fetch it: get_component("theme") on the Echo MCP server.
Installation
This one ships in the echo-ui-core package rather than being copied into your project — npx echo-ui-cli init installs it for you. Import it from echo-ui-core/theme.
At a glance
- Weight
- 9.5 KB (minified and gzipped; React not counted)
Usage
import { createEchoTheme } from "echo-ui-core/theme";
const { css } = createEchoTheme({ accent: "#3D8EF0" });
return <style>{css}</style>;Examples
Default theme
Inject the default device look at :root.
const { css } = createEchoTheme();
// <style>{css}</style> or inject into document headRetheme by seed
One color in, the whole device relights, with nothing of the default left in it.
const { css, values } = createEchoTheme({ accent: "#3D8EF0" });Scoped theme
Scope variables to a class instead of :root.
createEchoTheme({ accent: "#F2674E" }, { selector: ".coral-device" });What contrast corrected
The seed can ask for text no one can read; the theme moves it to AA and says so, in `report`.
const { report } = createEchoTheme({ paper: "#2A2A2A", chassis: "#26222B" });
// report.corrected: ["text (→ 9.04:1)", "text-dim (→ 6.41:1)"]A light device
A pale chassis makes a light device: a light page, caps with their own bevel, screens of paper glass printed in ink, the accent lit at 3:1 with its text and focus ring a step deeper (accent-ink), the same primary cap with a light label (primary-cap-ink), and shadows in the body's own colour. The chassis decides; there's no mode to set.
const { values } = createEchoTheme({ chassis: "#EBEBEB", paper: "#1D1D1D", accent: "#0594F9" });
// values.polarity: "light"Pill keys, crisp boxes
`radius` sets the corners of boxes, panels and fields; `pills` makes buttons and segments pills whatever they are, the way a spec sheet draws pill keys on square-cornered tables. Unset, pills come with radius 1, as they always have.
const { css } = createEchoTheme({ radius: 0.3, pills: true });
// --echo-cap-round: 1, and the panels keep their crisp cornersA drawn device
`render: "drawn"` draws the same parts in the ink instead of molding and lighting them, the way a spec sheet draws a key: every part outlined at one weight (`--echo-line`, `--echo-line-width`), hollow on the body, raised parts standing on a hard ink edge they travel down when pressed. Nothing glows, so what a molded device lights is solid ink; the primary key is the paper key that stands highest, and the accent stays the seed's own colour as the fill for what's on. Every contrast guarantee holds. Molded is the default, and a molded device renders exactly as it did.
const { css } = createEchoTheme({ chassis: "#FBFBFB", paper: "#050505", accent: "#D3F44F", render: "drawn" });
// --echo-drawn: 1; --echo-line-width: 1.5px; --echo-primary-cap-top: #d3f44fWhat's hard to read, past AA
The WCAG 2 ratio overrates bright colour on a dark ground. `report.readability` measures every text role on every ground in APCA, a dark device's cap against its body, and what changes for people with colour blindness: a `fail` is hard to read for anyone, a `note` is under comfortable. Lit text on a dark device is already lifted to read.
const { report } = createEchoTheme({ accent: "#0A33E3", chassis: "#030529" });
// report.readability: [{ kind: "text", level: "note", message: "dim text (#…) reads at APCA Lc 5… on the cap; small text reads well from 60." }, …]When the accent moves
On a molded device every accent is held to one lightness, so a lit cap reads the same whatever the seed, and an acid yellow comes out olive. `report.accent` says what it became. A drawn device keeps the seed's colour as its fill, so nothing is reported.
const { report } = createEchoTheme({ accent: "#F1FF01" });
// report.accent: { from: "#F1FF01", to: "#929b00" }Props
| Prop | Type | Default |
|---|---|---|
accentThe one saturated colour, as a hex: the mark, the on state, the lit rail. | string | "#8B6FE8" |
chassisThe molded plastic every surface, cap and well is derived from. A pale one makes a light device. | string | "#232130" |
paperThe source of every text colour. | string | "#EFEBFA" |
lcdTintThe screens' tint. Derived from the chassis when omitted. | string | |
finishMatte (0) to glossy (1): how brightly lit edges shine and how deep shadows fall. | number | 0.5 |
radiusCrisp (0) to soft (1): the corners of boxes, panels and fields. | number | 0.5 |
densitycompact, default or comfortable: spacing and control heights. A target is never under 24px. | "compact" | "default" | "comfortable" | "default" |
rendermolded or drawn: lit plastic, or the same parts drawn in the ink, outlined and hollow on a hard ink edge. | "molded" | "drawn" | "molded" |
pillsButtons and segments as pills (true) or following the radius (false), whatever the radius. Unset, they're pills at radius 1 only. | boolean | |
fontMachineInstrument labels: caps, panel legends, captions. A font stack. | string | "ui-sans-serif, system-ui, -apple-system, 'Segoe UI', sans-serif" |
fontSansBody and UI prose. A font stack. | string | "ui-sans-serif, system-ui, -apple-system, 'Segoe UI', sans-serif" |
fontMonoData, code, and digits that must be told apart. A font stack. | string | "ui-monospace, SFMono-Regular, 'SF Mono', Menlo, Consolas, monospace" |
fontLcdThe screen face on LCD panels and readouts. A font stack. | string | "ui-monospace, SFMono-Regular, 'SF Mono', Menlo, Consolas, monospace" |
Anti-patterns
- Never hardcode hex in component CSS — consume var(--echo-*) so themes propagate.
- Never write a box-shadow by hand — take a recipe, var(--echo-shadow-well) and the rest, so it follows the key light and the finish.
- Do not read the seed inside components; components see only Layer 3 roles.
- Do not bypass createEchoTheme to hand-write role values — you lose the WCAG-AA contrast guardrail.
Changelog
- Every item needs
echo-ui-core0.3.0 (all components). The drawn device, readability past AA and the design tests live in core, and the components that use them are built against it, so each item now declaresecho-ui-core >=0.3.0 <0.4.0. Update core with the items:npm install echo-ui-core@^0.3.0(or your package manager's), thennpx echo-ui-cli update. An item whose files didn't change is the same code under a new version; the 0.2.0 files stay published. - Comments only, no behaviour change. Comments in the component source used to cite documents, milestones and ticket numbers from Echo's own planning, none of which exist in your project. The citations are gone and the sentences around them stay. Where a comment was only a citation, it now says what it meant. Three development warnings (
HardwareButton'sengraved/captionchecks andStatusDot's missing-label check) also dropped the ticket code at the end of the message.echo-ui diffwill show changes in most items; none of them are to code. - An item's version is now a function of declared data, not of how the changelog is formatted. The version an item ships under is published once and never rewritten, and it used to be inferred from prose: a regex for a bolded "Breaking" run across the whole unreleased section, and attribution by "does a bullet mention this name in backticks". One breaking entry therefore moved every item that changed that cycle, and an entry explaining that "
MenuandSheetare unchanged here" filed itself as a change to Menu and Sheet — which it really had. - The agent brief says Echo has not shipped.
llms.txtand the MCP server's instructions led withnpx echo-ui-cli init, and that name on npm is a reserved stub with no executable — so a model following the brief wrote an install step that fails with npx's own error. The brief now opens with the caveat, and the docs carry it on every surface that shows a command. One switch (CLI_RELEASED) silences all of them at launch, and a test refuses to let one move without the others. Affectsall componentsonly in what the brief says about installing them; no item's props, CSS or rendered semantics change. - Echo's home is
echo-ui.com. The registry, the docs and the packages all move to it fromecho.nawwara.studio. Nothing was published under the old host, so noecho.jsonanywhere points at it and there is nothing to migrate — this is the last release where that is true. - Weights, line heights, letter-spacing, z-index, border widths and icon sizes come from the theme scales in
all components, with the old value as a fallback, so components keep working withecho-ui-core@0.0.1. Strays moved to the nearest step: most by under a pixel; HardwareButton icons are 14/16/20px (were 15/17/19). - Content now reflows into a 320px screen (400% zoom) and survives a doubled browser text size (WCAG 1.4.10, 1.4.4), across
all components.Select's 180px floor,NumberField's readout,SegmentedandStepsas flex items,EmptyState's grid columns, andAvatar's fixed-pixel disc all gave way;Panelcounts its padding inside a width you set (box-sizing: border-box), so a<Panel style={{ width: 220 }}>is now 220px wide overall rather than 260px.