Skip to content
Barua UI v0.2
Foundations

Foundations

Barua's entire look lives in one token layer: an Apple-derived palette tinted around Barua blue, glass materials for depth, and light-dark() pairs that keep every color correct in both themes. Learn the tokens once — every component on the following pages is assembled from them, and re-theming means changing a variable, not a component.

Color System

Every color token is a light-dark() pair, so a single name is always correct in either scheme. The accent family drives buttons, links, focus and selection; re-tint the whole system with data-accent="indigo|purple|pink|teal|green" on <html>.

Accent — Barua blue
--b-color-accent
Accent hover
--b-color-accent-hover
Accent active
--b-color-accent-active
Accent soft wash
--b-color-accent-soft
Accent text
--b-color-accent-text
On accent
--b-color-on-accent

System colors

The Apple system palette, tuned per scheme. Use them for meaning — the semantic aliases --b-color-success, --b-color-warning, --b-color-danger and --b-color-info map to green, orange, red and accent, and each has a translucent *-soft wash for badges and tints.

Red
--b-color-red
Orange
--b-color-orange
Yellow
--b-color-yellow
Green
--b-color-green
Mint
--b-color-mint
Teal
--b-color-teal
Cyan
--b-color-cyan
Indigo
--b-color-indigo
Purple
--b-color-purple
Pink
--b-color-pink

Grays

Apple systemGray 1–6, darkest to lightest in light mode. Reach for grays only when a fill or separator token doesn't fit — most neutral UI should use the translucent fills so it layers correctly.

Gray 1
--b-gray-1
Gray 2
--b-gray-2
Gray 3
--b-gray-3
Gray 4
--b-gray-4
Gray 5
--b-gray-5
Gray 6
--b-gray-6

Backgrounds & surfaces

Depth is a stack: canvas below, surfaces on top, elevated layers for overlays. Cards and fields sit on --b-surface; nest with --b-surface-2 and -3; menus and dialogs use --b-elevated over a --b-scrim.

Canvas
--b-bg
Canvas secondary
--b-bg-secondary
Surface
--b-surface
Surface 2
--b-surface-2
Surface 3
--b-surface-3
Elevated
--b-elevated
Scrim
--b-scrim

Text hierarchy

Four label levels, Apple-style, built from opacity so they read correctly on any surface. Primary for content, secondary for supporting copy, tertiary for hints and placeholders, quaternary for watermarks only — never for information people must read.

Primary
--b-text
Secondary
--b-text-secondary
Tertiary
--b-text-tertiary
Quaternary
--b-text-quaternary

Wallpaper

Glass is a material, and a material only reads as one when there is something behind it. Over a flat page every level of glass collapses into the same grey — the blur has nothing to blur, the specular edge has nothing to catch. Give the wall a picture and the whole system comes alive at once.

So changing the background is not a decoration setting in Barua, it is part of the system: put .b-wall on the surface that carries it — usually <body> — and let the person using the interface choose the picture. It costs nothing until one is set, and the system learns its accent and its light/dark scheme from whatever they choose. Some people will leave the wall bare; the ones who don’t should be rewarded for it.

Glass needs a backdrop
Live

Change the wall and watch the material, the ink and the accent all answer to the picture.

In a product you put .b-wall on <body> and call Barua.wallpaper.set(src) or Barua.wallpaper.setFile(file) — the picture is remembered, resized in the browser, and the system learns from it. The documentation site itself stays bare on purpose: reading surfaces want quiet, application surfaces want a wall.

Adaptive accent & scheme

The system can learn from a picture — a user’s wallpaper. Barua.adapt(imageOrUrl) samples the image and reads two things from it: its most vibrant hue, which re-tints the accent tokens on <html>, and its overall brightness, which sets data-wall-scheme to light or dark. Anything marked .b-wallpaper-scope then resolves every light-dark() token that way — so a dark wallpaper gets light text and dark glass without touching the app’s theme. Barua.adapt.reset() returns to stock. Try it — the whole page follows:

So do badges And links

A wall of its own: the surface below is a .b-wallpaper-scope. Swap its wallpaper and the glass, the ink and the accent inside it all follow the picture — the page around it never changes theme.

