Bruhs Design System — Agent Guide
A framework-agnostic design system. OKLCH fruit-named colors, Monaspace all-mono typography, warm-dark neutrals, elevation by tone and ring (no drop shadows). Works anywhere Tailwind CSS v4 runs (React, Vue, Svelte, Astro, Solid, Leptos, Go templates, Rails, raw HTML), and also works zero-build via a precompiled CSS bundle.
This document is self-contained. Read it once and implement. Do not invent tokens, scale values, or class names — use exactly what's listed here.
1. Adoption
Option A — Tailwind v4 project (preferred)
/* your-app.css */
@import "tailwindcss";
@import "@bruhs/theme/styles/index.css";
Ensure fonts are served at /fonts/* (the CSS references them via relative paths). Copy @bruhs/theme/fonts/* into your public/fonts/ at build time, or link them in via your bundler.
Option B — Zero-build / cross-language (Leptos, Go, Rails, plain HTML)
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@bruhs/theme/dist/bruhs.css">
All utilities are pre-compiled. Fonts load from the CDN too. Drop into any HTML document.
Option C — Copy-paste
Download dist/bruhs.css + the fonts/ directory from the package. Serve them from your own host.
2. Core philosophy
Four non-negotiable rules that define Bruhs:
- All-mono typography. Every pixel of text is a Monaspace variant. No sans-serif, no system fonts.
- Warm-dark neutrals. The canvas is
longan(warm brown-black), not gray or pure black. - No drop shadows. Elevation is expressed by tone step +
inset-ring.shadow-xsthroughshadow-2xldo not exist in Bruhs. - Persimmon is the brand. Reserved for primary actions, focus, and "pay attention" moments. Never use it as ambient chrome.
3. Color vocabulary
Ten named scales. Every scale has 11 steps (50, 100, 200, …, 900, 950) in OKLCH. Use the semantic name, not a hex.
| Token | Role | Tint |
|---|---|---|
lychee |
Errors, destructive actions, critical alerts | Red |
persimmon |
Primary actions, brand, focus, bloom | Orange |
durian |
Warnings, attention highlights | Yellow |
pandan |
Success, live indicators, positive states | Green |
blueberry |
Informational, links, selection | Blue |
mangosteen |
Decorative, secondary accents | Purple |
dragonfruit |
Decorative, tertiary accents | Pink |
rambutan |
Foreground text, UI surfaces | Warm gray |
longan |
Background canvas, panels, cards | Warm brown-black |
bruh-{base,eye,mouth} |
Mascot branding only | Custom |
Utilities
Any Tailwind color utility works with these names:
bg-persimmon-500,text-rambutan-100,border-pandan-400ring-persimmon-500,inset-ring-white/10,outline-persimmon-400fill-lychee-400,stroke-durian-300,decoration-blueberry-500divide-rambutan-100/10,accent-persimmon-500
Opacity modifiers on a color scale (bg-persimmon-500/15) are the primary way to produce soft/muted tints. Do not introduce new shades; use opacity.
Canvas / panel / card tones
| Surface | Background | Ring |
|---|---|---|
| Page canvas | bg-longan-950 |
— |
| Panel / sidebar / nav | bg-longan-900 |
inset-ring inset-ring-white/5 |
| Card | bg-longan-800 |
inset-ring inset-ring-white/10 |
| Floating (popover, menu) | bg-longan-700 |
inset-ring inset-ring-white/15 |
| Modal | bg-longan-800 on bg-longan-950/70 scrim |
inset-ring inset-ring-white/20 |
4. Radius vocabulary
Fruit-named scale. Never use rounded-sm, rounded-md, rounded-lg, rounded-xl, rounded-2xl, rounded-3xl, or rounded-full — those are not Bruhs vocabulary.
| Class | Value | Use for |
|---|---|---|
rounded-seed |
4px | Tags, micro-chips, dense controls |
rounded-grape |
8px | Buttons, inputs, badges |
rounded-lychee |
14px | Cards, panels, sidebar items |
rounded-mango |
22px | Hero tiles, media containers |
rounded-pomelo |
36px | Marketing blocks, pull-quotes |
rounded-orb |
full | Avatars, pills, toggles, dots |
Directional variants work: rounded-l-grape, rounded-tr-lychee, rounded-b-mango, etc.
Concentric nesting
For nested rounded elements, enforce inner = outer − padding:
<div class="rounded-mango p-4 [--radius:var(--radius-mango)] [--padding:--spacing(4)]">
<div class="rounded-[calc(var(--radius)-var(--padding))] p-4">inner</div>
</div>
5. Typography
Every .text-* utility self-sets its Monaspace variant. Do not apply font-* utilities on top of .text-* — the role system is the source of truth.
| Class | Font | Size | Weight | Use for |
|---|---|---|---|---|
text-display-lg |
Krypton | clamp(40…72px) | 300 | Hero headlines |
text-display-md |
Krypton | clamp(32…56px) | 400 | Section heroes |
text-display-sm |
Krypton | clamp(28…44px) | 400 | Page headers |
text-h1 |
Xenon | clamp(24…32px) | 500 | Primary headings |
text-h2 |
Xenon | 24px | 500 | Section headings |
text-h3 |
Xenon | 20px | 500 | Sub-sections |
text-h4 |
Xenon | 18px | 500 | Component headings |
text-body-lg |
Neon | 18px | 400 | Lead paragraphs |
text-body |
Neon | 16px | 400 | Default prose |
text-body-sm |
Neon | 14px | 400 | Dense / secondary prose |
text-label-lg |
Argon | 16px | 500 | Form labels |
text-label |
Argon | 14px | 500 | UI labels, metadata |
text-label-sm |
Argon | 12px | 500 | Captions, eyebrows |
text-code-lg |
Neon | 16px | 400 | Code blocks |
text-code |
Neon | 14px | 400 | Inline code |
text-code-sm |
Neon | 12px | 400 | Small code annotations |
text-button-lg |
Argon | 16px | 500 | Primary action buttons |
text-button |
Argon | 14px | 500 | Standard buttons |
text-button-sm |
Argon | 12px | 500 | Compact buttons, chips |
text-callout |
Radon | 18px | 400 | Pull quotes, asides |
Rules
- Never use
font-bold— usefont-semibold(600) orfont-medium(500). - Never add
leading-*to headings — the role utility already sets line-height. - Add
text-balanceto headings,text-prettyto body paragraphs. - Constrain prose width with
max-w-[65ch]ormax-w-[72ch]on the block. - Add
tabular-numsto any element displaying numbers that change (counters, prices, stats). - Never apply
text-*utilities to inline elements (<span>,<a>,<strong>,<em>,<code>). They inherit from their block-level parent. For inline<code>, apply only color (text-rambutan-200) — the parent'stext-body/text-body-smalready sets the Neon mono font. text-code-*utilities are for block code (<pre>,<div>wrappers). For inline code, omit them.
6. Elevation
No shadow-* utilities. Elevation is a two-lever system:
| Step | Background | Ring |
|---|---|---|
| Canvas | bg-longan-950 |
— |
| Panel | bg-longan-900 |
inset-ring-white/5 |
| Card | bg-longan-800 |
inset-ring-white/10 |
| Floating | bg-longan-700 |
inset-ring-white/15 |
| Modal | bg-longan-800 |
inset-ring-white/20 |
Bloom
One exception: --shadow-bloom is a persimmon glow reserved for focus states, active triggers, and deliberate attention accents.
<button class="[box-shadow:var(--shadow-bloom)]">Focused</button>
Do not introduce your own glows in other colors.
7. Motion
Animation timing is tokenized so transitions feel coordinated across the system. Five durations, four easings — all exposed as CSS custom properties and as duration-* / ease-* Tailwind utilities.
Durations
| Token | Value | Use for |
|---|---|---|
duration-blink |
80ms | Instant feedback — radio select, keyboard nav, typeahead |
duration-fast |
150ms | Hover, button press, small state changes (default) |
duration-standard |
250ms | Expand, fade, slide |
duration-slow |
400ms | Page-level transitions, big panel moves |
duration-bloom |
600ms | Deliberate attention moments, bloom accents |
--default-transition-duration is set to var(--duration-fast), so a bare class="transition" uses 150ms automatically.
Easings
| Token | Curve | Use for |
|---|---|---|
ease-standard |
cubic-bezier(0.2, 0, 0, 1) |
Default for most UI transitions |
ease-entrance |
cubic-bezier(0, 0, 0, 1) |
Content entering view (slide-in, fade-in) |
ease-exit |
cubic-bezier(0.4, 0, 1, 1) |
Content leaving view (slide-out, fade-out) |
ease-bloom |
cubic-bezier(0.34, 1.56, 0.64, 1) |
Playful overshoot — reserved for bloom/attention accents |
Rules
- Bare
transitiongets the right default. Don't reach forduration-150manually. - Pair durations with their intended easings.
duration-standard ease-standardfor expand/fade;duration-bloom ease-bloomonly for brand moments. - Respect
prefers-reduced-motion. Wrap non-essential motion inmotion-safe:variants. - Never use durations > 1000ms except for splash/onboarding moments.
8. Spacing
Tailwind's default 4px grid (--spacing: 0.25rem) is the only spacing rhythm. Every gap, padding, and margin should be a multiple of 4px.
Common patterns
| Context | Value | Why |
|---|---|---|
| Inline gap (button → icon, badge → text) | gap-1.5 (6px) |
Tight without touching |
| Form label → field | gap-2 (8px) |
Tightly bound |
| Form field → field | gap-6 (24px) |
Clear separation without feeling loose |
| Card padding (compact) | p-4 (16px) |
Dense apps |
| Card padding (default) | p-6 (24px) |
Standard |
| Section → section | gap-12 md:gap-16 (48/64px) |
Breathing room on long pages |
| Page padding | p-6 md:p-8 (24/32px) |
Mobile-first |
Rules
- Never use
mt-*/mb-*/ml-*/mr-*/mx-*/my-*between flex or grid children — usegap-*on the parent. - Prefer shorthand over split axes.
p-4notpx-4 py-4; keep them split only when a variant overrides one axis (e.g.p-6 md:px-8). - Use
--spacing(…)for arbitrary spacing values —--padding: --spacing(3)not12pxor0.75rem. - Never use
calc(var(--spacing)*…)— use--spacing(…). - Never use
theme(spacing.…)— use--spacing(…).
9. Responsive breakpoints
Mobile-first. Tailwind's default breakpoints apply: sm: 640px, md: 768px, lg: 1024px, xl: 1280px, 2xl: 1536px.
Layout shifts
| Shift | At breakpoint |
|---|---|
| 1 → 2 column | md: (768px) |
| 2 → 3 column | lg: (1024px) |
| 3 → 4 column | xl: (1280px) |
| Off-canvas nav → in-flow sidebar | lg: (1024px) |
| Page padding 24px → 32px | md: (768px) |
| Section gap 48px → 64px | md: (768px) |
Typography
- Display and headings use
clamp()internally — don't addmd:text-*overrides to them. The role utilities handle fluid scaling. - Body text is fixed at its declared size. If a context needs
text-body-smat desktop density, bump it to mobile-base viamax-sm:text-base/6.
Rules
- Always
min-h-dvh, nevermin-h-screen(deprecated). - Touch targets stay ≥44×44px regardless of breakpoint. Use the absolute overlay pattern for small icon buttons.
- Reconfigure dividers per breakpoint when column count changes — see §10.19.
- Checkboxes, radios, toggles are bigger on mobile (
size-5 sm:size-4,w-11 sm:w-9) for finger-friendliness.
10. Component patterns
All patterns below are copy-paste ready. Adapt to your framework's template syntax — the classes do not change.
10.1 Button — primary
<button type="button" class="inline-flex items-center gap-1.5 rounded-grape bg-persimmon-500 px-3 py-2 text-button text-longan-950 ring-1 ring-persimmon-500 hover:bg-persimmon-400 hover:ring-persimmon-400 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-persimmon-400 active:bg-persimmon-600 transition">
Save
</button>
Rules: only one primary per page/dialog. Ring matches fill color exactly — never use reduced-opacity rings on a solid button.
10.2 Button — soft (secondary default)
<button type="button" class="inline-flex items-center gap-1.5 rounded-grape bg-persimmon-500/15 px-3 py-2 text-button text-persimmon-200 inset-ring inset-ring-persimmon-400/20 hover:bg-persimmon-500/25 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-persimmon-400 transition">
Cancel
</button>
10.3 Button — outline
<button type="button" class="inline-flex items-center gap-1.5 rounded-grape bg-transparent px-3 py-2 text-button text-rambutan-100 inset-ring inset-ring-rambutan-100/15 hover:bg-longan-800 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-persimmon-400 transition">
Details
</button>
10.4 Button — ghost
<button type="button" class="inline-flex items-center gap-1.5 rounded-grape px-3 py-2 text-button text-rambutan-200 hover:bg-longan-800 hover:text-rambutan-100 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-persimmon-400 transition">
Skip
</button>
10.5 Button — destructive
<button type="button" class="inline-flex items-center gap-1.5 rounded-grape bg-lychee-600 px-3 py-2 text-button text-lychee-50 ring-1 ring-lychee-600 hover:bg-lychee-500 hover:ring-lychee-500 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-lychee-400 transition">
Delete
</button>
Use the soft style for destructive actions unless the action is the primary purpose of the page/dialog.
10.6 Button sizes
Default (36px): 28px): px-3 py-2 text-button.
Small (px-2.5 py-1.5 text-button-sm.
Use at most two sizes per surface.
10.7 Button with icon
Asymmetric padding — icon side padding equals vertical padding.
<!-- leading icon -->
<button type="button" class="inline-flex items-center gap-1.5 rounded-grape bg-persimmon-500 py-2 pr-3 pl-2 text-button text-longan-950 ring-1 ring-persimmon-500 hover:bg-persimmon-400 transition">
<svg viewBox="0 0 16 16" fill="currentColor" class="size-4 shrink-0"><!-- icon --></svg>
New item
</button>
<!-- trailing icon -->
<button type="button" class="inline-flex items-center gap-1.5 rounded-grape py-2 pr-2 pl-3 text-button text-rambutan-100 inset-ring inset-ring-rambutan-100/15 hover:bg-longan-800 transition">
Continue
<svg viewBox="0 0 16 16" fill="currentColor" class="size-4 shrink-0"><!-- icon --></svg>
</button>
10.8 Badge — soft
<span class="inline-flex items-center rounded-grape bg-pandan-500/15 text-pandan-200 px-2 py-1 text-label-sm inset-ring inset-ring-pandan-400/20">
Live
</span>
Pattern: any accent tone with -500/15 bg, -200 text, -400/20 ring. Works for lychee, persimmon, durian, pandan, blueberry, mangosteen, dragonfruit, rambutan.
10.9 Badge — outline
<span class="inline-flex items-center rounded-grape px-2 py-1 text-label-sm text-persimmon-300 inset-ring inset-ring-persimmon-400/30">
Draft
</span>
10.10 Badge with icon
<span class="inline-flex items-center gap-1 rounded-grape bg-pandan-500/15 text-pandan-200 inset-ring inset-ring-pandan-400/20 py-1 pr-2 pl-1 text-label-sm">
<svg viewBox="0 0 16 16" fill="currentColor" class="size-3.5 shrink-0"><!-- icon --></svg>
Live
</span>
10.11 Text input
<div class="flex flex-col gap-2">
<label for="email" class="text-label text-rambutan-200">Email</label>
<input
id="email"
name="email"
type="email"
placeholder="you@bruhs.dev"
class="block w-full rounded-grape bg-white/5 px-3 py-2 text-body text-rambutan-100 placeholder:text-rambutan-500 inset-ring inset-ring-white/10 focus-visible:outline-2 -outline-offset-1 focus-visible:outline-persimmon-400 max-sm:text-base/6"
/>
</div>
10.12 Textarea
Same classes as text input, on <textarea rows="3">.
10.13 Select
<span class="inline-grid grid-cols-[1fr_--spacing(8)] rounded-grape bg-white/5 inset-ring inset-ring-white/10 focus-within:outline-2 focus-within:-outline-offset-1 focus-within:outline-persimmon-400">
<select
id="fruit"
name="fruit"
class="col-span-full row-start-1 appearance-none bg-transparent px-3 py-2 pr-8 text-body text-rambutan-100 focus:outline-hidden"
>
<option>Lychee</option>
<option>Persimmon</option>
<option>Durian</option>
</select>
<svg viewBox="0 0 8 5" width="8" height="5" fill="none" class="pointer-events-none col-start-2 row-start-1 place-self-center text-rambutan-400">
<path d="M.5.5 4 4 7.5.5" stroke="currentcolor" />
</svg>
</span>
For error state: swap wrapper to bg-lychee-500/5 inset-ring-lychee-400/40 focus-within:outline-lychee-400.
For disabled: add disabled to <select> and opacity-60 bg-white/[0.02] inset-ring-white/5 on the wrapper.
For a leading icon: use grid-cols-[--spacing(9)_1fr_--spacing(8)] and place the icon at col-start-1 row-start-1.
10.14 Checkbox
Supports three states: unchecked, checked, indeterminate. The indeterminate state is a DOM property — set input.indeterminate = true in JavaScript (it is not an HTML attribute).
<div class="flex items-center gap-2 text-body text-rambutan-200">
<span class="h-lh items-center inline-flex">
<span class="group inline-grid size-5 sm:size-4 grid-cols-1">
<input
id="agree"
name="agree"
type="checkbox"
class="col-start-1 row-start-1 appearance-none rounded-seed border border-white/10 bg-white/5 checked:border-persimmon-500 checked:bg-persimmon-500 indeterminate:border-persimmon-500 indeterminate:bg-persimmon-500 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-persimmon-400 disabled:border-white/5 disabled:bg-white/10 disabled:checked:bg-white/10 forced-colors:appearance-auto"
/>
<svg viewBox="0 0 14 14" fill="none" class="pointer-events-none col-start-1 row-start-1 size-7/8 self-center justify-self-center stroke-longan-950 group-has-disabled:stroke-white/25">
<path d="M3 8L6 11L11 3.5" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="group-not-has-checked:opacity-0" />
<path d="M3 7H11" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="group-not-has-indeterminate:opacity-0" />
</svg>
</span>
</span>
<label for="agree">I agree</label>
</div>
10.15 Radio
Identical structure to checkbox but with:
type="radio"on the inputrounded-orb(notrounded-seed)- A filled dot indicator:
<span class="pointer-events-none col-start-1 row-start-1 size-[round(down,40%,1px)] self-center justify-self-center rounded-orb bg-longan-950 group-not-has-checked:opacity-0"></span>
10.16 Toggle
<div class="group relative inline-flex w-11 sm:w-9 shrink-0 rounded-orb bg-white/5 p-0.5 inset-ring inset-ring-white/10 outline-persimmon-500 outline-offset-2 has-checked:bg-persimmon-500 has-focus-visible:outline-2 transition-colors duration-200 ease-in-out">
<span class="aspect-square w-1/2 rounded-orb bg-white ring-1 ring-white/10 transition-transform duration-200 ease-in-out group-has-checked:translate-x-full"></span>
<input id="enabled" name="enabled" type="checkbox" aria-label="Enable" class="absolute inset-0 size-full appearance-none focus:outline-hidden" />
</div>
10.17 Card
<div class="rounded-lychee bg-longan-800 p-6 inset-ring inset-ring-white/10">
<h3 class="text-h3 text-rambutan-100 mb-2">Card title</h3>
<p class="text-body text-rambutan-300">Card body.</p>
</div>
10.18 Well (recessed panel)
<div class="rounded-lychee bg-longan-900/60 p-6 inset-ring inset-ring-white/5">
<p class="text-body text-rambutan-200">Nested or secondary content.</p>
</div>
10.19 Divided grid
<ul role="list" class="grid grid-cols-3">
<li class="p-6"><!-- first item: no left border --></li>
<li class="p-6 [&:not(:nth-child(3n+1))]:border-l [&:not(:nth-child(3n+1))]:border-rambutan-100/10"></li>
<li class="p-6 [&:not(:nth-child(3n+1))]:border-l [&:not(:nth-child(3n+1))]:border-rambutan-100/10"></li>
</ul>
Reconfigure divider patterns when column count changes at a breakpoint.
11. Icons
Use Heroicons as the default set.
Never scale icons. The viewBox size determines the Tailwind size class, exactly:
| Set | viewBox | Class |
|---|---|---|
| Heroicons Micro | 0 0 16 16 |
size-4 |
| Heroicons Mini | 0 0 20 20 |
size-5 |
| Heroicons Outline | 0 0 24 24 |
size-6 |
- App UIs (dashboards, settings, forms): Micro only.
- Navigation lists, marketing: Mini.
- Hero illustrations: Outline.
Rules
- Always add
shrink-0to icons inside flex/grid containers. - Align icons with adjacent text using
h-lh. - Use
fill-*for filled icons andstroke-*for stroked icons. Never usetext-*withcurrentColor(legacy hack). - Never wrap icons in decorative containers (colored squares, circle backgrounds).
Icon-only buttons
Add a hidden 48×48 touch target to meet mobile tap requirements:
<button type="button" class="relative rounded-grape p-2 text-rambutan-300 hover:text-rambutan-100 hover:bg-longan-800 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-persimmon-400 transition">
<svg viewBox="0 0 16 16" fill="currentColor" class="size-4"><!-- icon --></svg>
<span class="absolute top-1/2 left-1/2 size-[max(100%,3rem)] -translate-1/2 pointer-fine:hidden" aria-hidden="true"></span>
</button>
12. Global document setup
Put these on the root elements of every Bruhs-powered document:
<html lang="en" class="scheme-only-dark antialiased">
<body class="bg-longan-950 text-rambutan-100 font-neon">
<main class="isolate min-h-dvh">
<!-- app -->
</main>
</body>
</html>
scheme-only-dark— makes native UI (scrollbars, form controls) render dark.antialiased— required for Monaspace to render correctly.isolateon the main container — prevents z-index conflicts with portalled elements.min-h-dvh— modern viewport unit, avoidsmin-h-screen(deprecated).
13. Accessibility checklist
- Every form control has a
nameattribute and either a<label>(associated viafor/id) or anaria-label. - Every
<button>has an explicittypeattribute ("submit"inside forms,"button"otherwise). - All
<ul>and<ol>in flex/grid layouts getrole="list"(Safari strips the implicit role whenlist-style: noneis applied). - Touch targets are at least 44×44px. Small icon buttons use the absolute overlay pattern above.
- Focus rings are always visible on interactive elements:
focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-persimmon-400. - On
<input>and<textarea>, use-outline-offset-1so the focus ring insets rather than spilling outside. - Form controls must never conjoin an input with a button into a shared border — leave a gap or nest the button visually.
14. Hard bans (never do these)
- Never use
shadow-xs,shadow-sm,shadow-md,shadow-lg,shadow-xl,shadow-2xl. Bruhs has no drop-shadow system. Use tone-step +inset-ringinstead. - Never use
rounded-sm,rounded-md,rounded-lg,rounded-xl,rounded-2xl,rounded-3xl,rounded-full. Use the fruit names. - Never use pure
#000,bg-black, or gray/slate/zinc/neutral scales. Uselongan-950or therambutanneutral for foreground. - Never override the typography role with
font-*utilities. Let.text-*classes declare the font. - Never use
font-bold. Usefont-semiboldorfont-medium. - Never scale an icon. Match viewBox size to size class.
- Never use
text-xsfor body copy. Minimum body size istext-smand only abovesm:breakpoints; mobile default istext-base. - Never wrap an icon in a colored square or circle.
- Never use reduced-opacity rings on a solid primary button. The ring must match the fill color.
- Never place more than one filled primary button on a single page/dialog.
15. Opacity conventions
When you need a softer tint of a color, use opacity modifiers on the scale:
| Pattern | Use for |
|---|---|
-500/15 |
Soft backgrounds (badges, tinted panels) |
-400/20 |
Soft ring pairing for -500/15 backgrounds |
-400/30 |
Outline-style ring on transparent background |
white/5, white/10, white/15, white/20 |
Elevation rings on dark surfaces |
longan-950/70 |
Modal / drawer scrim |
16. Focus & state pattern reference
- Focus (keyboard):
focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-persimmon-400 - Focus inside input:
focus-visible:outline-2 -outline-offset-1 focus-visible:outline-persimmon-400 - Hover (elevated surfaces): bump the
inset-ringopacity by 5 (white/5→white/10) and optionally tone-step the background. - Active / pressed: darker fill (
bg-persimmon-600for primary) oractive:scale-[0.98]for subtle feedback. - Disabled:
opacity-60,cursor-not-allowedon the element,disabled:variant on interactive children.
17. Writing / copy voice
Not every design system documents voice, but Bruhs is opinionated:
- Short. Complete sentences beat fragments when it matters; use fragments for UI labels.
- Lowercase button labels are allowed when the surrounding voice supports it. Don't mix cases inconsistently within one page.
- Numbers use
tabular-numsand a thousands separator (1,284not1284).
18. When in doubt
- Prefer whitespace over borders, borders over wells, wells over cards. Escalate only when the content demands it.
- If a pattern isn't documented here, compose it from the primitives above rather than inventing new tokens.
- Bruhs is a vocabulary, not a component library. Reach for the classes, not imported components.
Bruhs is a personal design system by bnle.me. This guide is the canonical adoption reference — if something here conflicts with older documentation, this wins.