Lumen

Components

The kit — Tailwind + the Lumen tokens, distributed the shadcn way.

Built on Tailwind + the Lumen tokens, distributed the shadcn way — source you own, not an opaque dependency. Everything below is rendered live from the lumen package (import … from 'lumen/react') — the docs show exactly what apps ship. Every component documents all of its states.

Live, from the package

These previews import the real components. Toggle Day/Night (bottom-left) and they re-theme with the rest of the system — they reference semantic tokens, never hard-coded color.

Button

The primary action. primary wears the signature Solar gradient; all variants share one focus ring, disabled, and press treatment.

Variants

Sizes

States

The full matrix, rendered — hover/focus/active are shown with their real classes forced on, so the matrix is visible without interaction (they're also live: hover and tab to any other button on this page).

default
hover (forced)
focus (forced)
active (forced)
loading
disabled
import { Button } from 'lumen/react';

<Button variant="primary" size="md" onClick={log}>Log event</Button>
<Button loading>Saving…</Button>

Badge

A status pill. Pairs a -text color with an icon so state survives the grayscale and color-blindness tests — meaning never rests on color alone.

On trackWatchCriticalSyncing28w 3d
import { Badge } from 'lumen/react';

<Badge variant="warning">Needs attention</Badge>
<Badge variant="neutral" icon={false}>28w 3d</Badge>

Input

A text field on the muted ground, with a Solar focus ring, a clear disabled state, and an invalid state driven by aria-invalid. Pair with a <label> — or let Field wire everything.

default
invalid
disabled
import { Input } from 'lumen/react';

<label htmlFor="q" className="sr-only">Search</label>
<Input id="q" placeholder="Search spans…" />

Field

The form unit — label, control, help, and the failure path. Field wires htmlFor/id, aria-describedby, and aria-invalid for you; the error renders with an icon (state never rests on color alone) and is announced via role="alert". Error microcopy follows the Voice rule: number, then meaning, then the next step.

From your latest lab panel.

Locked by your plan.

import { Field, Input } from 'lumen/react';

<Field label="Hemoglobin (g/dL)" error={error}>
  <Input value={value} onChange={onChange} />
</Field>

Tabs

Radix underneath (arrow keys, roving tabindex, ARIA for free); Lumen on top — a quiet muted rail, the active tab lifted onto the card with calm Solar text.

Five spans on the timeline.
import { Tabs, TabsList, TabsTrigger, TabsContent } from 'lumen/react';

<Tabs defaultValue="spans">
  <TabsList>
    <TabsTrigger value="spans">Spans</TabsTrigger>

  </TabsList>
  <TabsContent value="spans">…</TabsContent>
</Tabs>

Avatar

One rule, no drift: people wear the warm accent (Dawn ground, rust initials); the Agent alone wears the Solar gradient — it's the system's living presence, and the gradient is its badge. Photos fall back to initials.

Amara Okaforperson · sm
Amara Okaforperson · md
Amara Okaforperson · lg
Halo Agentagent
import { Avatar } from 'lumen/react';

<Avatar name="Amara Okafor" src={photoUrl} />
<Avatar name="Halo Agent" kind="agent" />

The loading kit

Five components and one hook, all made of the same light and all sitting over one timing contract. Which rung to reach for — and why a 90ms wait must render nothing at all — is Content → Loading; this is the API.

Spinner

The compact wait, for inside controls — the beam's channel looped: the same light bent into a ring, its tail fading into the track, the glow head as the light-front. Pure CSS (conic gradient + radial mask), so it stays crisp from 12px to 48px. tone="current" inherits the label's color — that's what <Button loading> uses.

12
16
24
40
muted
in button
import { Spinner } from 'lumen/react';

<Spinner />                              {/* decorative — aria-hidden */}
<Spinner size={24} label="Signing in…" /> {/* the whole message — role="status" */}

Skeleton

A placeholder in the shape of what's coming, with the wait-light sweeping through it. SkeletonText gives a paragraph its ragged last line; SkeletonGroup carries the one announcement for the whole region (the shapes themselves are aria-hidden by contract). Stagger the sweep with delay and the light reads down a list instead of every row flashing in unison.

Loading your reading
import { Skeleton, SkeletonText, SkeletonGroup } from 'lumen/react';

<SkeletonGroup label="Loading your biograph">
  <Skeleton shape="circle" size={40} />
  <Skeleton shape="line" width="42%" height={11} />
  <SkeletonText lines={3} />
  <Skeleton height={72} />   {/* a chart well */}
</SkeletonGroup>

Beam

The wait as a channel — light running the lumen of a vessel, for when you know neither the shape nor the duration. RouteProgress is this beam pinned to the top of the viewport, with the timing contract already applied.

import { Beam, RouteProgress } from 'lumen/react';

<Beam thickness={2} />

// React Router v7 — the page you're leaving stays legible
const nav = useNavigation();
<RouteProgress active={nav.state !== 'idle'} />

LoadingOverlay

For content that is stale, not absent: the answer already on screen stays readable under a dim veil while a fresher one lands. First load gets skeletons; every load after gets this.

MK

Resting heart rate

54 bpm — down 3 from your 30-day median, and the lowest reading since the Tuesday you started the iron protocol.

import { LoadingOverlay } from 'lumen/react';

<LoadingOverlay active={revalidating} label="Refreshing your timeline">
  <Timeline events={events} />
</LoadingOverlay>

Loader

The presence scale — for a wait that owns the screen. Deliberately not the logo, so the identity mark isn't welded to every wait: five cells on the vessel line, beating an interior rhythm. rhythm picks which of the six channel motions (heartbeat · pulse · perfuse · peristalsis · breath · sleep), presence picks how much the system is showing up, and the vital driver gives the loop a wander so it never ticks like a clock. At urgent the light scatters into a field around the cells.

16
24
40
cycle
muted
dormant
urgent
import { Loader } from 'lumen/react';

<Loader label="Syncing Oura data…" />
<Loader rhythm="perfuse" presence="resting" />
<Loader size={48} presence="urgent" />  {/* the cell field comes along */}
<Loader size={48} cycle />              {/* heartbeat → breath → perfuse */}

Vital — the motion language: one lumen, one tempo, six rhythms, the presence dial, and why variance is what makes it read alive. wave still works as an alias of perfuse.

Arrival

The wait's ending, designed: while pending it shows the placeholder; when the wait resolves, a Loader in the placeholder glides its cells to rest (the field draws its light home), and only then does the content rise in on the exhale (lumen-arrive, ×0.32 of the tempo each way). Placeholders with nothing to land — skeletons, spinners — skip straight to the rise, and content that was never waited for never animates. Custom placeholders join the handshake with useSettle.

settle ×0.32 · enter ×0.32

Oura synced

312 new samples — resting heart rate 54 bpm.

import { Arrival } from 'lumen/react';

<Arrival pending={pending} placeholder={<Loader size={40} />}>
  <Reading data={data} />
</Arrival>

usePending

The contract itself: 180ms before a wait is admitted, 420ms minimum once it is, 6s before the copy escalates. RouteProgress, LoadingOverlay, and the Arrival recipes ride it; everywhere else, you call it yourself.

import { usePending, WAIT } from 'lumen/react';

const { pending, long } = usePending(fetcher.state !== 'idle');

HaloMark

The identity itself — five dots holding the resting Vitruvian pose (head, two hands, two feet), breathing at rest, dancing the logo-lab cycle (body → hand → pulse → constellation) while Halo works. Fills currentColor. Reserved for presence and brand moments — it is deliberately not the Loader (loading has its own language), and never a bullet. Under reduced motion every pose settles to rest.

rest · 16
rest · 28
breathe
dance
on ink
import { HaloMark } from 'lumen/react';

<HaloMark />                      {/* resting, 28px */}
<HaloMark pose="breathe" />       {/* calm presence */}
<HaloMark pose="dance" size={40} />  {/* Halo is working */}

Card

The surface family — full shadcn anatomy (CardHeader / CardTitle / CardDescription / CardAction / CardContent / CardFooter) on the Lumen skin: warm card ground, hairline linen border, the resting shadow.

Iron protocol
Week 3 of 8 — on track.
On track

Hemoglobin 11.2 g/dL, up from 9.8 — the supplement is working.

import { Card, CardHeader, CardTitle, CardDescription, CardAction, CardContent, CardFooter } from 'lumen/react';

<Card>
  <CardHeader>
    <CardTitle>Iron protocol</CardTitle>
    <CardDescription>Week 3 of 8</CardDescription>
    <CardAction><Badge variant="success">On track</Badge></CardAction>
  </CardHeader>
  <CardContent>…</CardContent>
</Card>

Separator

The hairline seam. Radix underneath — decorative by default; pass decorative={false} when it separates real content regions.

Vitals
Spans
Flows
Sources

Dialog

Radix underneath (focus trap, Escape, aria wiring); Lumen on top — the warm --overlay wash on the modal layer, a raised card panel that arrives with the gentle entrance and leaves instantly. The close target clears the 24px floor.

import { Dialog, DialogTrigger, DialogContent, DialogHeader, DialogTitle, DialogDescription, DialogFooter } from 'lumen/react';

<Dialog>
  <DialogTrigger asChild><Button>Flag for review</Button></DialogTrigger>
  <DialogContent>
    <DialogHeader>
      <DialogTitle>Flag this reading?</DialogTitle>
      <DialogDescription>…</DialogDescription>
    </DialogHeader>
    <DialogFooter>…</DialogFooter>
  </DialogContent>
</Dialog>

Sheet

A Dialog that enters from an edge — same modal lifecycle, a side-keyed entrance (right default · left · bottom · top). The bottom sheet wears the grabber bar.

import { Sheet, SheetTrigger, SheetContent, SheetHeader, SheetTitle } from 'lumen/react';

<Sheet>
  <SheetTrigger asChild><Button>Open</Button></SheetTrigger>
  <SheetContent side="bottom">…</SheetContent>
</Sheet>

States

The failure paths as first-class components — one calm scaffold, the mark in three expressions: rest (empty), breathe (loading), and the alert (error). Copy carries the meaning; the visual is hidden from assistive tech. Loading announces politely; Error announces via role="alert" and offers the retry.

No readings yet

Log your first blood pressure to light the halo.

Syncing Oura data…

Sync failed

Oura didn't answer. Your readings are safe.

import { EmptyState, LoadingState, ErrorState } from 'lumen/react';

<EmptyState title="No readings yet" actionLabel="Log a reading" onAction={openLog} />
<LoadingState label="Syncing Oura data…" />
<ErrorState title="Sync failed" body="Oura didn't answer." onRetry={retry} />

Checkbox

Radix underneath (keyboard, aria-checked, indeterminate); the Solar fill when checked. Use for consent and multi-select — immediate effects get a Switch. Pair with a label so the target grows past the 24px floor.

off
on
indeterminate
disabled
on · disabled
import { Checkbox } from 'lumen/react';

<label className="flex items-center gap-2 text-sm">
  <Checkbox defaultChecked /> Share readings with Dr. Reyes
</label>

RadioGroup

One choice from a short list, all options visible. Radix roving arrows move selection and focus together; the chosen ring holds the Solar dot.

Switch

An immediate effect, on or off — linen track, warm thumb, Solar when on. Snaps under reduced motion. Consent belongs to Checkbox.

off
on
disabled
on · disabled

SegmentedControl

A few mutually-exclusive choices, all visible — choosing a value, not switching a view (views are Tabs). The selected segment lifts onto the card with quiet Solar text, the same calm-active rule as Tabs.

import { SegmentedControl, SegmentedControlItem } from 'lumen/react';

<SegmentedControl value={range} onValueChange={setRange} aria-label="Range">
  <SegmentedControlItem value="day">Day</SegmentedControlItem>
  <SegmentedControlItem value="week">Week</SegmentedControlItem>
</SegmentedControl>

Select

Pick one from many — collapsed until asked. The trigger matches Input on the muted ground; options open on the popover layer with the Solar check on the chosen one. A few always-visible choices read better as a SegmentedControl.

Slider

A value on a continuous range — quiet muted track, Solar range, warm thumb. The interactive band is taller than the 6px line, so fingers have something to hit. Known small steps read better as a Stepper.

Stepper

A small count adjusted in known steps — role="spinbutton", arrows on the readout, −/+ that disable at the bounds, tabular numerals on a fixed-width readout so digits don't shift the buttons.

2
default
75mg
with unit
0
at bound
3
disabled
import { Stepper } from 'lumen/react';

<Stepper value={doses} onChange={setDoses} min={0} max={10} unit="mg" label="Iron dose" />

Tooltip

A hint on hover/focus — the inverted surface (ink ground, paper text), a transient whisper on the top layer. Never essential content: touch has no hover, so anything that matters must also exist on the surface.

import { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider } from 'lumen/react';

<TooltipProvider>
  <Tooltip>
    <TooltipTrigger asChild><Button>Setpoint?</Button></TooltipTrigger>
    <TooltipContent>The target your plan steers toward.</TooltipContent>
  </Tooltip>
</TooltipProvider>

A persistent inline notice. Tone is carried three ways — accent bar + glyph + wording — so it survives grayscale; the body stays neutral ink. Warning/danger announce assertively (role="alert"), info/success politely. Transient confirmation belongs to Toast.

Visit prep is ready

Three questions for Dr. Reyes, from this week's readings.

Iron protocol complete

8 of 8 weeks — hemoglobin back in range.

import { Banner } from 'lumen/react';

<Banner tone="warning" title="9.1 g/dL is below the expected range (9.5–15)"
  body="Re-enter the value, or flag it for your care team."
  actionLabel="Flag it" onAction={flag} onDismiss={dismiss} />

Toast

Transient confirmation — sonner wearing the Lumen skin, mounted once (<Toaster />) near the app root, fired with toast(…). Sonner brings the live region, hover-pause, and reduced-motion handling. A notice that must persist is a Banner.

import { Toaster, toast } from 'lumen/react';

// once, near the root:
<Toaster />

// anywhere:
toast.success('Plan updated', { description: 'Step moved to Thursday.' });

Actions behind a trigger — Radix typeahead/arrows/submenus, the popover-layer card. Items are actions; picking a value is Select's job. variant="destructive" marks the irreversible one.

Chip

A small labeled token — a filter that toggles (aria-pressed), a removable tag, or both. Selected wears the Dawn tint with Solar ink; the remove × is its own ≥24px target with its own focus ring.

Locked
FatigueNauseaHeadache
import { Chip } from 'lumen/react';

<Chip selected={on} onClick={toggle}>Sleep</Chip>
<Chip selected onRemove={remove}>Fatigue</Chip>

Metric

One vital, read at a glance — label · tabular value · unit · optional status Badge (triple-encoded). Tabular numerals keep metric grids column-aligned. Display-only: charts add the trend, blocks add the panel.

Blood pressure118/76mmHgIn range
Hemoglobin9.1g/dLBorderline
Sleep7.4hr
Steps8,912

SearchInput

The Input grammar with a magnifier, a clear × that appears only with text (a ≥24px labelled target), and a real Enter action (onSubmit). Always render the no-match state in words — never an empty void.

  • Dr. Reyes — OB-GYN
  • Dr. Okafor — Primary care
  • Nadia Osei — Doula
import { SearchInput } from 'lumen/react';

<SearchInput value={q} onChange={(e) => setQ(e.target.value)} onSubmit={run} placeholder="Search…" />

Calendar & DatePicker

Calendar is react-day-picker fully re-skinned on the tokens. The two markers coexist, distinct on purpose: today wears a Solar ring (where you are), selected wears the Solar fill (what you chose). DatePicker puts it behind an Input-matched trigger on the popover layer — a date is one gesture, so it commits on day tap and closes.

September 2026

Commits on day tap.

The platform's own picker.

import { Calendar, DatePicker, TimePicker } from 'lumen/react';

<DatePicker value={date} onChange={setDate} label="Next visit date" />
<TimePicker value="08:30" onChange={setTime} aria-label="Reminder time" />
<Calendar mode="range" selected={range} onSelect={setRange} />

TimePicker

The native time input wearing the Input grammar with mono tabular digits. On web the platform's own picker wins — keyboard-native, screen-reader-native, and mobile browsers surface their wheel for free. (The hand-rolled column picker stays a native-app pattern.)

What's here / what's next

The kit now covers the foundational families end to end — actions (Button), forms (Input, Field, Checkbox, RadioGroup, Switch, SegmentedControl, Select, Slider, Stepper, SearchInput, DatePicker, TimePicker), surfaces (Card, Separator, Dialog, Sheet, Popover), the whole loading ladder (Spinner, Skeleton, Beam, LoadingOverlay, Loader, Arrival, usePending), feedback (Badge, Banner, Toast, Tooltip, the States), menus (DropdownMenu), identity (Avatar, HaloMark), and data (Metric; Trend/Ring/Progress live with Charts — Progress runs indeterminate on the same beam with value={null}) — all the same pattern: tokens → cva variants → cn merge → all states rendered → a11y baked in. The Halo compositions (headers, entity cards, the Timeline and Steps rails) live in Blocks. Coming next: a registry.json so everything installs via the shadcn CLI, and the remaining app patterns (the Ask surface, the hero motifs, the Notice register) promoted as their APIs settle.

The retired v0 screen mock shows the kit's ancestors assembled.

On this page