For the complete documentation index, see llms.txt. Markdown variants are available by appending .md to documentation URLs. Homepage markdown is available at /llms.md/content.md. An agent skill is available at /.well-known/agent-skills/site-skill.md.

Stage Bars Card 01

Compare stages of a journey with colored or monochrome bars.

Pass ranges with a label and an array of stages. Each stage has a label and value, and can also have a color and a Remix icon. The first stage is the baseline for percentages. Use mono for a single-color chart or showIcons={false} to hide icons. Pass the preview theme to popupClassName when the menu is rendered outside a scoped theme.

Installation

Set up the @cobalt registry as described in Installation, then add the block:

$ pnpm dlx shadcn@latest add @cobalt/stage-bars-card-01

Source

"use client";

import { RiCalendarLine, RiArrowDownSLine } from "@remixicon/react";
import type { RemixiconComponentType } from "@remixicon/react";
import { cn } from "cn";
import * as React from "react";

import { useCountUp } from "@/components/hooks/use-count-up";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import {
  Card,
  CardAction,
  CardContent,
  CardHeader,
} from "@/components/ui/card";
import {
  DropdownMenu,
  DropdownMenuContent,
  DropdownMenuGroup,
  DropdownMenuItem,
  DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import { Reveal } from "@/components/ui/reveal";

export interface StageBar {
  label: string;
  value: number;
  color?: string;
  icon?: RemixiconComponentType;
}

export interface StageBarRange {
  label: string;
  stages: StageBar[];
  delta?: number;
}

const colors = [
  "var(--settings-art-lime, var(--chart-2))",
  "var(--settings-art-blue, var(--primary))",
  "var(--chart-5)",
  "var(--chart-3)",
  "var(--status-dot-yellow-dot, var(--chart-2))",
  "var(--chart-1)",
];

const format = (value: number) => value.toLocaleString("en-US");

export function StageBarsCard({
  title = "Journey",
  ranges,
  mono = false,
  showIcons = true,
  popupClassName,
  className,
}: {
  title?: string;
  ranges: StageBarRange[];
  mono?: boolean;
  showIcons?: boolean;
  popupClassName?: string;
  className?: string;
}) {
  const [rangeIndex, setRangeIndex] = React.useState(0);
  const [active, setActive] = React.useState<number | null>(null);
  const range = ranges[rangeIndex] ?? ranges[0];
  const stages = range?.stages ?? [];
  const baseline = Math.max(0, stages[0]?.value ?? 0);
  const highlighted = active === null ? undefined : stages[active];
  const total = highlighted?.value ?? baseline;
  const totalRef = useCountUp<HTMLParagraphElement>(total, { format });

  return (
    <Reveal
      variant="card"
      render={
        <Card
          radius={14}
          role="region"
          aria-label={title}
          variant="muted"
          size="flush"
          className={cn("@container w-full max-w-100 min-w-0", className)}
        />
      }
    >
      <CardHeader className="mx-4 mt-4">
        <div className="min-w-0">
          <p className="truncate text-sm leading-5 font-medium text-muted-foreground">
            {highlighted?.label ?? title}
          </p>
          <div className="flex items-center gap-2">
            <p
              ref={totalRef}
              className="text-2xl leading-8 font-medium tabular-nums"
            >
              {format(total)}
            </p>
            {active === null && range?.delta != null && (
              <Badge variant="label" tone={range.delta >= 0 ? "lime" : "rose"}>
                {range.delta > 0 ? "+" : ""}
                {(range.delta * 100).toFixed(1)}%
              </Badge>
            )}
          </div>
        </div>
        {ranges.length > 1 && (
          <CardAction>
            <DropdownMenu>
              <DropdownMenuTrigger
                render={<Button variant="outline" size="sm" />}
              >
                <RiCalendarLine data-icon="inline-start" />
                {range?.label}
                <RiArrowDownSLine
                  data-icon="inline-end"
                  className="transition-transform duration-150 ease-out group-aria-expanded/button:rotate-180 motion-reduce:transition-none"
                />
              </DropdownMenuTrigger>
              <DropdownMenuContent
                align="end"
                className={cn("w-40", popupClassName)}
              >
                <DropdownMenuGroup>
                  {ranges.map((item, index) => (
                    <DropdownMenuItem
                      key={item.label}
                      onClick={() => {
                        setRangeIndex(index);
                        setActive(null);
                      }}
                    >
                      {item.label}
                    </DropdownMenuItem>
                  ))}
                </DropdownMenuGroup>
              </DropdownMenuContent>
            </DropdownMenu>
          </CardAction>
        )}
      </CardHeader>
      <CardContent
        className="mx-4 mt-5 gap-2"
        onMouseLeave={() => setActive(null)}
      >
        {stages.map((stage, index) => {
          const percent =
            baseline > 0
              ? Math.max(0, Math.min(100, (stage.value / baseline) * 100))
              : 0;
          const Icon = stage.icon;
          const withIcon = showIcons && Icon && percent > 0;
          const color = mono
            ? "var(--muted-foreground)"
            : (stage.color ?? colors[index % colors.length]);
          return (
            <Button
              key={`${stage.label}-${index}`}
              variant="unstyled"
              radius={4}
              type="button"
              onMouseEnter={() => setActive(index)}
              onFocus={() => setActive(index)}
              onBlur={() => setActive(null)}
              className="grid w-full grid-cols-[minmax(0,1fr)_minmax(0,2fr)_minmax(0,76px)] items-center gap-2 whitespace-normal"
            >
              <span className="min-w-0 text-right text-xs font-normal text-muted-foreground [overflow-wrap:anywhere]">
                {stage.label}
              </span>
              {/*
                The bar is a full-width fill slid left inside a clipping track,
                so a change only moves it (translate, composited) and never
                re-lays out the row. Its round end keeps its shape.
              */}
              <span className="relative h-[18px] overflow-hidden rounded-full bg-surface-strong">
                <span
                  className={cn(
                    "stage-bars-card-01-draw absolute inset-0 translate-x-(--bar-translate) rounded-full bg-(--bar-color) transition-all duration-450 ease-out after:absolute after:inset-0 after:rounded-full after:bg-black/10 after:opacity-0 after:transition-opacity after:duration-150 motion-reduce:transition-none motion-reduce:after:transition-none",
                    active !== null && active !== index && "opacity-50",
                    active === index && "after:opacity-100"
                  )}
                  style={
                    {
                      "--bar-translate": `calc(${withIcon ? `max(${percent}%, 20px)` : `${percent}%`} - 100%)`,
                      "--bar-color": color,
                      "--delay": `${index * 40}ms`,
                    } as React.CSSProperties
                  }
                />
                {withIcon && (
                  <Icon
                    aria-hidden
                    className="absolute top-1/2 left-1 size-3 -translate-y-1/2 text-(color:--alerts-icon-dark,var(--foreground))"
                  />
                )}
              </span>
              <span className="min-w-0 text-right text-xs font-medium tabular-nums [overflow-wrap:anywhere]">
                {format(stage.value)}{" "}
                <span className="text-xs font-normal text-muted-foreground @max-[300px]:hidden">
                  {Math.round(percent)}%
                </span>
              </span>
            </Button>
          );
        })}
      </CardContent>
      <div className="mx-2 mt-5 mb-2 grid grid-cols-3 gap-1.5">
        {stages.map((stage, index) => (
          <div
            key={`${stage.label}-${index}`}
            className="min-w-0 rounded-[8px] bg-(--stat-footer-surface,var(--background)) px-2 py-1.5"
          >
            <div className="flex items-center gap-1.5 text-xs leading-4 text-muted-foreground">
              {!mono && (
                <svg
                  viewBox="0 0 10 10"
                  aria-hidden
                  className="size-2.5 shrink-0"
                >
                  <rect
                    width="10"
                    height="10"
                    rx="3"
                    fill={stage.color ?? colors[index % colors.length]}
                  />
                </svg>
              )}
              <span className="min-w-0 [overflow-wrap:anywhere]">
                {stage.label}
              </span>
            </div>
            <p className="text-xs leading-4 font-medium tabular-nums [overflow-wrap:anywhere]">
              {format(stage.value)}
            </p>
          </div>
        ))}
      </div>
    </Reveal>
  );
}