Kelas Sekejap — Foundations
The designer-facing guidance behind Say it with confidence v7: the rules for type, space, shape, states, accessibility, motion, iconography, and voice. Tokens named here live in src/styles/tokens.css (and typed in src/tokens/tokens.ts).
Everything carries the four brand rules: named colour blocks · type-first · keep the product calm · organic shapes.
01 · Colour as surface — the ratio rule
Every brand colour is a surface, not just an accent. Sunflower, Pulse, Moss, Spark and Flame are as legitimate as full-screen blocks as they are as badges. Card and ColorBlock both take the whole palette.
Target ratio: 60 / 40. Roughly 60% neutral (Peach canvas, Haze, Paper) to 40% colour, measured across a flow rather than inside a single screen. If a flow reads as 90% neutral, it has drifted — that is the failure this rule exists to catch.
Give every screen one full-colour block. Not a coloured badge on a cream screen — one element that carries the screen. Auth and onboarding are the screens that most often get missed and the first ones anyone sees; a cream screen with one orange button is the anti-pattern.
The energy comes from colour blocking, not decoration. The fix for a flat screen is to make one element go full colour — not to add icons, patterns or doodles. Decoration is a marketing-tier move; in-product, the block does the work. Keep the product calm is still one of the four brand rules.
Onboarding is the one exception, and it is capped at two marks. First-run is the only place in the product where a colour block also carries decoration: a blob bleeding off the top corner plus a flat dot texture opposite it, shipped as <BlockAccent on={tone} />. Four densities were tried on an identical Flame block — none, one, two, and the four-mark density of the reference frames. Two is where the screen still reads as product rather than poster, so two is the ceiling, not a starting point. Everything past auth and onboarding ships bare. This exception buys warmth on the screens a new user sees first; it does not reopen decoration as a fix for a flat screen — that one is still a colour problem.
Colour the loud part, keep the targets Paper. Inside a colour block, interactive elements stay Paper (OptionRow, PillSelect, OtpInput) so the surface can go loud without costing legibility or contrast.
On a phone, the block stays a card. The 2px Ink outline and hard offset shadow are the signature — running a block edge-to-edge (shape="flush") buys margin but throws the silhouette away, so it's for special cases, not the default. If a card feels oversized on a phone, pull in the chrome, not the colour: screen gutter to 14px, block padding to --ks-space-5, shadow to 5px. That recovers ~20px of row width and reads noticeably lighter with the brand intact.
Walk the palette across a flow. Give consecutive steps different surfaces (language select on Flame, code entry on Sunflower, interests on Pulse) so progress is legible by colour and no single screen has to be loud twice.
Rules that fall out of this
| Rule | Why |
|---|---|
| Text pairing is automatic — white only on Blue and Ink | Decision A1. Every other brand colour carries Ink. Components enforce it; don't override it |
Selected states use the opaque --ks-tint-* set |
A translucent tint over a colour block picks up the surface beneath it. --ks-flame-tint / --ks-blue-tint are reserved for error and focus-glow — not selection |
Inside a colour block, focus uses --ks-focus-ring-on-color |
The default Blue ring disappears against a Blue surface and fights Flame and Pulse. Ink reads on every brand surface |
| A filled mark on a light accent is Ink, not white | Non-text UI still needs 3:1. White clears it on Flame (3.89), Moss (3.2) and Blue (5.4), but fails on Sunflower (1.6), Pulse (2.0) and Spark (2.3) |
| Selection always carries a non-colour signal | A filled control or a check, so state survives greyscale and colour-blindness |
| Decoration is two marks max, and only on onboarding | <BlockAccent> is the sanctioned pair. Hand-placing a third mark on a product block is the drift this rule catches |
02 · Typography
Three roles do all the work: a heavy Display for loud moments, a friendly Body for reading, a Mono for labels/tokens/eyebrows. One Display element per view — the same discipline as "one loud primary."
| Role | Family | Usage |
|---|---|---|
| Display | Archivo Black | UPPERCASE, stacked. Tracking −0.065 to −0.085em, leading 0.80–0.86 |
| Body | Plus Jakarta Sans | Sentence case. Runs heavy — 700 floor, 900 for buttons |
| Mono / Label | JetBrains Mono | UPPERCASE eyebrows, tags, tokens. Tracking +6–10% |
Type scale (token · desktop / mobile · line-height):
| Token | Size | Line-height |
|---|---|---|
--ks-text-display-xl |
64 / 40px | .95 |
--ks-text-display-l |
40 / 32px | 1.0 |
--ks-text-heading-l |
28px | 1.1 |
--ks-text-heading-m |
20px | 1.25 |
--ks-text-body-l |
18px | 1.5 |
--ks-text-body-m |
16px | 1.5 |
--ks-text-body-s |
14px | 1.45 |
--ks-text-label |
12px | — (UPPER, mono) |
Rules — One Display element per screen. Never stretch, condense, or outline the Display face. Body line length ≤ 66 characters. Uppercase is Display & Mono only — never uppercase body copy. Minimum body size on mobile is 16px.
03 · Spacing & Layout
A 4px base keeps rhythm predictable. Everything — padding, gaps, insets — snaps to --ks-space-1…10 = 4 · 8 · 12 · 16 · 20 · 24 · 32 · 40 · 48 · 64.
Breakpoints (mobile-first): --ks-bp-sm 480 · --ks-bp-md 768 · --ks-bp-lg 1024 · --ks-bp-xl 1280.
Containers & grid
| Use | Value |
|---|---|
Reading width (--ks-container-reading) |
720px max |
App / page max (--ks-container-app) |
1120px |
| Grid — mobile | 4 col · 16px gutter |
| Grid — desktop | 12 col · 24px gutter |
| Screen edge margin (mobile) | 16–20px |
The primary surface is a phone — design at 375–430px first, respect safe-area insets (notch, home indicator), and keep the primary CTA within thumb reach at the bottom.
04 · Shape & Elevation
The hard, blur-free offset shadow paired with a 2px Ink outline is the single most recognisable thing about the system.
Radii: --ks-radius-sm 14 (inputs) · md 20 (chips) · lg 28 (cards) · xl 36 (hero) · pill 999.
Elevation (always Ink, never grey, never blurred):
| Token | Value | Use |
|---|---|---|
--ks-shadow-sm |
4px 4px 0 | small chips |
--ks-shadow-button |
5px 5px 0 | resting buttons, pills, tags |
--ks-shadow-card |
8px 8px 0 | cards, modals |
--ks-shadow-hover |
7px 7px 0 | hover lift |
--ks-shadow-soft |
0 22px 60px rgba(16,16,20,.14) | marketing / hero docs only — never interactive UI |
The press rule is mandatory: every shadowed element carries the 2px Ink border. On press the element "sits down" — translate(5px, 5px) and drop the shadow to 0. That physical click is the brand's core interaction feel.
Organic shapes — the system explicitly calls for fluid shape language: blobs, swashes, sticker cut-outs (see DoodleAccent). Radii are the floor, not the whole story — reach for organic forms in marketing and hero moments.
06 · Component States
Define the full set for every interactive component — not just rest and disabled.
| State | Signal | Notes |
|---|---|---|
rest |
Fill + 2px Ink + shadow-button | Default |
hover |
Lift −2/−2, shadow-hover | Pointer only |
focus-visible |
Blue ring offset by the canvas colour (--ks-focus-ring) |
Keyboard — never remove |
active / pressed |
Sink +5/+5, shadow 0 | The signature press feel |
loading |
Label → spinner, size held | Block re-tap; announce via aria-live |
selected |
Ink fill or check badge | Toggles, nav, choices |
error / invalid |
Flame border + message + icon | Never colour alone |
disabled |
#DEDBD3 fill, 45% Ink text, 18% border, no shadow |
not-allowed cursor |
Keep the product calm — the loud, high-contrast treatment belongs to marketing and hero moments. Inside the learning UI, dial states down: readable and quiet so practice stays low-pressure. Same tokens, gentler application.
07 · Accessibility
Measured WCAG 2.1 contrast for the real palette. Default to Ink text; only Blue is safe for white body text. (AA body needs 4.5; large/bold ≥24px needs 3.0.)
Ink text on… (recommended default)
| Surface | Ratio | |
|---|---|---|
| Paper | 19.0 | AAA |
| Peach | 16.0 | AAA |
| Haze | 15.9 | AAA |
| Sunflower | 11.8 | AAA |
| Pulse | 9.4 | AAA |
| Spark | 7.8 | AAA |
| Moss | 5.9 | AA |
| Flame | 4.9 | AA |
| Blue | 3.5 | Large text only |
White text on…
| Surface | Ratio | |
|---|---|---|
| Blue | 5.4 | AA |
| Flame | 3.9 | Large only |
| Moss | 3.2 | Large only |
| Spark | 2.4 | ✗ Fail |
| Pulse | 2.0 | ✗ Fail |
| Sunflower | 1.6 | ✗ Fail |
| Peach | 1.2 | ✗ Fail |
| Haze | 1.2 | ✗ Fail |
Do — Ink on any accent. White only on Blue (body), or Flame/Moss at large sizes. Give every state a non-colour signal (icon, label, shape). Focus ring always visible. Touch targets ≥ 44×44px. Support 200% zoom & dynamic type.
Decision A1 (2026-07): Flame and Moss surfaces carry Ink text system-wide (4.88 / 5.94 AA). White icons on them are fine (non-text, ≥3:1), and display-size white (≥24px, e.g. PosterCard headlines) passes AA-large for marketing surfaces.
Decision B2 (2026-07): the v7 board's "Soft" layer — Lavender
#CAA7FF, Mint#B8DDCE, Cream#FFF7ED— is formally retired. In-product calm is expressed with Haze + Paper only; the board should no longer be read as spec for those three colours.
Don't — White text on Sunflower/Pulse/Spark/Haze. Colour as the only meaning (a streak or progress state needs a label). Removing focus outlines. Sub-44px tap areas. Body text below 16px on mobile.
Audio-specific (this app is voice-first) — transcripts/captions for every listening task; announce recording start/stop and results with aria-live; a visible non-audio state for "tap to speak"; never make audio the only way to complete a task; respect prefers-reduced-motion on the mic pulse.
08 · Motion
Physical (matching the tactile shadow) and quick. Purposeful, never decorative. Every looping effect has a reduced-motion fallback.
| Token | Value | Use |
|---|---|---|
--ks-dur-press |
80ms | Press / snap — no ease |
--ks-dur-fast |
150ms | Hover |
--ks-dur-base |
200ms | Most transitions |
--ks-dur-slow |
300ms | Entrances, toasts |
--ks-dur-reward |
500ms | Celebrations |
Easing --ks-ease = cubic-bezier(0.2, 0.8, 0.2, 1). Signatures: press snaps down/up (no ease); toast slides up from bottom and settles; reward pops (overshoot) then settles; card hover lifts −2/−2 into shadow-hover. Recording pulse = ksPulse on the Flame mic while listening.
Reduced motion — under prefers-reduced-motion: reduce, replace movement and loops with a simple opacity change and hold the mic on a static "listening" state.
10 · Iconography
Lucide React only — not custom icons. Lucide's rounded caps and even weight already match the brand.
| Rule | Value |
|---|---|
| Library | lucide-react |
| Default stroke | 2 (reserve 2.4–3 for rare emphasis) |
| Sizes | 16 · 20 · 24 (match text scale) |
| Colour | Ink single-tone; accent only for reward moments |
| Alignment | Optical-centre with adjacent text baseline |
Live set includes mic, house, chart-column, user, check, sparkles, arrow-right, circle-alert. Don't swap in another icon set, mix stroke weights in one view, recolour with low-contrast accents, or draw custom one-offs when a Lucide glyph exists.
The only sanctioned non-Lucide expressive graphic is the branded Expression set — see the emoji-rule exception in the README.
11 · Voice & Tone
The brand speaks in a distinct voice — "No scores — only progress," the Say it / Say more / Say better ladder.
- Loud, but warm — confident and energetic in Display; kind and encouraging in body. Hype the learner, never the grade.
- Progress, not scores — frame everything as practice and growth. "Getting clearer," not "62%." Celebrate attempts.
- Spoken & short — write how Malaysians speak: direct, plain, brief. Read it aloud; if it's a mouthful, cut it.
Core terminology
| Use | Not | Why |
|---|---|---|
| Practice / session | Test / exam | Lowers pressure |
| Streak | Score / rank | Momentum, not judgement |
| Say it · Say more · Say better | Level 1/2/3 | The brand's speaking ladder |
| Getting clearer / Building | Fail / Wrong | Growth framing (see BandBadge) |
| SPM · Form 5 · Paper 3 | (localise, keep proper nouns) | Malaysian exam context |
Do — "We couldn't hear you — try again." · "Your streak is safe for today." · "Practice today to start your streak." Encourage after every attempt.
Don't — "Error: invalid input." · "You scored 3/10." · "Incorrect." Cold, clinical, or score-first language.
Bilingual — natural Malay-English code-switching is welcome in warmth ("Sekejap," playful lines), but keep instructions in clear English.
Sections numbered to match the source foundations pack (02–04, 06–08, 10–11). Colour (01), the component set (05), and the app kit (09) live in the README and src/.