# Dash Health

> A health summary dashboard with pinned metric cards, sparklines, and category views in a sidebar.

A shadcn/ui block from the [@esuiss](https://suiss.app/ui?utm_source=shadcn.exchange) registry, listed on [shadcn.exchange](https://shadcn.exchange/r/esuiss/dash-health).

- Page, with a live preview: https://shadcn.exchange/r/esuiss/dash-health
- Screenshot: https://abzjhwgwpdpz7ua5.public.blob.vercel-storage.com/shots/esuiss/dash-health-light-a2d3ad328c538b80.png
- Registry item: https://suiss.app/ui/r/new-york-v4/dash-health.json
- Categories: blocks, dash
- Listed: 2026-10-20, source last changed 2026-10-20

## Install

```bash
npx shadcn@latest add https://shadcn.exchange/r/esuiss/dash-health.json
```

Run it in a project that has shadcn/ui set up. It adds the files below and installs what they depend on.

## Dependencies

- Registry items: `@esuiss/avatar`, `@esuiss/button`, `@esuiss/sidebar`

## Files

### `components/blocks/dash-health.tsx`

```tsx
"use client"

import * as React from "react"

import { IconPlaceholder } from "@/app/(create)/components/icon-placeholder"
import { Avatar, AvatarFallback } from "@/components/ui/avatar"
import { Button } from "@/components/ui/button"
import {
  Sidebar,
  SidebarContent,
  SidebarFooter,
  SidebarGroup,
  SidebarGroupContent,
  SidebarGroupLabel,
  SidebarHeader,
  SidebarInset,
  SidebarMenu,
  SidebarMenuBadge,
  SidebarMenuButton,
  SidebarMenuItem,
  SidebarProvider,
  SidebarRail,
  SidebarTrigger,
  useSidebar,
} from "@/components/ui/sidebar"

type Category = "Activity" | "Heart" | "Sleep" | "Mobility"

type CategoryStyle = {
  value: Category
  icon: React.ReactNode
  tint: string
  text: string
}

const categories: [CategoryStyle, ...CategoryStyle[]] = [
  {
    value: "Activity",
    icon: (
      <IconPlaceholder
        lucide="FlameIcon"
        tabler="IconFlame"
        hugeicons="FireIcon"
        phosphor="FlameIcon"
        remixicon="RiFireLine"
        className="fill-current"
      />
    ),
    tint: "bg-orange-500",
    text: "text-[color-mix(in_oklab,var(--color-orange-500),var(--foreground)_45%)] dark:text-orange-500",
  },
  {
    value: "Heart",
    icon: (
      <IconPlaceholder
        lucide="HeartIcon"
        tabler="IconHeart"
        hugeicons="FavouriteIcon"
        phosphor="HeartIcon"
        remixicon="RiHeartLine"
        className="fill-current"
      />
    ),
    tint: "bg-red-500",
    text: "text-[color-mix(in_oklab,var(--color-red-500),var(--foreground)_25%)] dark:text-red-500",
  },
  {
    value: "Sleep",
    icon: (
      <IconPlaceholder
        lucide="MoonIcon"
        tabler="IconMoon"
        hugeicons="MoonIcon"
        phosphor="MoonIcon"
        remixicon="RiMoonLine"
        className="fill-current"
      />
    ),
    tint: "bg-cyan-500",
    text: "text-[color-mix(in_oklab,var(--color-cyan-500),var(--foreground)_50%)] dark:text-cyan-500",
  },
  {
    value: "Mobility",
    icon: (
      <IconPlaceholder
        lucide="ChartColumnIcon"
        tabler="IconChartBar"
        hugeicons="ChartColumnIcon"
        phosphor="ChartBarIcon"
        remixicon="RiBarChartLine"
      />
    ),
    tint: "bg-green-500",
    text: "text-[color-mix(in_oklab,var(--color-green-500),var(--foreground)_45%)] dark:text-green-500",
  },
]

const metrics: {
  category: Category
  label: string
  value: string
  unit?: string
  color: string
  data: number[]
  bars?: boolean
}[] = [
  {
    category: "Activity",
    label: "Move",
    value: "486",
    unit: "kcal",
    color: "var(--color-red-500)",
    data: [40, 62, 38, 70, 55, 81, 65],
    bars: true,
  },
  {
    category: "Activity",
    label: "Steps",
    value: "8,412",
    unit: "steps",
    color: "var(--color-orange-500)",
    data: [60, 80, 45, 90, 70, 95, 52],
    bars: true,
  },
  {
    category: "Heart",
    label: "Resting Heart Rate",
    value: "58",
    unit: "bpm",
    color: "var(--color-red-500)",
    data: [61, 60, 59, 58, 60, 57, 58],
  },
  {
    category: "Sleep",
    label: "Sleep",
    value: "7 hr 12 min",
    color: "var(--color-cyan-500)",
    data: [68, 74, 59, 79, 64, 86, 72],
    bars: true,
  },
  {
    category: "Heart",
    label: "Cardio Fitness",
    value: "42.1",
    unit: "VO₂ max",
    color: "var(--color-red-500)",
    data: [40, 40, 41, 41, 42, 42, 42],
  },
  {
    category: "Mobility",
    label: "Walking Speed",
    value: "5.1",
    unit: "km/h",
    color: "var(--color-green-500)",
    data: [48, 50, 49, 51, 50, 52, 51],
  },
]

function Tile({ icon, tint }: { icon: React.ReactNode; tint: string }) {
  return (
    <span
      className={`flex size-6 shrink-0 items-center justify-center rounded-md text-white [&_svg]:size-3.5 ${tint}`}
    >
      {icon}
    </span>
  )
}

type NavItem = {
  value: string
  label: string
  icon: React.ReactNode
  badge?: React.ReactNode
  group?: string
}

function NavSidebar({
  items,
  value,
  onValueChange,
  header,
  footer,
}: {
  items: NavItem[]
  value: string
  onValueChange: (value: string) => void
  header?: React.ReactNode
  footer?: React.ReactNode
}) {
  const { isMobile, setOpenMobile } = useSidebar()
  const groups = [...new Set(items.map((item) => item.group ?? ""))]

  return (
    <Sidebar collapsible="icon">
      {header && (
        <SidebarHeader className="group-data-[collapsible=icon]:hidden">
          {header}
        </SidebarHeader>
      )}
      <SidebarContent>
        {groups.map((group) => (
          <SidebarGroup key={group}>
            {group && <SidebarGroupLabel>{group}</SidebarGroupLabel>}
            <SidebarGroupContent>
              <SidebarMenu>
                {items
                  .filter((item) => (item.group ?? "") === group)
                  .map((item) => (
                    <SidebarMenuItem key={item.value}>
                      <SidebarMenuButton
                        isActive={item.value === value}
                        tooltip={item.label}
                        aria-current={item.value === value ? "page" : undefined}
                        onClick={() => {
                          onValueChange(item.value)
                          if (isMobile) setOpenMobile(false)
                        }}
                      >
                        {item.icon}
                        <span>{item.label}</span>
                      </SidebarMenuButton>
                      {item.badge !== undefined && (
                        <SidebarMenuBadge>{item.badge}</SidebarMenuBadge>
                      )}
                    </SidebarMenuItem>
                  ))}
              </SidebarMenu>
            </SidebarGroupContent>
          </SidebarGroup>
        ))}
      </SidebarContent>
      {footer && (
        <SidebarFooter className="group-data-[collapsible=icon]:hidden">
          {footer}
        </SidebarFooter>
      )}
      <SidebarRail />
    </Sidebar>
  )
}

const items: NavItem[] = [
  {
    value: "Summary",
    label: "Summary",
    icon: (
      <Tile
        icon={
          <IconPlaceholder
            lucide="HeartIcon"
            tabler="IconHeart"
            hugeicons="FavouriteIcon"
            phosphor="HeartIcon"
            remixicon="RiHeartLine"
            className="fill-current"
          />
        }
        tint="bg-pink-500"
      />
    ),
  },
  {
    value: "Sharing",
    label: "Sharing",
    icon: (
      <Tile
        icon={
          <IconPlaceholder
            lucide="UsersIcon"
            tabler="IconUsers"
            hugeicons="UserGroupIcon"
            phosphor="UsersIcon"
            remixicon="RiGroupLine"
          />
        }
        tint="bg-blue-500"
      />
    ),
  },
  ...categories.map((category) => ({
    value: category.value,
    label: category.value,
    icon: <Tile icon={category.icon} tint={category.tint} />,
    group: "Categories",
  })),
]

const days = ["mon", "tue", "wed", "thu", "fri", "sat", "sun"]

function Spark({
  data,
  color,
  bars,
}: {
  data: number[]
  color: string
  bars?: boolean
}) {
  const max = Math.max(...data)
  const min = Math.min(...data)

  if (bars) {
    return (
      <span aria-hidden className="flex h-9 items-end gap-1">
        {data.map((value, index) => (
          <span
            key={days[index]}
            style={{ height: `${(value / max) * 100}%`, background: color }}
            className="w-2 rounded-[3px] opacity-45 last:opacity-100"
          />
        ))}
      </span>
    )
  }

  const points = data
    .map(
      (value, index) =>
        `${((index / (data.length - 1)) * 120).toFixed(1)},${(36 - ((value - min) / Math.max(1, max - min)) * 30 - 3).toFixed(1)}`
    )
    .join(" ")

  return (
    <svg
      aria-hidden
      width="120"
      height="36"
      viewBox="0 0 120 36"
      className="overflow-visible"
    >
      <polyline
        points={points}
        fill="none"
        stroke={color}
        strokeWidth={2.2}
        strokeLinecap="round"
        strokeLinejoin="round"
      />
    </svg>
  )
}

export function DashHealth() {
  const [section, setSection] = React.useState("Summary")
  const [editing, setEditing] = React.useState(false)
  const [pinned, setPinned] = React.useState<string[]>([
    "Move",
    "Steps",
    "Resting Heart Rate",
    "Sleep",
  ])
  const summary = section === "Summary"
  const cards = metrics.filter((metric) =>
    summary
      ? editing || pinned.includes(metric.label)
      : metric.category === section
  )

  return (
    <SidebarProvider>
      <NavSidebar
        items={items}
        value={section}
        onValueChange={(value) => {
          setSection(value)
          setEditing(false)
        }}
        header={
          <p className="px-2 pt-1 text-2xl font-bold tracking-tight">Health</p>
        }
      />
      <SidebarInset className="bg-muted dark:bg-background">
        <div key={section} className="flex flex-col gap-2.5 px-5 py-6 md:px-7">
          <div className="flex items-center gap-2.5">
            <SidebarTrigger className="-ms-2" />
            <h1 className="flex-1 text-3xl font-bold tracking-tight">
              {section}
            </h1>
            <Avatar className="size-8.5">
              <AvatarFallback className="bg-linear-to-br from-[oklch(0.78_0.1_250)] to-[oklch(0.6_0.15_280)] text-white dark:from-[oklch(0.78_0.1_250)] dark:to-[oklch(0.6_0.15_280)]">
                JR
              </AvatarFallback>
            </Avatar>
          </div>
          <div className="mt-2.5 flex items-center gap-2">
            <h2 className="flex-1 text-xl font-bold">
              {summary ? "Pinned" : "Last 7 days"}
            </h2>
            {summary && (
              <Button
                variant="plain"
                size="sm"
                onClick={() => setEditing(!editing)}
                data-editing={editing ? "" : undefined}
                className="text-base data-editing:font-semibold"
              >
                {editing ? "Done" : "Edit"}
              </Button>
            )}
          </div>
          {cards.length > 0 ? (
            <ul className="flex flex-col gap-2.5">
              {cards.map((metric, index) => {
                const category =
                  categories.find((item) => item.value === metric.category) ??
                  categories[0]
                const isPinned = pinned.includes(metric.label)
                return (
                  <li
                    key={metric.label}
                    style={{ transitionDelay: `${index * 40}ms` }}
                    className="flex items-center gap-3 transition-[opacity,translate] duration-800 ease-[cubic-bezier(0.32,0.72,0,1)] starting:translate-y-2 starting:opacity-0 motion-reduce:transition-none"
                  >
                    {editing && (
                      <Button
                        size="icon-xs"
                        aria-label={`${isPinned ? "Unpin" : "Pin"} ${metric.label}`}
                        onClick={() =>
                          setPinned((current) =>
                            isPinned
                              ? current.filter(
                                  (label) => label !== metric.label
                                )
                              : [...current, metric.label]
                          )
                        }
                        data-pinned={isPinned ? "" : undefined}
                        className="relative bg-[color-mix(in_oklab,var(--color-green-500),var(--foreground)_35%)] text-white after:absolute after:-inset-2.5 after:content-[''] hover:bg-[color-mix(in_oklab,var(--color-green-500),var(--foreground)_45%)] data-pinned:bg-destructive data-pinned:hover:bg-destructive/90"
                      >
                        {isPinned ? (
                          <IconPlaceholder
                            lucide="MinusIcon"
                            tabler="IconMinus"
                            hugeicons="MinusSignIcon"
                            phosphor="MinusIcon"
                            remixicon="RiSubtractLine"
                          />
                        ) : (
                          <IconPlaceholder
                            lucide="PlusIcon"
                            tabler="IconPlus"
                            hugeicons="PlusSignIcon"
                            phosphor="PlusIcon"
                            remixicon="RiAddLine"
                          />
                        )}
                      </Button>
                    )}
                    <a
                      href={`#${metric.label.toLowerCase().replaceAll(" ", "-")}`}
                      data-dimmed={editing && !isPinned ? "" : undefined}
                      className="flex min-w-0 flex-1 flex-col gap-2.5 rounded-xl bg-background px-4 py-3 outline-none transition-opacity duration-200 focus-visible:ring-3 focus-visible:ring-ring/50 data-dimmed:opacity-55 dark:bg-muted"
                    >
                      <span
                        className={`flex items-center gap-1.5 text-base font-semibold [&_svg:not([class*='size-'])]:size-4 ${category.text}`}
                      >
                        {category.icon}
                        <span className="flex-1">{metric.label}</span>
                        <span className="text-sm font-normal text-muted-foreground">
                          9:41
                        </span>
                        <IconPlaceholder
                          lucide="ChevronRightIcon"
                          tabler="IconChevronRight"
                          hugeicons="ArrowRight01Icon"
                          phosphor="CaretRightIcon"
                          remixicon="RiArrowRightSLine"
                          className="size-3 text-muted-foreground rtl:rotate-180"
                        />
                      </span>
                      <span className="flex items-end gap-4">
                        <span className="flex-1">
                          <span className="text-3xl font-bold tracking-tight tabular-nums">
                            {metric.value}
                          </span>
                          {metric.unit && (
                            <span className="ms-1 text-base font-semibold text-muted-foreground">
                              {metric.unit}
                            </span>
                          )}
                        </span>
                        <Spark
                          data={metric.data}
                          color={metric.color}
                          bars={metric.bars}
                        />
                      </span>
                    </a>
                  </li>
                )
              })}
            </ul>
          ) : (
            <p className="py-7.5 text-center text-base text-muted-foreground">
              No pinned items. Use Edit to add some.
            </p>
          )}
        </div>
      </SidebarInset>
    </SidebarProvider>
  )
}
```
