Skip to main content
ENKO

Color

Senco uses 7 color groups — primary, accent, info, neutral, and 3 status colors. Each one maps to a specific UI function, not decoration. All pairings pass WCAG 2.2 AA contrast checks in both light and dark mode.

Color in a transfer app carries load that color in a marketing site does not. A status color is the difference between "your money arrived" and "your money is stuck", and it has to survive a dimmed screen, a colorblind user, and a screenshot sent to support. Every rule below exists because one of those three broke something.

Palette

The base palette provides raw color values organized by function. Each group spans 10 shades from darkest (900) to lightest (50), used to differentiate contrast levels and visual hierarchy.

  • Primary — Dark anchors for headings, text, and UI structure
  • Accent — Brand blue for interactive elements, links, and focus states
  • Info — Softer blue for informational feedback, distinct from brand accent
  • Neutral — Gray scale for text hierarchy, borders, and surfaces
  • Green / Yellow / Red — Status colors for success, warning, and error states

Why info is not accent

Both are blue, and separating them costs a whole palette group. The reason: on a transfer screen, accent blue means you can act on this and info blue means the system is telling you something. When they were the same hue, users tapped informational banners expecting them to do something. Info was pulled toward gray-blue (#3B82B8 at 600) until the two read as different roles at a glance.

Status colors are deliberately muted

Status values are one to two steps softer than their conventional equivalents — green-600 is #22915A rather than a saturated #16A34A, red-600 is #BB4A4A rather than #DC2626.

A transfer app shows status constantly. Every pending transaction, every rate alert, every verification step carries a status color. At full saturation the dashboard reads as an emergency. Muted status colors let a genuine error — a failed transfer, a rejected document — actually stand out, because it is not competing with eleven other colored chips.

All muted values were re-verified against AA after toning down. Softening never came out of the contrast budget.

Two neutral steps are off-ramp

TokenConventionalSencoWhy
--ds-gray-500#6B7280#636971Muted text needed 4.5:1 on gray-100 surfaces, not just white
--ds-gray-400#9CA3AF#727780Placeholder text is text; #9CA3AF fails at 2.54:1

Placeholder contrast is the most common AA failure in shipped design systems, and it is invisible in review because the reviewer knows what the field is for. Fixing it in the palette means no component can reintroduce it.

Palette grid

Primary

primary-900
primary-800
primary-700
primary-600
primary-500
primary-400
primary-300
primary-200
primary-100
primary-50

Neutral / Gray

gray-900
gray-800
gray-700
gray-600
gray-500
gray-400
gray-300
gray-200
gray-100
gray-50
white

Accent / Blue

accent-900
accent-800
accent-700
accent-600
accent-500
accent-400
accent-300
accent-200
accent-100
accent-50

Green

green-900
green-800
green-700
green-600
green-500
green-400
green-300
green-200
green-100
green-50

Yellow

yellow-900
yellow-800
yellow-700
yellow-600
yellow-500
yellow-400
yellow-300
yellow-200
yellow-100
yellow-50

Red

red-900
red-800
red-700
red-600
red-500
red-400
red-300
red-200
red-100
red-50

Info

info-900
info-800
info-700
info-600
info-500
info-400
info-300
info-200
info-100
info-50

Semantic usage

Components reference semantic tokens, not raw palette values. This indirection allows theme switching and ensures consistent intent across the system.

TokenMaps toPurpose
color-primary{accent-600}Brand, primary actions, links
color-primary-hover{accent-700}Primary hover state
color-primary-light{accent-50}Primary tinted backgrounds
color-bg{white}Page background
color-surface{gray-100}Card/section fill
color-fg{gray-700}Body text
color-heading{primary-900}Headings, strong text
color-muted{gray-500}Secondary text, hints, labels
color-border{gray-200}Dividers, input borders
color-link{accent-600}Inline links
color-focus{accent-600}Focus ring

Surface layering model

Senco stacks at most three surface levels. The model is defined here once so that a card inside a sheet inside a dialog does not become an improvised gray.

LevelNameLightDarkUsed for
0Page--ds-bg #FFFFFF#0F172APage background
1Raised--ds-white#1E293BCards, form cards, dialogs, sheets
2Recessed--ds-surface {gray-100}#1E293BInset fills — search bars, code blocks, table headers

Rules:

  • Level 1 is raised by shadow in light mode, by lightness in dark mode. Light mode uses --ds-shadow-xs on white; dark mode cannot use shadow on near-black, so #1E293B sits above #0F172A by value instead.
  • Never stack two level-1 surfaces. A card inside a card means the inner one should be a level-2 inset or a divider.
  • Borders do the work shadows cannot. At level 2 and in dark mode, --ds-border separates surfaces. Elevation alone is not sufficient in dark mode and never in high-contrast mode.
  • --ds-white is a semantic token, not a color. In dark mode it resolves to #1E293B. Anything that means literal white uses #FFFFFF directly — which in practice is only --ds-on-accent.

Feedback colors

Each feedback state uses a set of background, border, text, and icon tokens for consistent signaling:

StateBackgroundBorderTextIcon
Info{info-50}{info-400}{info-800}{info-700}
Success{green-50}{green-400}{green-800}{green-700}
Warning{yellow-50}{yellow-400}{yellow-900}{yellow-700}
Error{red-50}{red-400}{red-800}{red-700}
Tip{accent-50}{accent-400}{accent-800}—

Warning text uses the 900 step where the others use 800. This is a visual weight decision, not a contrast one: yellow-800 (#854D0E) measures 6.46:1 on yellow-50 and passes AA comfortably. At 800 the warning text read lighter than the success and error text beside it in a stacked notice list, so it takes one more step down.

An earlier version of this file justified the 900 step by claiming 800 measured 4.3:1 and failed. It does not. The claim was plausible — yellow is the hue where AA is hardest — which is exactly why it survived unchallenged. Recording the correction here rather than quietly swapping the sentence.

For the full token architecture (base → semantic → state → component → platform → bridge), see Tokens.

Semantic token mapping
Semantic tokenBase tokenResolved
--ds-bg--ds-white
--ds-fg--ds-gray-700
--ds-heading--ds-primary-900
--ds-muted--ds-gray-500
--ds-border--ds-gray-200
--ds-surface--ds-gray-100
--ds-primary--ds-accent-600
--ds-primary-hover--ds-accent-700
--ds-primary-light--ds-accent-50
--ds-success-bg--ds-green-50
--ds-success-border--ds-green-400
--ds-success-text--ds-green-800
--ds-warning-bg--ds-yellow-50
--ds-warning-border--ds-yellow-400
--ds-warning-text--ds-yellow-900
--ds-error-bg--ds-red-50
--ds-error-border--ds-red-400
--ds-error-text--ds-red-800
--ds-info-bg--ds-info-50
--ds-info-border--ds-info-400
--ds-info-text--ds-info-800

Contrast verification

All color pairings are validated against WCAG 2.2 AA requirements: 4.5:1 for text, 3:1 for UI components. Every ratio below is computed from the token value that actually ships, not from a sampled screenshot.

Light mode (verified)

PairingForegroundBackgroundRatioPass
Body text#374151#FFFFFF10.31:1✓
Heading#111827#FFFFFF17.74:1✓
Muted text#636971#FFFFFF5.54:1✓
Muted on surface#636971#F3F4F65.04:1✓
Primary link#0F62FE#FFFFFF5.00:1✓
Placeholder (--ds-placeholder)#727780#FFFFFF4.50:1✓
White on primary btn#FFFFFF#0F62FE5.00:1✓
White on destructive#FFFFFF#BB4A4A5.01:1✓
Info text#1E4D8C#F0F7FF7.80:1✓
Success text#166534#F0FAF36.68:1✓
Warning text#713F12#FDF8EA8.17:1✓
Error text#922525#FDF3F37.67:1✓
Tip text#002D9C#F5F8FF10.65:1✓

For dark mode contrast verification, see Themes.

Non-text contrast

Text is not the only thing that must clear a bar. WCAG 2.1 SC 1.4.11 requires 3:1 for UI components and meaningful graphics.

ElementForegroundBackgroundRatioPass
Input border{gray-200} #E5E7EB#FFFFFF1.24:1✗ — see below
Input border (focused)#0F62FE#FFFFFF5.00:1✓
Focus, bordered control — border#0F62FE#FFFFFF5.00:1✓ — the border is the indicator
Focus, borderless control — outline#0F62FE 3px#FFFFFF5.00:1✓
Focus halo at 20%#CFE0FF#FFFFFF1.33:1— decoration, not the indicator
Status dot, success#22915A#FFFFFF3.99:1✓
Status dot, warning#C07C14#FFFFFF3.42:1✓
Status dot, error#BB4A4A#FFFFFF5.01:1✓
Divider{gray-200}#FFFFFF1.24:1✓ — decorative
Info border{info-400}{info-50}1.71:1✗ — see below
Success border{green-400}{green-50}1.89:1✗ — see below
Warning border{yellow-400}{yellow-50}1.82:1✗ — see below
Error border{red-400}{red-50}2.40:1✗ — see below

The four feedback borders are a known and accepted deviation. SC 1.4.11 governs boundaries that are the only indicator of a component's presence or state. A feedback notice signals itself with a tinted background, a 700-step icon, and a text label, all of which clear their own thresholds — the 400-step border is a refinement on an already-identified surface, not the thing that identifies it. Holding the border to 3:1 would push it to the 600 step, at which point the notice reads as an outlined alert box rather than a tinted one, and the muted-status decision above gets undone. The trade is recorded rather than carried silently.

The resting input border at 1.24:1 is a second known and accepted deviation. SC 1.4.11 applies to controls whose boundary is the only indicator of their presence. Senco inputs always carry a persistent visible label above the field and a 48px filled control area, so the field is identifiable without its border. Darkening the border to gray-400 was tested and made dense forms — the contact address form has nine fields — read as a cage. The deviation is recorded rather than silently carried.

Color is never the only signal

Triple signaling is mandatory for every status in the system: color + icon + text.

StatusColorIconText
Completed{green-600}Check"Completed"
Pending{yellow-600}Clock"Pending"
Failed{red-600}Alert triangle"Failed"
Processing{accent-600}Spinner"Processing"

About 1 in 12 men has a color vision deficiency, and deuteranopia collapses exactly the green/yellow/red axis this app relies on. Under simulation, green-600 and yellow-600 converge to near-identical muddy tones. The icon and the label are what carry the meaning; the color is reinforcement. See Status indicators.

How contrast is verified

Verification is not a one-time audit. Every pairing in these tables is re-checked when a base token changes, because a single base edit cascades through the semantic layer into every component. Changing --ds-accent-600 invalidates the link, focus ring, primary button, and tip token rows at once.

Contrast checker
AaSample text
Contrast
—
AA Normal (4.5:1)
Fail
AA Large (3:1)
Fail
AAA (7:1)
Fail
FG: / BG:

Usage guidelines

  • Always use semantic tokens in components — never reference base palette directly
  • Pair color with icon + text for status indicators (never color alone)
  • Focus: bordered controls turn the border --ds-focus and add a 20% halo; borderless controls take a 3px solid --ds-focus outline with a 2px offset. See Tokens
  • Disabled states rely on both color and a visual cue, never color alone

Do and don't

DoDon't
Use --ds-primary for the one action you want taken on a screenUse brand blue for every clickable thing until nothing is primary
Use --ds-error-* for states the user must resolveUse red for a neutral "remove" affordance that is easily undone
Let a status chip carry icon and label alongside its colorShip a bare colored dot as the only status indicator
Put green on a completed transferPut green on a submitted transfer — money has not moved yet
Use --ds-surface for an inset region inside a cardNest a second white card inside a white card
Reference --ds-muted for helper textReach for gray-400 because the helper text "looks heavy"

The submitted-versus-completed rule is the one that matters most. Green in a money app is read as done, and in cross-border transfer a submitted payment can still fail two days later. Submitted is --ds-info-*; completed is --ds-success-*.

Color on dark surfaces

Fixed-dark surfaces — camera viewfinder, QR scanner — do not follow the theme. They stay #111827 in both modes and use hard-coded light values for their overlays. A viewfinder that inverts in light mode costs the user their frame of reference and gains nothing. This is the one documented exception to semantic theming; it lives in Tokens.

Known gaps

ItemStatus
Resting input border at 1.24:1Accepted deviation, rationale above
High-contrast mode (forced-colors)Not implemented — system colors will override, untested
Chart paletteBridged to --chart-1…5 from existing tokens; not validated for categorical distinguishability
Skeleton tokensNot defined — skeletons currently use --ds-surface directly
Do
CSS
color: var(--ds-heading);
background: var(--ds-surface);

Semantic tokens adapt to themes automatically.

Don't
CSS
color: #111827;
background: #F3F4F6;

Hardcoded hex breaks in dark mode and ignores token updates.

Do
Transfer sent
Your $250.00 CAD is on the way. We'll let you know when it arrives.

Semantic colors with icon + background reinforcement. Success is clear at a glance.

Don't
Transfer sent
Your $250.00 CAD is on the way. We'll let you know when it arrives.

Colored text on white — no background, no icon. Easy to miss and inaccessible.

Do
You send$250.00 CAD
Service fee (1.5%)$3.75
Rate113.38 JPY
Yuki received¥27,731.90 JPY

Heading at 17.7:1, muted at 5.5:1 — both pass AA. Financial data must be readable.

Don't
You send$250.00 CAD
Service fee (1.5%)$3.75
Rate113.38 JPY
Yuki received¥27,731.90 JPY

Light gray text fails contrast. Users can't read their own transfer summary.