ERP Design System

One set of parts. The coder assembles from this file and never invents a component — if something is missing it gets designed here first, then used. Every rule below answers a question that has a wrong answer.

The shape doctrine — round or square

Radius follows function, not tasteThe single rule that stops a page from looking assembled by three people
8pxIf the cursor enters it, it is 8px. Inputs, selects, textareas, buttons, segmented controls, steppers, date fields. These are containers for content, and content is rectangular. A pill button next to a square input is the mismatch you spotted.
999pxIf it only reports, it is a pill. Tags, status badges, counts, avatars, the switch track, progress bars. These are not containers, they are labels on data. They may be round because nothing has to line up inside them.
10pxCards and panels are 10px — 2px above the controls inside them, so a control never sits flush against a corner it matches.
14pxAnything that floats is 14px. Modals, drawers, popovers, menus, toasts. Off the page plane means a softer corner.
6px6px is for parts inside a control — segmented thumb, menu item, kbd. Never used on its own on the page.
borderOne border colour, one weight: 1px #e7e9ee. Depth on the page plane is a hairline, never a shadow. Read-only and disabled fields drop the border entirely and take a fill instead — that is how you tell them apart without adding a second border style.

Colour

Ink ladder--ink → --ink-4Four steps. A fifth grey is always someone avoiding a decision.
ink#12161f
ink-2#4a5261
ink-3#818a99
ink-4#aab1bd
Surface & brandBrand indigo is for one primary action, links, and active state. Nothing decorative.
canvas#f5f6f8
white#ffffff
fill#f4f5f7
line#e7e9ee
brand#4f46e5
brand-fill#eef0fe
SemanticEach state is one ink + one fill. Used on status, alerts, and ledger numbers — always the same meaning.
ok · available#15803d
warn · reserved#b45309
bad#b91c1c
info#0369a1

Type

ScaleInterSeven sizes. Numbers are always tabular so columns align.
--t-26 / 640Page title
--t-20 / 600Section heading
--t-16 / 600Card title, modal title
--t-14 / 400Body text and default UI copy
--t-13 / 400Control text, table cell, dense UI
--t-12 / 500Field label, hint, meta
.mlabelMicro label above data
--mono084165293047 · 2 008416 529304 4

Space & radius

4pt scale--s1 … --s8Only these values. No 15px, no 18px.
4
8
12
16
20
24
32
48
Radius ladderLeft to right: inner part, control, card, overlay, data label.
6 · inner
8 · control
10 · card
14 · overlay
999 · label
Control heights--h-sm / --h / --h-lgThree. Everything in one row must share one height.

Elevation

Three levels--line / --e1 / --e2On the page plane, depth is a hairline. Shadow means the thing is actually floating.
0 · hairline
1 · raised
2 · floating

Buttons

Variants.btn.primary / .secondary / .ghost / .dangerOne primary per screen. Everything else is secondary or ghost.
Primary
Secondary
Ghost
Danger
Danger quiet

With icon
Trailing icon
Icon only
Loading
Disabled
Dense

Inputs & fields

Field.field > .lbl + .ctl + .hint|.errLabel above, always. Placeholder never repeats the label.
UPC must be 12 digits
Check digit is calculated for you

$
in
Stepper

oud amber

Choice controls

Which one to use2–4 exclusive options in a row: segmented. More: select. Independent options: checkbox. Instant on/off: switch.
Segmented
Segmented · dense
Checkbox
Radio
Switch

Tags & status

The pill family.tag / .status / .count / .avatarThese report. They never accept typing, so they are round.
Tag
oudambercoreSKU 93047
Removable
oud 100 ml
Status
In stockReservedLateIn transitDraft
Count
123
Avatar
MA
Kbd
K

Alerts, hints & tooltips

Alert.alert.info/.ok/.warn/.badPage-level or card-level. One per view; if there are two problems, say both in one alert.
3 shipments need attention. Two are past ETA and one is missing a customs document.
Product created. SKU 93047 is live in the catalogue.
GTIN-14 codes are generated from the UPC and the units per carton.
Could not save. UPC 084165293047 already belongs to Nocturne EDP 50 ml.

HintLeave blank to inherit the brand default.
Inline errorWeight is required
Tooltip
Cube is length × width × height, in cubic feet.
Master cube ?

Loading

Pick by durationUnder 1s: nothing. 1–3s: spinner in place. Over 3s or a known shape: skeleton. Known percentage: bar.
Spinner
Progress
Indeterminate
Skeleton row
Button

Table

Data table.table-shell > table.tblNumbers right and tabular. Row hover, selected row, uppercase micro headers, no zebra.
4
ShipmentStatusDestination Units Arrives
SHP-2291LateToronto DC12,4006 days ago
SHP-2288In transitNewark DC8,160in 4 days
SHP-2284DeliveredMiami DC3,048Jul 22
SHP-2280Draft0
4 of 128

No shipments match these filters

Clear the status filter, or create the first shipment for this route.

Overlays

Menu, modal, toast--e2 + --r-overEverything that floats shares one shadow and one 14px corner.
Dropdown menu
Modal
Toast
Product createdSKU 93047 · Nocturne EDP 100 ml
Export failedTry again in a moment

Patterns — parts assembled

Form rulesNine rules. Follow them and two people building two different forms produce the same-looking screen.
widthA field's width states how long the answer is, never how much room is left. Money 2 columns, a dimension 2, a date or a select 3, a code 4, a name 5, a title 8, notes 12. A field stretched across a row is the system telling the user to type a paragraph into a price.
rowA row is a group, not a leftover. Length / Width / Height / Weight belong on one row because they are one thought. Two unrelated fields never share a row just because the space was free — ending a row early is correct, not unfinished.
controlSegmented is for switching a view, not for answering a question. List vs Cards is a segmented. Market US vs Canada is data, so it is a select — or radios if there are three or fewer and seeing all of them matters. Boolean saved with the form is a checkbox; boolean that takes effect the instant you touch it is a switch.
stretchOnly text inputs fill their column. Segmented controls, steppers, switches and buttons are as wide as their content — the field never stretches them. This was the bug in v1.
labelLabel above, sentence case, no colon. Mark optional, never required — most fields are required, so marking those is noise on every line. Placeholders show format, never repeat the label.
helpHint lives under the field permanently; the error replaces it. Never both at once, never a hint that appears only after failure.
derivedAnything the system calculates is read-only and grouped. Generated codes, cubes and totals go in one block, not scattered between fields the user types into. Read-only means no border and a fill, so it never reads as an empty input.
sectionOne card per section, about six fields before you split. The card head carries the section name and, at most, the one value that identifies the record.
actionsOne action bar for the whole form, right aligned, primary last, sticky when the form is taller than the screen. Never a save button per section.
Before / afterthe two things you caughtLeft is v1. Right is the rule applied.
Wrong — stretched, and a toggle for data
Right — sized to the answer, and a select
Wrong — container wider than its options
Right — hugs its options
Form card.form > .card + .form-footEvery rule above, applied. Nothing here is a new component.

Identity

SKU 93047
Check digit is calculated for you
$
$

Unit

Measured on the sellable bottle
in
in
in
lb

Packaging

in
in
in
lb
GTIN-14 master2 008416 529304 4
Master cube2.7 ft³
Pallet50 cases
All required fields complete
Stat trioThe ERP's ledger idiom, rebuilt from tokens: micro label, tabular figure, semantic ink.
Available
39,696
Reserved
8,410
Total
48,106