Skip to content
Barua UI v0.2
Patterns

Interaction

The state language every Barua component speaks — hover, pressed, selected, dragging — plus the drag & drop, selection and keyboard patterns built on top of it. Learn the grammar once and every component reads the same way.

State Conventions

Barua uses one small vocabulary everywhere. Native pseudo-classes do the work when the browser can (:hover, :active, :disabled, :focus-visible); a matching is-* class exists for every state your JavaScript needs to drive itself.

StateClass / pseudoVisual treatment
Hover:hoverFill deepens one step; pointer-only, never fires on touch
Pressed:active / .is-pressedScales to 0.97 over --b-duration-instant
Current / toggled on.is-activeAccent-soft background, accent text
Selected.is-selectedAccent-soft background; selectables add an inset accent ring
Dragging.is-draggingOpacity drops to 0.55, cursor becomes grabbing
Disabled:disabled / .is-disabledOpacity --b-opacity-disabled (0.4), not-allowed cursor
Focused (keyboard):focus-visibleAccent glow from --b-focus-ring

The same button, hard-wired into each state. Hover and focus can only be shown live — mouse over the first one, or Tab to it.

Keep the state grammar consistent across components. A selected chip, table row, tree node and card all read as accent-soft plus a second cue — if you invent a new selected look for one component, people have to relearn the language.
Don't remove focus outlines. outline: none without a replacement strands keyboard users. If the default ring clashes, restyle it through --b-focus-ring — never delete it.

Hover State

Hover is an invitation, not information: the surface tint deepens one step (--b-fill-quaternary--b-fill-tertiary) and interactive cards lift 2px. Treat it as enhancement only — touch screens never fire it, so nothing may exist solely behind a hover.

Cards

Weekly digest
Hover me — I lift and gain elevation
Team inbox
.b-card--interactive opts a card in

List items

  • Amara Okafor
    Re: Q3 launch checklist
  • Kelvin Mbwilo
    Design tokens are merged

Table rows

CampaignStatusSent
August newsletterDelivered12,408
Product updateSending3,112
Onboarding dripDraft

Pressed State

Buttons compress to scale: 0.97 on :active — a 100ms (--b-duration-instant) squeeze that makes every press feel physical, the way Apple's controls do. When state is driven from JavaScript instead of the pointer — keyboard activation, pointer capture on a custom control — apply .is-pressed to get the identical physics.

Touch Targets

Anything pressable needs a 44px hit area on touch — the --b-touch-target token. Controls at --b-control-h-lg and up qualify on their own; for smaller visuals (an .b-btn--xs icon button, a drag handle) add the .b-touch-target utility, which expands the invisible hit area to 44 × 44 without changing the rendered size.

Active & Selected State

.is-active means "you are here" or "this is on" — navigation items, segmented controls, toggles. .is-selected means "this is chosen and something will happen to it" — list rows, chips, cards in a picker. Both tint with --b-color-accent-soft so the whole system re-themes from one accent token.

  • Flight itinerary
  • Invoice #2044
  • Weekly summary

For free-form selection surfaces — file grids, photo pickers, canvas objects — add .b-selectable. Selected items gain the accent-soft wash plus an inset accent ring.

brief.pdf
1.2 MB
logo.svg ✓
Selected
deck.key
18 MB
Accessibility. Selected state must never be color-only — roughly 1 in 12 men can't rely on the blue tint. Pair it with a second cue: a checkmark (as above), a weight change, or an icon swap. In markup, mirror the class with aria-selected="true" (options, tabs, rows) or aria-pressed="true" (toggle buttons) so the state is announced, not just painted.

Disabled State

Disabled controls fade to --b-opacity-disabled (0.4) and swap to a not-allowed cursor. The native disabled attribute is the default; the .is-disabled class produces the same look on elements that can't take the attribute (links, custom widgets).

disabled removes the control from the tab order — quick, but invisible to keyboard and screen-reader users, who can't discover why it's off. When the reason matters (a Send button waiting on a valid form), prefer aria-disabled="true" + .is-disabled: the control stays focusable and announced as dimmed, and your handler simply ignores activation. Add a tooltip explaining the condition.

Dragging State

Mid-drag, the grabbed element takes .is-dragging: opacity drops to 0.55 and the cursor switches to grabbing — on .b-task cards it also tilts 2° for a picked-up feel. Leave a .b-drag-ghost copy (50% opacity) at the origin so the layout doesn't jump. Anything with draggable="true" shows a grab cursor at rest.

At rest
cursor: grab
Origin ghost
.b-drag-ghost
Being dragged
.is-dragging

Drag & Drop

