# Health

> A personal health page in the app shell with a sidebar: vitals with healthy ranges, an insight with its reason, activity rings, last night's sleep and today's medication.

Pattern · Pro kit · [Live page](https://ui.twentyfirst.media/pro/patterns/HealthScreen/)

A personal health page in the app shell with a sidebar: vitals with healthy ranges, an insight with
its reason, activity rings, last night's sleep and today's medication.


The whole screen is glass over `Ambient`: one `<UIProvider surface="glass">` turns the shell, the
sidebar, panels and the Life cards (vitals, goals, insights, journal entries) into glass, with
inputs and chips on them translucent too. Without the provider the same markup is solid.

## Code

```tsx
import * as React from "react";
import {
  ActivityRings,
  Ambient,
  AppShell,
  Button,
  InsightCard,
  MedicationList,
  PageHeader,
  Panel,
  Segmented,
  SleepChart,
  Stagger,
  UIProvider,
  VitalTile,
} from "@twentyfirstmedia/pro";
import type { MedicationItem, SidebarGroup, SleepSegment } from "@twentyfirstmedia/pro";

const NAV: SidebarGroup[] = [
  {
    items: [
      { id: "today", label: "Today", icon: "sun" },
      { id: "assistant", label: "Assistant", icon: "sparkle", badge: "dot" },
    ],
  },
  {
    label: "Life",
    items: [
      { id: "health", label: "Health", icon: "heartPulse" },
      { id: "habits", label: "Habits & journal", icon: "check" },
      { id: "money", label: "Money", icon: "wallet" },
      { id: "insurance", label: "Insurance", icon: "shield" },
      { id: "documents", label: "Documents", icon: "folder", count: 2 },
    ],
  },
];

const at = (d: number, h: number, m: number) => new Date(2026, 10, d, h, m).getTime();
const night: SleepSegment[] = [
  { stage: "awake", start: at(12, 23, 10), end: at(12, 23, 22) },
  { stage: "light", start: at(12, 23, 22), end: at(13, 0, 5) },
  { stage: "deep", start: at(13, 0, 5), end: at(13, 1, 10) },
  { stage: "rem", start: at(13, 1, 10), end: at(13, 1, 45) },
  { stage: "light", start: at(13, 1, 45), end: at(13, 3, 0) },
  { stage: "deep", start: at(13, 3, 0), end: at(13, 3, 35) },
  { stage: "light", start: at(13, 3, 35), end: at(13, 4, 50) },
  { stage: "rem", start: at(13, 4, 50), end: at(13, 5, 40) },
  { stage: "light", start: at(13, 5, 40), end: at(13, 6, 35) },
  { stage: "awake", start: at(13, 6, 35), end: at(13, 6, 45) },
];

/** A personal health page in glass over ambient colour: vitals with healthy ranges, activity, sleep, medication and an insight. */
export default function Preview() {
  const [range, setRange] = React.useState("Today");
  const [meds, setMeds] = React.useState<MedicationItem[]>([
    { id: "d", name: "Vitamin D", dose: "1,000 IU", time: "07:30", status: "taken" },
    { id: "r", name: "Ramipril", dose: "5 mg", time: "13:00", status: "due" },
    { id: "s", name: "Magnesium", dose: "300 mg", time: "22:00", status: "upcoming" },
  ]);
  return (
    <UIProvider scope="subtree" surface="glass">
      <Ambient />
      <AppShell
        navigation="sidebar"
        brand="Life"
        sidebarGroups={NAV}
        active="health"
        user={{ name: "Mara Ellis", email: "mara@halo.app" }}
        userMenu={[
          { label: "Settings", icon: "settings" },
          "-",
          { label: "Sign out", icon: "logout" },
        ]}
        dock={false}
        side={
          <Panel side title="Medication" subtitle="3 doses today">
            <MedicationList
              items={meds}
              onTake={(id) =>
                setMeds((all) => all.map((m) => (m.id === id ? { ...m, status: "taken" } : m)))
              }
            />
          </Panel>
        }
      >
        <div className="tf-in">
          <PageHeader
            title="Health"
            subtitle="Friday, 13 November"
            actions={[
              <Segmented
                key="r"
                label="Range"
                options={["Today", "Week", "Month"]}
                value={range}
                onChange={setRange}
              />,
            ]}
          />
        </div>
        <Stagger
          className="tf-grid"
          style={
            {
              "--cols": "repeat(3, minmax(0, 1fr))",
              "--cols-sm": "1fr",
              gap: 16,
            } as React.CSSProperties
          }
        >
          <VitalTile
            label="Resting heart rate"
            value={58}
            unit="bpm"
            normal={[50, 80]}
            trend={[61, 60, 62, 59, 58, 58, 57, 58]}
            caption="Watch · 06:45"
          />
          <VitalTile
            label="Blood pressure"
            icon="zap"
            value={138}
            unit="mmHg"
            normal={[90, 129]}
            trend={[128, 131, 130, 134, 136, 135, 138]}
            caption="Cuff · yesterday"
          />
          <VitalTile
            label="Weight"
            icon="trend"
            value={74.2}
            digits={1}
            unit="kg"
            trend={[75.8, 75.4, 75.1, 74.9, 74.6, 74.4, 74.2]}
            caption="Scale · today"
          />
        </Stagger>
        <InsightCard
          tone="warning"
          icon="zap"
          meta="Health · this week"
          title="Blood pressure has been above range for 5 days"
          sources={[{ title: "Cuff · 7 readings", icon: "zap" }]}
          why="Five of your last seven evening readings were above 129 mmHg. You asked to be told when that happens more than three days in a row."
          acceptLabel="Share with Dr. Weber"
          onAccept={() => {}}
          onSnooze={() => {}}
        >
          Consider taking the next readings in the morning too, and mention it at your check-up.
        </InsightCard>
        <div
          className="tf-grid"
          style={
            {
              "--cols": "minmax(0, 1fr) minmax(0, 1.4fr)",
              "--cols-sm": "1fr",
              gap: 16,
            } as React.CSSProperties
          }
        >
          <Panel title="Activity" subtitle="Goals for today">
            <ActivityRings
              rings={[
                { label: "Move", value: 420, goal: 600, unit: "kcal", tint: "blush" },
                { label: "Exercise", value: 24, goal: 30, unit: "min", tint: "mint" },
                { label: "Stand", value: 9, goal: 12, unit: "h", tint: "sky" },
              ]}
            />
          </Panel>
          <Panel
            title="Sleep"
            subtitle="Last night"
            action={
              <Button variant="ghost" size="sm">
                Trends
              </Button>
            }
          >
            <SleepChart segments={night} />
          </Panel>
        </div>
      </AppShell>
    </UIProvider>
  );
}
```

## Props

### AppShell

| Prop | Type | Notes |
| --- | --- | --- |
| `active?` | `string` |  |
| `brand?` | `ReactNode` | The product name, or a node (a logo mark with the name; collapsed sidebars show it whole). |
| `tag?` | `string` |  |
| `nav?` | `NavItem[]` |  |
| `user?` | `{ name: string; email?: string }` |  |
| `onNavigate?` | `(label: string) => void` |  |
| `topExtra?` | `ReactNode` |  |
| `sheetStyle?` | `CSSProperties` |  |
| `contentKey?` | `string \| number` |  |
| `side?` | `ReactNode` |  |
| `dock?` | `boolean` |  |
| `dockItems?` | `DockProps["items"]` |  |
| `railTop?` | `RailItem[]` |  |
| `railBottom?` | `RailItem[]` |  |
| `navigation?` | `"top" \| "sidebar"` | `top` (default): navigation in the top bar, tools in the side rail. `sidebar`: a labelled sidebar on the frame, with groups, nesting and a drawer on phones. |
| `sidebarGroups?` | `SidebarGroup[]` | With `navigation="sidebar"`: groups instead of the flat `nav` list. |
| `sidebar?` | `Partial<Omit<SidebarProps, "variant" \| "groups">>` | With `navigation="sidebar"`: more sidebar props (collapsed, storageKey, shortcut, linkAs …). |
| `workspaces?` | `WorkspaceSwitcherProps` | With `navigation="sidebar"`: the workspace switcher at the top instead of the brand. |
| `userMenu?` | `(MenuItem \| "-")[]` | The account menu's entries (with `navigation="sidebar"` at the foot of the sidebar). |
| `children?` | `ReactNode` |  |
| `className?` | `string` | Extra class on the root element. |
| `style?` | `CSSProperties` | Inline style on the root element. |

### VitalTile

| Prop | Type | Notes |
| --- | --- | --- |
| `surface?` | `Surface` | `"glass"`: translucent glass over a backdrop (`Ambient`, a photo, a map); `"solid"` opts out inside a glass `UIProvider`. |
| `label` | `string` | Required |
| `value` | `number` | Required |
| `unit?` | `string` |  |
| `icon?` | `IconName` |  |
| `normal?` | `[number, number]` | The healthy range [low, high]; the tile says whether the value is below, in or above it. |
| `scale?` | `[number, number]` | The whole scale of the band [min, max] (default: the normal range with room on both sides). |
| `trend?` | `number[]` | Recent values, oldest first, drawn as a sparkline. |
| `caption?` | `ReactNode` | Under the value: when it was measured, the source … |
| `digits?` | `number` | Digits after the decimal point (default 0). |
| `className?` | `string` | Extra class on the root element. |
| `style?` | `CSSProperties` | Inline style on the root element. |

### ActivityRings

| Prop | Type | Notes |
| --- | --- | --- |
| `rings` | `ActivityRing[]` | Outer ring first. |
| `size?` | `number` | Diameter of the rings (default 132). |
| `legend?` | `boolean` | Hide the legend beside the rings. |
| `label?` | `string` | Accessible name (default "Activity"). |
| `className?` | `string` | Extra class on the root element. |
| `style?` | `CSSProperties` | Inline style on the root element. |

### SleepChart

| Prop | Type | Notes |
| --- | --- | --- |
| `segments` | `SleepSegment[]` | The night in order, from falling asleep to getting up. |
| `label?` | `string` | Accessible name (default "Sleep stages"). |
| `summary?` | `boolean` | Hide the totals per stage under the chart. |
| `className?` | `string` | Extra class on the root element. |
| `style?` | `CSSProperties` | Inline style on the root element. |

### MedicationList

| Prop | Type | Notes |
| --- | --- | --- |
| `items` | `MedicationItem[]` | Required |
| `onTake?` | `(id: string) => void` | "Mark as taken" was pressed. |
| `label?` | `string` | Accessible name (default "Medication"). |
| `className?` | `string` | Extra class on the root element. |
| `style?` | `CSSProperties` | Inline style on the root element. |

### InsightCard

| Prop | Type | Notes |
| --- | --- | --- |
| `surface?` | `Surface` | `"glass"`: translucent glass over a backdrop (`Ambient`, a photo, a map); `"solid"` opts out inside a glass `UIProvider`. |
| `title` | `string` | Required |
| `children?` | `ReactNode` |  |
| `icon?` | `IconName` |  |
| `tone?` | `"info" \| "success" \| "warning" \| "highlight"` |  |
| `meta?` | `ReactNode` | Above the title: area and time ("Money · 2 h ago"). |
| `sources?` | `Source[]` | Where the insight comes from, as chips (like chat sources). |
| `why?` | `ReactNode` | Why the assistant shows it; folded away behind "Why am I seeing this?". |
| `onAccept?` | `() => void` |  |
| `acceptLabel?` | `string` |  |
| `onSnooze?` | `() => void` |  |
| `onDismiss?` | `() => void` |  |
| `className?` | `string` | Extra class on the root element. |
| `style?` | `CSSProperties` | Inline style on the root element. |

### PageHeader

| Prop | Type | Notes |
| --- | --- | --- |
| `crumbs?` | `(string \| Crumb)[]` | A breadcrumb above the title (see `Breadcrumb`): labels, or items with `href`/`onClick`. |
| `crumbLinkAs?` | `ElementType` | The element crumb links render as, e.g. Next.js `Link`. |
| `title` | `string` | Required |
| `subtitle?` | `string` |  |
| `actions?` | `ReactNode[]` |  |
| `className?` | `string` | Extra class on the root element. |
| `style?` | `CSSProperties` | Inline style on the root element. |

### Panel

| Prop | Type | Notes |
| --- | --- | --- |
| `side?` | `boolean` |  |
| `title?` | `string` |  |
| `subtitle?` | `string` |  |
| `action?` | `ReactNode` |  |
| `children?` | `ReactNode` |  |
| `surface?` | `Surface` | `"glass"`: translucent glass over what is behind (maps, 3D, video, photos); `"solid"` opts out inside a glass `UIProvider`. |
| `className?` | `string` | Extra class on the root element. |
| `style?` | `CSSProperties` | Inline style on the root element. |

### Segmented

| Prop | Type | Notes |
| --- | --- | --- |
| `options` | `(string \| Option)[]` | Required |
| `value?` | `string` |  |
| `defaultValue?` | `string` |  |
| `onChange?` | `(v: string) => void` |  |
| `size?` | `"sm"` |  |
| `block?` | `boolean` |  |
| `label?` | `string` |  |
| `className?` | `string` | Extra class on the root element. |
| `style?` | `CSSProperties` | Inline style on the root element. |

### Stagger

| Prop | Type | Notes |
| --- | --- | --- |
| `as?` | `string` |  |
| `className?` | `string` |  |
| `style?` | `CSSProperties` |  |
| `delay?` | `number` | Extra delay before the first child, in ms. |
| `reveal?` | `boolean` | Below the fold, wait until the group scrolls into view (default true). |
| `children?` | `ReactNode` |  |

### Button

| Prop | Type | Notes |
| --- | --- | --- |
| `variant?` | `"primary" \| "secondary" \| "soft" \| "ghost" \| "danger" \| "danger-solid"` |  |
| `size?` | `"md" \| "sm"` |  |
| `icon?` | `IconName` |  |
| `iconRight?` | `IconName` |  |
| `disabled?` | `boolean` |  |
| `type?` | `"button" \| "submit" \| "reset"` |  |
| `onClick?` | `(e: MouseEvent<HTMLButtonElement>) => void` |  |
| `children?` | `ReactNode` |  |
| `asChild?` | `boolean` | Render the single child element (a link, a router Link) with the button's look instead of a <button>. |
| `id?` | `string` |  |
| `name?` | `string` | Form field name and value, sent with the form when this button submits it. |
| `value?` | `string` |  |
| `form?` | `string` | The id of the form this button belongs to, when it sits outside it. |
| `title?` | `string` |  |
| `tabIndex?` | `number` |  |
| `autoFocus?` | `boolean` |  |
| `aria-label?` | `string` |  |
| `aria-labelledby?` | `string` |  |
| `aria-describedby?` | `string` |  |
| `aria-expanded?` | `boolean \| "true" \| "false"` | For disclosure and menu triggers: whether what the button controls is open. |
| `aria-controls?` | `string` |  |
| `aria-haspopup?` | `boolean \| "true" \| "false" \| "menu" \| "listbox" \| "tree" \| "grid" \| "dialog"` |  |
| `aria-pressed?` | `boolean \| "true" \| "false" \| "mixed"` | For toggle buttons. |
| `aria-current?` | `boolean \| "true" \| "false" \| "page" \| "step" \| "location" \| "date" \| "time"` |  |
| `className?` | `string` | Extra class on the root element. |
| `style?` | `CSSProperties` | Inline style on the root element. |
