echo.json
echo.json is the one file that says how a project uses Echo: where the components go, which
registry they come from, the seed its theme is made from, the style it wears, and what the check
and Echo's tests do. init writes it, and this is what it writes:
{
"$schema": "https://echo-ui.com/e/schema/config.json",
"dir": "src/components/echo",
"framework": "next",
"packageManager": "pnpm",
"registry": "https://echo-ui.com/e"
}Everything else is added by a command (init --seed, style use, style set) or by you (check
and test). Nothing past these is needed. The file points at its own schema, so an
editor completes each field and says what it is, in the same words as this page.
It's plain JSON, so a trailing comma or a comment makes it unreadable. Every command, the hook
and the Stop gate included, then stops and says which file and where it stopped reading, and
nothing is written over it, init --yes included. echo-lock.json is held the same way.
Every field
| Field | Takes | Written by | What it is |
|---|---|---|---|
$schema | string | init | This file's schema. |
dir (required) | string | init (--dir) | The one folder every Echo item is copied into, relative to the project root. |
framework (required) | "next" | "vite" | "unknown" | init, as it detected it | What the project is built with, as init detected it. |
packageManager (required) | "npm" | "pnpm" | "bun" | "yarn" | init, as it detected it | What installs the packages Echo's items need. |
registry (required) | a URL | init (--registry) | Where items come from: Echo's /e/ registry, or a copy of it. |
seed | an object | init --seed, style use, style set | The seed the theme and DESIGN.md are made from, as createEchoTheme takes it. init --seed writes it; Echo's defaults when absent. |
seed.accent | string | The one saturated colour, as a hex. | |
seed.chassis | string | The plastic the device is molded from, as a hex. | |
seed.paper | string | The colour everything is written in, as a hex. | |
seed.lcdTint | string | The screens' tint, as a hex; worked out from the chassis when absent. | |
seed.finish | 0 to 1 | Matte (0) to glossy (1). | |
seed.radius | 0 to 1 | Crisp (0) to soft (1). | |
seed.density | "compact" | "default" | "comfortable" | How tightly parts are packed. | |
seed.pills | boolean | Buttons and segments as pills (true) or following the radius (false), whatever the radius. | |
seed.render | "molded" | "drawn" | Molded and lit (plastic under the key light), or drawn in the ink (outlined, hollow, standing on a hard ink edge). | |
seed.fontMachine | string | Labels, caps and captions: a font stack. | |
seed.fontSans | string | Body text: a font stack. | |
seed.fontMono | string | Data and code: a font stack. | |
seed.fontLcd | string | The screens' face: a font stack. | |
style | an object | style use and init --style; style set adds to changed | The style this project wears, as npx echo-ui-cli style use applied it: what DESIGN.md tells the agent. Its colours, fonts and shape are the seed. |
style.name | string | ||
style.title | string | ||
style.essence | string | One line: what the style is. | |
style.from | "echo" | "yours" | Echo's library, or your own (your Documents' styles folder). | |
style.rules | a list of string | Short lines for the agent: how screens in this style are put together. | |
style.refusals | a list of { wants, instead } | What the style wanted that Echo's grammar refuses, and the closest Echo gets. | |
style.fonts | a list of { role, family, source, url } | The fonts the style uses, and where to get them. | |
style.changed | a list of "accent" | "chassis" | "paper" | "lcdTint" | "finish" | "radius" | "density" | "render" | "pills" | "fontMachine" | "fontSans" | "fontMono" | "fontLcd" | The settings npx echo-ui-cli style set has moved since the style was worn: the project wears the style with these changed. style use clears it. | |
check | an object | You | The team's own additions to npx echo-ui-cli check. Nothing here is needed; it only adds. |
check.rules | a rule name → "off" | "info" | Turn a rule off, or to info: said, never counted. axe is every axe rule test runs, and axe:<id> one of them (axe:target-size). Problems a project already has are kept rule by rule, "axe:target-size": "info", so new ones still count; "axe": "info" hides those too. | |
check.ban | a list of { import, message } | Imports the team doesn't use, each with what to use instead. | |
check.colours | a list of string | Brand colours the seed doesn't produce (a logo's colour), allowed where the check would flag a colour written by hand. | |
test | an object | You | What npx echo-ui-cli test and the Stop gate run Echo's tests on (the design tests, contrast, names and axe). Nothing here is needed: without it they find the app's routes and the port its dev script runs on. |
test.url | a URL | Where the app runs, http://localhost:3000. | |
test.routes | a list of string | The pages to test, as paths (/, /pricing): for a page with a [slug], name one. | |
test.widths | a list of 240 to 3840 | Widths to test each page at, in pixels. Default [1280]. | |
test.stop | boolean | false turns the Stop gate off: Claude Code no longer runs Echo's tests when the agent stops. test still runs. |
seed is what Theming describes, and style is what
style use writes. Move a setting with npx echo-ui-cli style set,
which checks every value first and writes echo-theme.css and DESIGN.md again; alone, it lists
where each setting is.
What you add
Your team's rules, in check. Turn a rule off, or to info (said, never counted); ban an
import, in your own words; allow brand colours the seed doesn't make:
{
"check": {
"rules": { "space": "info", "axe:target-size": "info" },
"ban": [{ "import": "lucide-react", "message": "our icons are in @/icons." }],
"colours": ["#FF5A1F"]
}
}Where Echo's tests run, in test: the app's address, the pages, the widths, and whether
Claude Code's Stop gate runs them:
{
"test": { "url": "http://localhost:3000", "routes": ["/", "/pricing"], "widths": [390, 1280], "stop": true }
}Without test, npx echo-ui-cli test finds the app on the port its dev script runs on and tests
every route it has.
Rule names
These are the names check.rules takes. A name that isn't one of them stops the check and says so,
rather than quietly turning nothing off.
| Rule | Looks at | What it finds |
|---|---|---|
colour | Your code and a running page | A colour written by hand, or painted on the page, that the seed doesn't make. The theme's colours at any opacity, and check.colours, are allowed. |
space | Your code | Spacing off Echo's scale, or a step of it written as a number. Page layout past 32px is yours. |
radius | Your code | A corner off Echo's radius scale. |
shadow | Your code | A shadow or focus ring drawn by hand instead of a --echo-shadow-* recipe. |
timing | Your code | A duration or easing invented instead of one of Echo's motion tokens. |
motion-library | Your code | A second animation library imported beside Echo. |
restyle | Your code | Echo restyled from outside: a class or style on a closed component, your CSS reaching into Echo's classes, a theme token set anywhere but the theme. |
label | Your code | A field with no label. |
error-colour | Your code | aria-invalid with no words tied to it: an error said only by colour. |
motion | Your code | GSAP used straight in a file that uses Echo but not its motion, or an animation with no reduced-motion fallback. |
screen | Your code and a running page | Echo's screen rules: another library's parts on an Echo screen, two primary caps in one decision, something destructive on one press, a confirmation that says "OK"; on the page, "you are here" lit twice. |
weight | Your code | A heavy part (WebGL, or a lot of code) loaded with the page instead of lazily. |
lookalike | Your code | A raw element where Echo has the component: <button>, <select>, <input type="checkbox">, role="switch". |
echo-class | Your code | One of Echo's classes borrowed onto a raw element. |
prop | Your code | A prop an Echo component doesn't take, a value it doesn't have, or one it needs and didn't get, read from the version you installed. |
name | Your code and a running page | A control with no accessible name, which a screen reader can only call "button" or "textbox". |
keyboard | Your code | A click on something a keyboard can't reach, like onClick on a div. |
img-alt | Your code | An <img> with no alt. |
focus-ring | Your code | The focus ring taken away with nothing in its place. |
tabindex | Your code | A positive tabIndex, which takes a part out of the reading order. |
hidden-focusable | Your code | aria-hidden on something the keyboard still lands on. |
team | Your code | An import the team banned in check.ban, with the team's own words for what to use. |
setup | A running page | The theme, the page's ground or SoundProvider missing from the page. |
contrast | A running page | Text under WCAG AA against what's actually behind it, gradients included; small text that passes AA but reads under APCA Lc 45. |
alignment | A running page | Text cut off with "…", cards side by side whose actions sit at different heights, a lone action off its column's edge, a grid that leaves columns empty. |
depth | A running page | A control that glows at rest, or a raised surface on a raised one with no well between. |
corners | A running page | Parts of one kind with different corners, or a corner inside a corner that doesn't follow it. |
icons | A running page | One group's icons at different sizes or stroke weights, or one filled among outlined ones. |
slop | Your code | What reads as made by AI: gradient text, an emoji as an icon, the sparkle for "AI", labels like "Get started", stand-in names and photos. |
axe | A running page, by axe | axe-core's WCAG 2.0, 2.1 and 2.2 A and AA rules. axe:<id> names one of them (axe:target-size). |
axe:<id> takes one of the rules axe runs, by axe-core's own ID: axe:target-size,
axe:image-alt. Keeping the problems a project already has, rule by rule, means new ones still
count; "axe": "info" would hide those too. Checks and tests has which rule runs
where.