The full pattern composes four pieces: .b-dropzone around any list (its 2px dashed border is transparent until needed), .is-dropover when a drag hovers a valid target (dashed accent border + accent-soft wash), .b-drop-indicator as the insertion line between items, and .is-dragging on the grabbed card. The HTML5 dragstart / dragover / drop wiring — and toggling these classes — is app-side JavaScript; Barua supplies the states, not the engine.

Backlog
Refactor tokens
BAR-201
Ship dark mode
BAR-202
This week
Draft changelog
BAR-198
QA pass
BAR-199

Drag handle & reorder

When the whole row is tappable, reserve dragging for an explicit .b-drag-handle grip — it keeps scroll and drag from fighting on touch (touch-action: none is built in).

  • Welcome email
    Step 1
  • Follow-up nudge
    Step 2
Accessibility. Pointer dragging always needs a keyboard alternative. Give every draggable item a "Move up" / "Move down" pair in its action menu (or ⌘↑/⌘↓ when focused), and announce the result — "Ship dark mode, moved to This week, position 2 of 3" — through a live region.

Resizable Handle

Place a .b-resize-handle between two .b-panel children of a .b-split. The 9px grab strip renders as a hairline; on hover — or with .is-active while dragging — it thickens into an accent bar. The pointer-tracking that actually resizes the panels is app-side JavaScript.

Folders
Messages

For zero-JS cases, .b-resizable uses the browser's native resize: horizontal (drag the bottom-trailing corner); .b-resizable--vertical does the same for height.

Native resize

Drag my corner — no JavaScript involved.

Selection & Multi-Selection

Multi-selection layers three pieces: .b-selectable items (with .is-selected), a .b-selection-rect marquee drawn while the pointer rubber-bands across the surface, and a .b-selection-count readout. The rect is normally position: fixed and sized from pointer coordinates — it's frozen here with inline positioning so you can see it.

photo-01.heic ✓
2.4 MB
photo-02.heic ✓
3.1 MB
notes.txt
4 KB
2 selected

Once a selection exists, surface its operations in a floating Bulk Actions bar — see Specialized for .b-bulk-actions. Support the platform grammar too: -click toggles, -click extends a range, ⌘A selects all, esc clears.

Keyboard Shortcut

Shortcuts render with the native <kbd> element — or .b-kbd on a span — styled as a small key cap with a weighted bottom edge. Show them inline in help text, in menus and in the command palette so they're learnable, not secret.

Open the palette with ⌘K, save with ⌘S, close with esc or press ⇧⌘P for actions.
ShortcutActionConvention
⌘KCommand paletteGlobal; the fastest route to anything
⌘SSaveIntercept the browser default
⌘↵Submit / sendComposers and forms
escClose / cancelDismisses the top-most overlay first
⌘⌫DeleteDestructive — always confirm or offer undo

Menus surface the same bindings with .b-menu__shortcut, right-aligned in each row — see Navigation for the full menu family.


Focus Ring

Every focusable element shares one ring: --b-focus-ring, a 3.5px accent glow at 30% opacity. It attaches via :focus-visible, so it appears for keyboard and assistive tech but not for mouse clicks — press Tab below to walk it across four different control types. Inputs pair the ring with an accent border. Custom focusable elements can opt in with the .b-focus-ring class.

Then this link
Accessibility. :focus-visible is not :focus. :focus matches on every focus — including mouse clicks — which tempted a generation of developers into outline: none. :focus-visible lets the browser show the ring only when it helps (keyboard, switch access), so there's no reason left to suppress it. Test by tabbing through every screen: focus must always be visible and never trapped.

Contextual Actions

Row-level actions stay hidden until the row is hovered, keeping dense lists calm. Inside a .b-datagrid this is automatic — .b-row-actions cells flip from visibility: hidden to visible on tr:hover. The list below recreates both moments statically: the first row is at rest, the second is shown as if hovered.

  • Quarterly-report.pdf
    Edited 2 hours ago
  • Launch-plan.key
    Edited yesterday — hovered, actions revealed

Hover-revealed actions still need non-hover routes: reveal them on :focus-within for keyboard users, and keep the same commands in the row's action menu. On touch, the equivalent pattern is swipe actions — see Mobile for .b-swipe.

Scroll reveal

SwiftUI has scrollTransition; the web now has a scroll timeline. .b-reveal ties an entrance animation to the element's own position in the scrollport, so the browser drives it — no observer, no scroll listener, nothing on the main thread.

or scroll inside the box
scroll down ↓
Rises as it enters
--fade
--scale
--start, from the leading edge
--end, from the trailing edge
scroll back up to replay ↑
The markup
<article class="b-card b-reveal">…</article>
<article class="b-card b-reveal b-reveal--fade">…</article>
<article class="b-card b-reveal b-reveal--scale">…</article>

The box is its own scrollport, which is why the effect repeats every time you scroll it. On a page the timeline is the page itself, so each element reveals once as it comes up.

