One system, every product.
Tokens, components and rules for the company site, Qatee, the CSA Toolkit, BOQ Builder and whatever comes next. Change a theme attribute and the whole screen re-skins; change a token and every product follows on its next build.
Getting started
Put data-theme on <html>, add data-density="compact" for tools and refined pages, and load one stylesheet. Use semantic tokens (--bg, --ink, --brand…) only, never raw hex.
<html lang="en" data-theme="bunyan" data-density="compact">
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Cairo:wght@400;500;600;700;800&display=swap">
<link rel="stylesheet" href="https://ds.bunyanlabs.com/v1/bunyan.css">
<button class="bl-btn bl-btn--primary">Save record</button>
Linking ds.bunyanlabs.com is fine for prototypes. Production apps copy the files in at build time (next tab) so they never depend on another server.
# Dockerfile of any project: copy a pinned design-system release in
FROM bunyanlabs/design-system:1 AS ds
FROM nginx:stable-alpine-slim
COPY --from=ds /usr/share/nginx/html/v1/ /usr/share/nginx/html/ds/
# pages then load /ds/bunyan.css and /ds/assets/*.svg
// main.tsx - CSS once, at the root
import './ds/bunyan.css' // copied from v1/ (or fetched in CI)
document.documentElement.dataset.theme = 'bunyan'
// tokens in code (charts, canvas, emails)
import tokens from './ds/tokens.mjs'
const teal = tokens.themes.bunyan.brand // "#0D7369"
// lib/theme/bunyan_tokens.dart is generated - copy v1/bunyan_tokens.dart
import 'theme/bunyan_tokens.dart';
final p = BunyanTokens.qatee; // or bunyan, bunyanDark, qateeDark
ThemeData(
colorScheme: ColorScheme.light(primary: p.brand, onPrimary: p.onBrand,
secondary: p.accent, onSecondary: p.onAccent, surface: p.surface,
onSurface: p.ink, error: p.danger),
scaffoldBackgroundColor: p.bg,
fontFamily: BunyanTokens.fontSans, // Cairo (google_fonts or bundled)
);
# reports, PDFs, charts
import json, urllib.request
tokens = json.load(urllib.request.urlopen("https://ds.bunyanlabs.com/v1/tokens.json"))
brand = tokens["themes"]["bunyan"]["brand"]
v1/bunyan.cssEverything below in one filev1/tokens.cssThemes, densities, type, space, radiusv1/base.css · layout.css · components.cssLoad separately if you need only partv1/tokens.json · tokens.mjsResolved values per themev1/bunyan_tokens.dartFlutter palette and sizesv1/assets/Logos (light and dark grounds), markColour
Semantic tokens for the current theme. Every text pair is checked at build time and the build fails below 4.5:1. Numbers show contrast against --bg.
Bunyan palette
Teal and gold, each with one job. Teal is the brand: the mark, links, focus, live states. Gold is the action: the main call to action, with Deep Teal text. Champagne is the detail: eyebrows, numerals, fine rules and emphasis in headlines. Deep Teal grounds the dark theme; Sand warms illustrations; Burgundy (برجندي) is held in reserve for illustration and charts.
Type
Cairo for Arabic and Latin alike, so both languages share one voice. Headlines ExtraBold; Arabic gets taller lines automatically under lang="ar".
Space & shape
--radius-sm badges--radius-md controls--radius-lg cards--radius-pill chipsBorders, not shadows. No gradients. Controls are --control-h tall: 44px comfortable, 36px compact, and 44px on touch screens in either density.
Buttons
Name the result ("Save record", not "Submit"). Gold accent for the one call to action on marketing pages; teal primary for everyday actions in product UI. Never both in one group.
<button class="bl-btn bl-btn--primary">Save record</button>
<button class="bl-btn bl-btn--secondary bl-btn--sm">Export</button>
Badges & chips
<span class="bl-badge bl-badge--success bl-badge--dot">Live</span>
<button class="bl-chip" aria-pressed="true">ECC-2:2024</button>
Forms
<div class="bl-field">
<label for="price">Unit price</label>
<input id="price" aria-describedby="price-help">
<span class="bl-help" id="price-help">In SAR, before VAT.</span>
</div>
<label class="bl-switch"><input type="checkbox" role="switch"> Offline mode</label>
Cards & panels
Qatee
Herd management for sheep, goats and cattle.
Tinted card
A quiet block inside a page section.
Linked card
The whole card is the link; the edge turns brand on hover.
<div class="bl-cells" style="--min:160px">
<div class="bl-stat"><span class="bl-stat__value">74%</span><span class="bl-stat__label">Compliant</span></div>
</div>
Data
| Item | Vendor | Status | Qty | Total (SAR) |
|---|---|---|---|---|
| Next-gen firewall | Fortinet | Approved | 2 | 184,000 |
| EDR licences | CrowdStrike | Draft | 2,500 | 612,500 |
| SIEM onboarding | Splunk | Over budget | 1 | 390,000 |
- Status
- Live
- Users
- Solution architects, consultancies
- Stack
- Standalone HTML · offline
Alerts & modal
<dialog class="bl-modal"> … </dialog> // dialog.showModal()
Layout
Compose pages from primitives instead of writing spacing per page. Each takes CSS variables for its knobs.
.bl-containerMax width --container, 24px gutters (16px on phones).bl-sectionVertical rhythm --section-y; adjacent sections get a hairline.bl-section-headEyebrow and title beside a short intro.bl-stack · .bl-clusterVertical / wrapping horizontal, --gap.bl-gridAuto-fit columns, --min column width.bl-splitTwo columns, --a / --b ratios, stacks below 860px.bl-topbar · .bl-footerPage chromeRules
Theme by product
Company site, decks, CSA Toolkit, BOQ Builder: bunyan or bunyan-dark. Qatee app and marketing: qatee or qatee-dark.
Colour
Teal is the brand, Gold the action, Champagne the detail. One accent button per view; status always carries a word, never hue alone.
Arabic first
Set lang="ar" dir="rtl"; fonts and line heights switch automatically. Consumer Arabic is dialect: Egyptian or Saudi.
Voice
Concise, concrete, calm. Sentence case, no emoji, no exclamation marks in product UI. Errors say what to fix.
Focus & touch
Every control shows a 2px focus ring. Touch targets stay 44px even in compact density.
Changing a token
Edit tokens/tokens.json, run the build. It fails if any text pair drops below 4.5:1.