On the wall
Live

Glass, ink and accent all read the picture behind them.

Typography

A text style is a whole set of metrics, not a size: every role below states its own size, weight, leading and tracking together. Small text is not body text made smaller — a caption opens its tracking slightly and tightens its leading, which is why inheriting those values from the page would be wrong. Headings of the same size as a role use the same metrics as that role, so <h5> and .b-headline are typographically identical.

SF first. --b-font-sans resolves to SF Pro on Apple hardware and degrades gracefully everywhere else, so the system feels native without shipping a webfont. Weight, tracking and leading are tokens too — pair tight tracking with large sizes, wide tracking with uppercase labels, and leave body text alone.

TokenValueUse it for
--b-font-sansSF Pro Text / Display, system fallbacksAll interface text
--b-font-monoSF Mono, Menlo, ConsolasCode, tokens, technical values
--b-font-roundedSF Pro Rounded, system fallbacksFriendly numerals, badges, empty states
--b-weight-regular400Body copy
--b-weight-medium500Labels, chips, quiet emphasis
--b-weight-semibold600Headlines, buttons, table headers
--b-weight-bold700Titles, stat numerals
--b-tracking-tight-0.022emLarge titles
--b-tracking-text-0.011emBody text (default)
--b-tracking-wide0.06emOverlines, uppercase labels
--b-leading-tight1.2Titles and headings
--b-leading-snug1.35Dense UI, code blocks
--b-leading-body1.5Reading text (default)
--b-leading-loose1.65Long-form articles

Font Scale

The Apple HIG scale, exposed as one class per role. Elements h1h6 map onto it automatically; use the classes when semantics and size need to differ — a card title that is an h3 but should read as a headline, for instance.

.b-large-title
Large Title
--b-text-large-title · 2.125rem / 34px · bold
.b-title1
Title 1
--b-text-title1 · 1.75rem / 28px · bold
.b-title2
Title 2
--b-text-title2 · 1.375rem / 22px · semibold
.b-title3
Title 3
--b-text-title3 · 1.25rem / 20px · semibold
.b-headline
Headline
--b-text-headline · 1.0625rem / 17px · semibold
.b-body
Body
--b-text-body · 1.0625rem / 17px
.b-callout
Callout
--b-text-callout · 1rem / 16px
.b-subheadline
Subheadline
--b-text-subheadline · 0.9375rem / 15px
.b-footnote
Footnote
--b-text-footnote · 0.8125rem / 13px
.b-caption
Caption
--b-text-caption · 0.75rem / 12px
.b-overline
Overline
--b-text-caption · 0.75rem / 12px · uppercase, wide tracking
.b-truncate
One line only — the rest is cut with an ellipsis rather than wrapping
Also .b-line-clamp-2 and .b-line-clamp-3 for a fixed number of lines
.b-text-uppercase
Case as its own concern
With .b-text-lowercase; the overline bundles case with colour and tracking, these do not
.b-tabular-nums
09:41 · 12,847
font-variant-numeric: tabular-nums — equal-width digits for clocks, timers and tables

Spacing

A 4px base scale. Use 1–3 for padding inside controls, 4–6 between related elements, 8–12 between groups, and 16–24 between page sections. If a gap isn't on the scale, it isn't in the system.

--b-space-10.25rem · 4px
--b-space-20.5rem · 8px
--b-space-30.75rem · 12px
--b-space-41rem · 16px
--b-space-51.25rem · 20px
--b-space-61.5rem · 24px
--b-space-82rem · 32px
--b-space-102.5rem · 40px
--b-space-123rem · 48px
--b-space-164rem · 64px
--b-space-205rem · 80px
--b-space-246rem · 96px

Grid

.b-grid is 12 columns by default; set --b-cols or use .b-grid--2/3/4/6 for equal tracks, and span with .b-col-span-*. .b-grid--auto auto-fills against --b-col-min, and .b-card-grid / .b-masonry handle card walls without media queries. Gaps come from the spacing scale via .b-gap-*.

1 / 3
1 / 3
1 / 3
span 6
span 6
span 4
span 4
span 4
span full

Layout

