# Uptime Bar

A status-page uptime strip: one bar per day tinted by availability, rising in one after another, with a tooltip per day and the period's uptime figure.

**Category:** Charts  
**Exports:** `UptimeBar`  
**npm dependencies:** motion (+ clsx, tailwind-merge for `cn`)

## Install

```bash
npx shadcn@latest add https://charcoalui.com/r/uptime-bar.json
```
The file lands at `components/ui/uptime-bar.tsx`; import from `@/components/ui/uptime-bar`.

## API — Uptime Bar
| Prop | Type | Default | Notes |
| --- | --- | --- | --- |
| `name` | `string` | `"API"` |  |
| `days` | `{ label: string; uptime: number; note?: string }[]` | — |  |

## Example — Uptime Bar

```tsx
import { UptimeBar } from "@/components/ui/uptime-bar";

const days = Array.from({ length: 60 }, (_, i) => {
  const d = new Date(2026, 7, 1 + i);
  const uptime = i === 17 ? 96.4 : i === 41 ? 99.2 : i === 52 ? 88.1 : 100;
  return { label: d.toLocaleDateString("en-US", { month: "short", day: "numeric" }), uptime, note: uptime < 100 ? (uptime < 90 ? "Major outage: database failover" : "Degraded: elevated error rate") : undefined };
});

export default function UptimeBarDemo() {
  return (
    <div className="grid w-full max-w-xl gap-6">
      <UptimeBar name="API" days={days} />
      <UptimeBar name="Dashboard" days={days.map((d) => ({ ...d, uptime: 100, note: undefined }))} />
    </div>
  );
}
```

## Source — components/ui/uptime-bar.tsx

```tsx
"use client";

import * as React from "react";
import { AnimatePresence, motion, useReducedMotion } from "motion/react";
import { cn } from "@/lib/utils";

export type UptimeDay = { label: string; /** Percent up, 0 to 100. */ uptime: number; note?: string };

export type UptimeBarProps = {
  name?: string;
  days: UptimeDay[];
  className?: string;
};

const tone = (u: number) => (u >= 99.9 ? "bg-success" : u >= 98 ? "bg-[color-mix(in_oklab,var(--color-success)_45%,#eab308)]" : u >= 90 ? "bg-[#eab308]" : "bg-destructive");

/** A status-page uptime strip: one rounded bar per day, tinted by availability, rising in one by one, with a tooltip per day and the period's overall figure. */
export function UptimeBar({ name = "API", days, className }: UptimeBarProps) {
  const reduce = useReducedMotion();
  const [hover, setHover] = React.useState<number | null>(null);
  const avg = days.reduce((s, d) => s + d.uptime, 0) / (days.length || 1);
  return (
    <div className={cn("w-full max-w-xl", className)}>
      <div className="flex items-baseline justify-between text-sm">
        <span className="font-medium">{name}</span>
        <span className="font-mono text-xs tabular-nums text-muted-foreground">{avg.toFixed(2)}% uptime</span>
      </div>
      <div className="relative mt-2">
        <ul className="flex h-9 items-stretch gap-[3px]" onPointerLeave={() => setHover(null)}>
          {days.map((d, i) => (
            <li key={d.label} className="flex flex-1">
              <motion.button
                type="button"
                aria-label={`${d.label}: ${d.uptime.toFixed(2)}% uptime${d.note ? `, ${d.note}` : ""}`}
                onPointerEnter={() => setHover(i)}
                onFocus={() => setHover(i)}
                onBlur={() => setHover(null)}
                initial={reduce ? false : { scaleY: 0 }}
                animate={{ scaleY: hover === i ? 1.12 : 1 }}
                transition={{ delay: reduce ? 0 : i * 0.012, type: "spring", stiffness: 380, damping: 26 }}
                className={cn("w-full origin-bottom cursor-default rounded-[3px] outline-none focus-visible:ring-2 focus-visible:ring-ring", tone(d.uptime), hover !== null && hover !== i && "opacity-60")}
              />
            </li>
          ))}
        </ul>
        <AnimatePresence>
          {hover !== null && (
            <motion.div
              role="status"
              initial={{ opacity: 0, y: 4 }}
              animate={{ opacity: 1, y: 0 }}
              exit={{ opacity: 0 }}
              className="pointer-events-none absolute top-11 z-10 w-48 -translate-x-1/2 rounded-lg border bg-popover px-3 py-2 text-xs text-popover-foreground shadow-lg"
              style={{ left: `${Math.min(88, Math.max(12, ((hover + 0.5) / days.length) * 100))}%` }}
            >
              <p className="font-medium">{days[hover].label}</p>
              <p className="font-mono tabular-nums text-muted-foreground">{days[hover].uptime.toFixed(2)}% uptime</p>
              {days[hover].note && <p className="mt-1">{days[hover].note}</p>}
            </motion.div>
          )}
        </AnimatePresence>
      </div>
      <div className="mt-2 flex justify-between font-mono text-[10px] text-muted-foreground">
        <span>{days[0]?.label}</span>
        <span>{days[days.length - 1]?.label}</span>
      </div>
    </div>
  );
}
```