-
Notifications
You must be signed in to change notification settings - Fork 1
Design System
This page is the reference for openrunic's visual language: the palette with its real token names, the type scale, the rules that govern status colour, and the accessibility constraints. It is for anyone writing CSS in this repository.
The implementation is @openrunic/ui. Every token below exists as a CSS custom property; nothing here is aspirational styling.
The interface is warm paper. Depth is expressed by stepping through three paper tones before any shadow is considered, and colour is used sparingly enough that when something is coloured, it means something.
The working ratio is roughly 60 percent bone, 25 percent espresso, 10 percent caramel and hazelnut, and 5 percent terracotta. If a screen feels busy, remove caramel first.
| Token | Value | Role |
|---|---|---|
--bone |
#F5EFE6 |
Primary background, application and marketing |
--cream |
#EDE4D3 |
Raised surfaces, cards, alternating sections |
--white |
#FFFFFF |
Input fields and data tables only |
--espresso |
#2E211A |
Primary ink: text, logo, navigation, footer. Never pure black |
--espresso-2 |
#3E2C23 |
Secondary ink, dark surface hover |
--hazelnut |
#8C5E3C |
Secondary text on bone, icons, strong borders |
--caramel |
#A9714B |
Decorative only: large headings, rules, illustration |
--terracotta |
#C4552D |
Accent: calls to action, links, active states, focus ring |
--terracotta-deep |
#A8431F |
Accent hover and pressed; small text links on bone |
--olive |
#6B7A4F |
Success and in-range. A reserved semantic colour |
--line |
#E2D7C3 |
Hairline borders and dividers on bone |
--danger |
#B3362B |
Warm-shifted red. Use sparingly |
Components read these, not the base tokens.
Surfaces --bg-page --surface-card --surface-field --surface-data
--surface-inverse --surface-inverse-hover --surface-wash
Ink --text-ink --text-heading --text-secondary --text-decorative
--text-inverse --text-inverse-secondary --text-link --text-link-hover
Action --action-primary --action-primary-hover --action-primary-ink
--action-accent --focus-ring
Borders --border-hairline --border-strong --border-inverse
Status --status-success --status-success-wash
--status-neutral --status-neutral-wash
--status-danger --status-danger-wash
Data viz --viz-1 .. --viz-6 categorical, warm-shifted
--viz-seq-0 .. --viz-seq-5 sequential, bone to espresso
One naming note. The design source calls body ink --text-body. The library renames it --text-ink, because typography.css is imported after colors.css and owns --text-body as a length. A colour of the same name would lose the cascade and resolve to 16px, which is invalid for a colour property. The rename is documented in the token file.
Three families, all self-hosted and never loaded from a CDN, which is a stated privacy decision:
--font-display Bricolage Grotesque, then system sans
--font-text Bricolage Grotesque, then system sans
--font-editorial Fraunces, then Georgia
--font-mono Spline Sans Mono, then system mono
All are variable fonts. The display family carries a weight, optical-size, and width axis; the editorial family adds softness and wonk axes; the mono family carries weight.
Sizes
| Token | Value |
|---|---|
--text-hero |
clamp(44px, 6vw, 72px) |
--text-h1 |
40px |
--text-h2 |
30px |
--text-h3 |
22px |
--text-body-lg |
18px |
--text-body |
16px |
--text-small |
14px |
--text-overline |
13px |
--text-caption |
12.5px |
Line heights --lh-hero: 1.05, --lh-h1: 1.1, --lh-h2: 1.15, --lh-h3: 1.25, --lh-body: 1.6, --lh-small: 1.5, --lh-caption: 1.4.
Weights --wght-regular: 400, --wght-medium: 500, --wght-semibold: 600. There is no bold weight in the scale.
Tracking --track-tight: -0.02em, --track-normal: 0, --track-caption: 0.01em, --track-overline: 0.14em.
Optical size is set per role rather than left to the browser: --opsz-hero: 96, --opsz-heading: 48, --opsz-h3: 24, --opsz-ui: 12.
Role classes. Use these rather than restyling elements: .or-hero, .or-h1, .or-h2, .or-h3, .or-body-lg, .or-body, .or-small, .or-caption, .or-overline, .or-editorial, .or-mono. The mono role sets tabular figures, which matters for money columns and lab values.
Casing. Sentence case everywhere. The overline is the only permitted all-caps.
Spacing is a 4 px scale: --space-1 at 4 px through --space-10 at 128 px, with 24, 32, 48, 64, and 96 in between. Layout tokens: --content-max: 1120px, a 12-column grid with a 24 px gutter, --section-y and --hero-y at 96 px, --card-pad at 24 px, control heights of 32, 40, and 48 px, and a 1 px hairline.
Radius is four values: --radius-sm: 6px for tags and chips, --radius-md: 10px for cards and fields, --radius-lg: 16px for modals, and --radius-pill: 999px. --radius-button aliases the pill, which makes button shape a single global switch.
Elevation is deliberately shallow. Depth comes from stepping bone to cream to white before any shadow. There are exactly two shadows, both espresso-tinted rather than grey-black, plus a scrim:
--shadow-raised: 0 1px 2px rgba(46,33,26,.06), 0 2px 8px rgba(46,33,26,.05);
--shadow-overlay: 0 4px 12px rgba(46,33,26,.10), 0 12px 40px rgba(46,33,26,.12);
--scrim-espresso: rgba(46,33,26,.82);At most one shadow layer per screen.
Motion is one easing curve and three durations: --ease-out: cubic-bezier(.2,.7,.2,1), with 150, 200, and 250 ms. The doctrine is opacity plus a translate of at most 6 px. No spring, no bounce, no parallax, no scroll-jacking. Reduced motion is honoured globally by collapsing durations to 0.01 ms.
One signature animation exists: the brand glyph draws its six strokes 60 ms apart, used as a loading affordance.
This is the part most likely to be got wrong, so the rules are absolute.
Three status tiers, and they are reserved.
| Meaning | Colour | Wash |
|---|---|---|
| Success, in range |
--olive #6B7A4F
|
#E7E9DC |
| Neutral, informational |
--hazelnut #8C5E3C
|
#EFE3D5 |
| Danger, out of range, destructive |
--danger #B3362B
|
#F6DFD9 |
Olive is a semantic colour, not a palette colour. Do not decorate with it.
The brand accent is not a status colour. Terracotta is the only saturated colour in the system and is scoped to calls to action, links, focus rings, and active navigation. A destructive action uses the danger variant, never terracotta. The button component says so in its own prop documentation.
Never colour alone. Every status carries a label or an icon. The token file itself carries this as a comment above the status block. For numbers the rule is stronger: a value is always paired with a unit and a worded range state, for example 7.4 mmol/L, above range. Never a bare number, never colour as the only signal.
This is enforced structurally in the library rather than left to discipline. Badge attaches a per-tone icon automatically. VitalStat drops its whole state row when no state label is supplied, rather than rendering a coloured chip with no words.
Data visualisation desaturates and warm-shifts into the family. Categorical series use --viz-1 through --viz-6; sequential ramps run bone to caramel to espresso through --viz-seq-0 to --viz-seq-5. Never a charting library's default palette.
The web app needed a fourth, caution tier and defined one locally with color-mix over caramel, flagged in a comment as a proposed library addition rather than quietly forked. That is the correct way to handle a gap: use it locally, name it as a gap.
Contrast, verified. These ratios are measured and recorded, not estimated:
| Pair | Ratio |
|---|---|
| espresso on bone | 13.62 |
| espresso on cream | 12.34 |
| bone on espresso | 13.62 |
| terracotta-deep on bone | 5.27 |
| hazelnut on bone | 4.86 |
| white on terracotta-deep | 6.02 |
| white on terracotta | 4.48 |
| bone on olive | 4.06 |
| terracotta on bone | 3.92 |
Three rules follow directly:
- Hazelnut is the only secondary text colour that passes AA for body text.
- Caramel is decorative only. It measures 3.57 on bone and fails AA for body text. Headings at 24 px and above, rules, and illustration only.
- Small text links must use terracotta-deep, not terracotta. Terracotta at 3.92 qualifies for large text and interface components on bone; small text needs the deep variant at 5.27.
Focus is a 2 px terracotta ring at 2 px offset on every focusable element, always through :focus-visible so keyboard users get it and mouse users do not.
Interaction states. Hover always darkens, never lightens. Press darkens further. Nothing scales or bounces. Disabled is 0.42 opacity with no colour change, so the control keeps its identity.
Touch targets are at least 44 px in each dimension below medium widths, implemented across every interactive component in the library.
Reduced motion is honoured globally.
These are non-negotiable and rejecting a pull request for them is expected:
- No gradients anywhere.
- No frosted glass or backdrop blur.
- No noise, grain, mesh, or aurora effects.
- No emoji or decorative unicode. Icons come from the
Iconcomponent. - No runic, Norse, or Viking motifs, despite the name.
- No red cross, green cross, caduceus, or Rod of Asclepius.
- The logo never sits on photography without a solid panel or a scrim of at least 80 percent.
- The wordmark ships as outlines. Do not retype it.
openrunic is an open-source operating system for human health. Pre-alpha: do not run it in production, and never put real patient data into it.
Repository · Licence (AGPL-3.0-only) · Security policy · Contributing · Code of conduct
Where this wiki and the repository disagree, the repository is right.