SwiftUI-style primitives: .b-vstack and .b-hstack flow content with a token gap, and .b-spacer absorbs the leftover space — that trio builds most toolbars and headers. .b-container centers a page column, .b-section spaces bands of content, and .b-split + .b-panel make resizable multi-pane layouts.

Inbox
VStack Children flow vertically; the gap comes from --b-gap.
TokenValueUse it for
--b-container-sm40rem · 640pxReading columns, auth forms
--b-container-md48rem · 768pxArticles, settings pages
--b-container-lg64rem · 1024pxDefault .b-container width
--b-container-xl80rem · 1280pxDashboards, data-dense views
--b-sidebar-w16.25rem · 260pxNavigation sidebar
--b-rail-w4.5rem · 72pxCollapsed icon rail
--b-topbar-h3.25rem · 52pxTop bar height, scroll offsets

Breakpoints

Custom properties can't drive @media, so breakpoints are a convention: six shared stops, written by value, always mobile-first with @media (min-width: …). Base styles serve the phone; each stop only adds.

NameMin-widthUse it for
xs480pxLarge phones — bump paddings, keep one column
sm640pxLandscape phones, small tablets — first two-column grids
md768pxTablets — .b-hide-mobile / .b-hide-desktop flip here
lg1024pxLaptops — persistent sidebars, split views
xl1280pxDesktops — right rails, TOCs, inspectors
2xl1536pxWide desktops — widen containers, cap line length

Border Radius

Corners grow with the surface, Apple-style: xs for inline code and checkboxes, sm for chips and kbd, md for buttons, inputs and menus, lg for panels and toolbars, xl for cards and modals, 2xl for sheets, and full for pills and avatars. Never give a small control a bigger radius than the surface it sits on.

--b-radius-xs 4px
--b-radius-sm 6px
--b-radius-md 10px
--b-radius-lg 14px
--b-radius-xl 20px
--b-radius-2xl 28px
--b-radius-full pill

Shadows

Soft, layered, never harsh — each token stacks a contact shadow with a wide ambient one, and --b-shadow-color deepens automatically in dark mode. --b-shadow-accent is a blue glow reserved for the FAB and other floating accent controls. Prefer the elevation tokens below, which pair these with a hairline ring.

--b-shadow-xs
--b-shadow-sm
--b-shadow-md
--b-shadow-lg
--b-shadow-xl
--b-shadow-accent

Elevation

The elevation ladder pairs each shadow with a 1px hairline ring (--b-ring) so edges stay crisp on any background. Use the utilities: 1 for resting cards, 2 for raised cards and glass toolbars, 3 for menus and popovers, 4 for modals and drawers, 5 for the command palette. Level 0 removes elevation for flush surfaces.

.b-elevation-1
.b-elevation-2
.b-elevation-3
.b-elevation-4
.b-elevation-5

Opacity

Three stops, three meanings. hover (0.85) dims media and imagery under the pointer, muted (0.6) de-emphasizes without disabling, and disabled (0.4) is applied automatically by :disabled and .is-disabled — never hand-roll a disabled look.

base 1
--b-opacity-hover 0.85
--b-opacity-muted 0.6
--b-opacity-disabled 0.4

Blur

The blur scale feeds the glass materials. Rather than using raw blur values, reach for the composite filters: --b-glass-light (blur-sm + saturate 1.6), --b-glass (blur-lg + saturate 1.8) and --b-glass-heavy (blur-xl + saturate 2) — the saturation boost is what makes color glow through the pane.

--b-blur-xs 4px
--b-blur-sm 8px
--b-blur-md 16px
--b-blur-lg 24px
--b-blur-xl 40px

Materials

Five glass thicknesses, from a whisper to nearly opaque. Thinner materials show more of what's behind but hold less legible content — use ultrathin and thin for hover surfaces, regular for floating toolbars and glass cards, thick for sheets, and chrome for app bars. .b-material-accent adds a Barua-blue tint. Each class pairs its --b-material-*-bg token with the --b-glass backdrop filter and a hairline border.