The animation finishes while the element is still travelling up the screen rather than when it reaches the middle, because a reader should never be watching something fade in that they are already trying to read.

It fails open. The whole block sits inside @supports (animation-timeline: view()), so where the timeline is unsupported everything is simply visible. An entrance animation that fails closed hides the page — which is the one outcome worse than no animation at all. Reduced motion disables it like everything else.

Context menu

SwiftUI's contextMenu, on the platform's own event. Point any element at a .b-menu and a right-click — or a long press, which the browser reports as the same event — opens it where the pointer is.

Q3 invoice.pdf
Right-click, or long-press on a touch screen

The menu is measured before it is placed, so one opened near the right or bottom edge turns inward instead of off the screen. Escape closes it, a click outside closes it, and focus moves to the first item so the keyboard can take over immediately.

Rolling numbers

SwiftUI's contentTransition(.numericText()). A figure that changes should travel to its new value rather than blink to it — on a dashboard the movement is what tells you something happened at all.

Messages sent 0
Outstanding TZS 0.0M
The markup
<span class="b-stat__value" data-b-count="12480">0</span>
<span class="b-stat__value" data-b-count="4.2">TZS 0.0M</span>

Anything carrying data-b-count rolls once it scrolls into view. Call it directly with Barua.count(el, value) when a figure changes in place. The formatting it finds is kept — thousands separators, a currency prefix, a unit suffix — and the element is given tabular figures for the duration so its width does not jitter as the digits change.

Scroll geometry

SwiftUI's onScrollGeometryChange. A scroll container carrying data-b-scroll-geometry writes its own position onto itself — --b-scroll-y, --b-scroll-x and --b-scroll-progress (0 to 1) — carries .is-scrolled once it has moved, and dispatches b:scroll with the numbers. Put it on <html> for the page. Anything that reads the properties is then pure CSS: the progress line below is a .b-scroll-progress whose width is the distance scrolled, and the title condenses on the browser's own scroll timeline with .b-scroll-condense.

Inbox

scrolled 0%

The line at the top is the distance scrolled.
The title above shrinks as the box moves.
Nothing here listens in JavaScript except the percentage.
The container carries .is-scrolled now.
And --b-scroll-progress is 1 at the end.
Keep going.
Nearly there.

For code, Barua.scroll.geometry(el, cb) calls back with { x, y, width, height, contentWidth, contentHeight, progressX, progressY }, coalesced to one read per frame, and returns a function that stops.

Scroll visibility

SwiftUI's onScrollVisibilityChange(threshold:). An element carrying data-b-visible — optionally the fraction of itself that counts, data-b-visible="0.8" — gains .is-visible as it crosses that much into the viewport, loses it on the way out, and dispatches b:visible with { visible, ratio }. It is also the entrance for browsers without a view timeline: .b-reveal with data-b-visible reveals on the class where it cannot on the timeline.

scroll down ↓
Half in view off screen
Almost all in view off screen
Half in view off screen
scroll back up ↑

For code, Barua.visible(el, cb, { threshold }); one observer is shared per threshold, so a page of a thousand rows costs one.

Scroll position

SwiftUI's scrollPosition(id:). A snapping container with data-b-snap names the child in view: data-b-snap-current on itself, .is-current on the child, .is-active on the matching dot in a data-b-snap-dots="#id" strip, and b:snapchange with { id, index, element }. The browser's own scrollsnapchange event drives it where that exists; elsewhere the nearest child after the scroll settles. Barua.snap.to(box, idOrIndex) goes there.

Dar es Salaamid="snap-dar"
Arushaid="snap-arusha"
Zanzibarid="snap-zanzibar"
Mwanzaid="snap-mwanza"

in view: snap-dar

Haptics

SwiftUI's sensoryFeedback. data-b-haptic on anything clickable — selection, impact, success, warning, error — plays a vibration on click; Barua.haptic(kind) does the same from code and returns whether anything could be felt. The web is honest about its limits here: Android phones vibrate; an iPhone has no vibration API, but Safari plays the system haptic when a switch toggles, so one is toggled off screen; a desktop feels nothing and says nothing.

Restraint. A haptic is punctuation, not prose: the moment a payment lands, a switch that flips, the end of a drag. A page that buzzes on every tap is a page people mute.

Persistence

SwiftUI's customizationID. data-b-persist="key" keeps a piece of interface state across reloads with no code: a <details> stays open or shut, a segmented control or tab strip keeps its choice, a scroll area keeps its place, a field keeps its value. Change the two below, reload the page, and they are as you left them. For anything else, Barua.persist.get(key), set(key, value) and clear(key) hold JSON under the same keys — which the React kit's usePersistedState shares, so the two can hand over.

Delivery details
Open this, reload, and it is still open.