Aa
.b-material-ultrathin
Aa
.b-material-thin
Aa
.b-material-regular
Aa
.b-material-thick
Aa
.b-material-chrome
When glass is too expensive. Every material is a translucent fill plus a backdrop-filter, and a blur costs the GPU a pass over everything behind it — the price scales with blur radius and painted area, not with how much markup you wrote. Under prefers-reduced-transparency the material tokens resolve to opaque surfaces and the blurs to none, so every glass surface in the system turns solid at once. The same happens where backdrop-filter is unsupported, because an ultrathin material without its blur is a 22% wash that text cannot survive. Liquid refraction, the most expensive thing here, does not start at all.

Baked glass

A blur is recomputed by the GPU on every frame that changes beneath it. When the thing beneath is the wallpaper, that blur has exactly one possible answer — so it can be worked out once, at the moment the picture is hung, instead of sixty times a second forever.

Barua.wallpaper bakes a 64px-wide copy when you set a wallpaper. Drawn back at cover size that thumbnail is a blur — that is what scaling up does to it — and it weighs about a kilobyte. Both the wall and the copy are fixed, so the copy lands exactly over the sharp original underneath and the panel appears to look through itself. Switch a document over with one attribute:

Live

backdrop-filter blurs the photograph on every frame that changes.

Baked

The same photograph at 64×42, 1,956 bytes, drawn back at cover size.

Left is the real filter, right is a two-kilobyte thumbnail of the same photograph — the original is 617 KB. Scroll the page: both stay pinned to the viewport, which is exactly why the baked copy keeps sitting over the sharp original in a real app. Photograph by Starkvisuals on Pixabay.

Two ways to the same picture
<html data-b-glass="baked">      <!-- no backdrop-filter anywhere -->

Barua.glass.baked(true);          <!-- or at runtime, remembered -->
Barua.glass.baked(false);

There is no backdrop-filter left in baked mode, so no compositor layer per panel and no per-frame pass — what remains is drawing an image. It is the right trade for wallpaper-backed chrome: docks, sidebars, top bars, anything sitting still over the wall.

Where it does not apply. A baked blur only knows the wallpaper. A sheet over a scrolling list, or glass above live content, must blur what is genuinely behind it — keep those on the real filter. That is why the mode is opt-in per document rather than a global swap, and why the CSS is scoped to .b-has-wallpaper: with no picture there is nothing to bake and the system stays as it was.

Backplate

SwiftUI's glassBackgroundEffect: glass put on any one element rather than a component that is glass by nature. .b-backplate is the regular material, a lit top edge, a soft shadow and the system radius — for a floating control, a caption over a picture, anything that needs to sit on something.

Now playing

Sauti Sol — Suzanna

Borders & Dividers

Four weights of edge, all translucent so they sit naturally on any surface: --b-hairline for glass edges and elevation rings, --b-separator for list rows and card borders, --b-border for inputs and controls, and --b-border-strong for their hover state. .b-divider draws separator rules — vertical inside an hstack, labeled for timeline breaks, and inset to align with list content.

Reply
Forward
Archive

Yesterday

Motion

Motion is quick, physical and quiet: five durations, five curves, and nothing animates that doesn't communicate. Small state changes use instant and fast; surfaces that move use slow with the spring curve. Everything collapses to near zero under prefers-reduced-motion — see Accessibility.

instant · 100ms fast · 150ms normal · 250ms slow · 350ms slower · 500ms standard · default out · entrances in · exits spring · sheets bounce · overshoot

Transitions

Components already transition on their tokens — hover these to feel fast · standard on the button tint and normal · out on the card lift.

Hover — 250ms ease-out lift

Animation

Looping animation is reserved for indeterminate progress; keep it small and let it stop the moment the work resolves.

TokenValueUse it for
--b-duration-instant100msHover tints, pressed states
--b-duration-fast150msControl transitions, focus rings, tooltips
--b-duration-normal250msReveals, card lifts, accordions
--b-duration-slow350msDrawers, sheets, large surfaces
--b-duration-slower500msPage-level moves, staged sequences
--b-ease-standardcubic-bezier(0.4, 0, 0.2, 1)Default for property changes
--b-ease-outcubic-bezier(0.22, 1, 0.36, 1)Entrances — fast start, gentle settle
--b-ease-incubic-bezier(0.55, 0, 1, 0.45)Exits and dismissals
--b-ease-springcubic-bezier(0.32, 0.72, 0, 1)Sheets and drawers — Apple slide feel
--b-ease-bouncecubic-bezier(0.34, 1.56, 0.64, 1)Playful overshoot — toggles, badges

Iconography

Icons are inline SVG — no font, no sprite request. Draw on a 20×20 viewBox with a 1.5 stroke, stroke="currentColor" and round caps and joins, so every icon inherits text color and matches the SF Symbols weight. .b-icon sizes the box (--sm 16, default 20, --lg 24, --xl 32) and .b-icon-tile seats an icon on an accent-soft tile for list leaders and feature rows. Decorative icons take aria-hidden="true".

Accessibility

Accessibility is baked into the tokens, not bolted on. The focus ring (--b-focus-ring, a 3.5px accent halo) appears on :focus-visible only — keyboards get it, mouse clicks don't. Tab through this demo to see it.

.b-sr-only keeps text available to screen readers while visually hidden — use it to name icon-only controls when aria-label won't do. .b-skip-link is the first focusable element on every page (press Tab from the top of this one), and .b-touch-target extends small controls to the 44px minimum without changing their look.

Contrast. Text tokens are tuned for WCAG: primary and secondary labels hold 4.5:1 on every surface token in both schemes, and large titles hold 3:1. Tertiary and quaternary labels are decorative — never use them for information people must read, and never place body text directly on an accent wash without checking it.
Reduced motion. A global prefers-reduced-motion rule collapses every animation and transition to 0.01ms — spinners, sheets and card lifts all respect it with no per-component work. Design so that motion is reinforcement, never the only signal.

Dark Mode & Light Mode

The root declares color-scheme: light dark and every color token is a light-dark() pair, so the whole system follows the OS by default. Set data-theme="light|dark" on <html> to force a scheme — the topbar's data-b-theme-toggle button does exactly that and persists the choice, which the inline <head> script restores before first paint to avoid a flash.

Because tokens resolve against the nearest color-scheme, you can force a subtree: the second card below is identical markup inside a style="color-scheme: dark" wrapper.

Light
color-scheme: light
Dark
color-scheme: dark
Test both. Never hardcode a hex where a token exists — a color that looks right in light mode will break in dark. The accent itself shifts (#0a7aff light, #0a84ff dark) to hold contrast on dark surfaces.

Responsive Rules

Mobile first, always: base styles serve the smallest screen, and each of the six breakpoints only adds. Most layouts shouldn't need media queries at all — .b-grid--auto, .b-card-grid and the stack primitives reflow on their own. When you do write one, use min-width at a shared stop.

/* Base styles serve the phone */
.inspector { display: none; }

/* lg · 1024px — laptops gain the inspector */
@media (min-width: 1024px) {
  .inspector { display: block; }
}

On small screens: controls grow to the 44px touch target, hover-only affordances get a visible fallback, .b-hide-mobile and .b-hide-desktop swap chrome at 768px, and .b-safe-area / .b-safe-bottom pad content away from notches and home indicators.

Start from the phone layout and layer @media (min-width: …) enhancements at the six shared stops. Let auto-fill grids and stacks do the reflowing.
Don't write desktop-first max-width overrides, invent bespoke breakpoint values, or hide core content on mobile — reflow it instead.

View Transitions

Two things, on the platform's View Transitions API. Opt in with data-vt on <html> and same-origin navigations cross-fade on the system's motion tokens (180ms out, 220ms in). Off by default, disabled automatically under reduced motion, and inert in browsers without the API.

Page transitions
<html data-vt>   <!-- pages now cross-fade on navigation -->

Shared elements

The second thing is the one that matters: a thumbnail in a list and the hero it opens into are the same object to the person looking at them, so they should be the same object to the browser. Give both data-vt-name="cover-42" and it morphs one into the other — across a navigation, or around a Barua.transition() call within one page. Everything unnamed still cross-fades. A name must be unique on the page; a duplicate is warned about rather than silently mis-morphed.

Open one, then close it. In a browser without the API the panel simply appears, and under reduced motion it never animates.