- Accordion
- Agent Thinking
- Alert
- Alert Dialog
- Announcement
- Aspect Ratio
- Attachment
- Avatar
- Badge
- Breadcrumb
- Bubble
- Button
- Button Group
- Calendar
- Card
- Carousel
- Checkbox
- Checkbox Card
- Code Block
- Collapsible
- Color Picker
- Combobox
- Command
- Composer
- Composer Loader
- Context Menu
- Data Grid
- Data Table
- Date Picker
- Dialog
- Direction
- Drawer
- Dropdown Menu
- Empty
- Field
- File Upload
- Hover Card
- Input
- Input Group
- Input Otp
- Item
- Kbd
- Label
- Marker
- Menubar
- Message
- Message Scroller
- Meter
- Native Select
- Navigation Menu
- Number Field
- Pagination
- Period Switcher
- Popover
- Progress
- Questionnaire
- Radio Card
- Radio Group
- Resizable
- Reveal
- Scroll Area
- Search Field
- Select
- Separator
- Sheet
- Sidebar
- Skeleton
- Slider
- Social Button
- Sonner
- Spinner
- Status Dot
- Switch
- Switch Card
- Table
- Tabs
- Tag Group
- Textarea
- Theme Toggle
- Time Field
- Toggle
- Toggle Group
- Tooltip
- Trace
- Tree View
- Agent Creator 01
- Agent Limits Card 01
- Agent Progress 01
- AI Chat 01
- AI Image Generation 01
- AI Profile 01
- Alerts Card 01
- Auth Card 01
- Calendar 01
- Composer 01
- Composer Attachments 01
- Composer Panel 01
- Finance Dashboard 01
- Home Dashboard 01
- Marketing Dashboard 01
- Medical Profile 01
- Meeting Scheduler 01
- Multi Agent Chat 01
- Notification Center 01
- Patient Info Card 01
- Project Board 01
- Questionnaire 01
- Settings Modal 01
- Sidebar 01
- Stat Cards 01
- Task List 01
- Web Search 01
- Activity Rings Card 01
- Area Chart Card 01
- Bar List Card 01
- Combo Chart Card 01
- Contributions Card 01
- Earnings Chart Card 01
- Funnel Chart Card 01
- Heatmap Chart Card 01
- Line Chart Card 01
- Most Active Days Card 01
- Orders Chart Card 01
- Radar Chart Card 01
- Radial Chart Card 01
- Revenue Chart Card 01
- Sankey Chart Card 01
- Scatter Chart Card 01
- Sleep Score Card 01
- Stage Bars Card 01
- Steps Card 01
Customer journey
2,400
+3.6%Discover
2,400
Explore
1,680
Subscribe
1,120
Engage
720
Purchase
360
Return
160
Customer journey
2,400
+3.6%Discover
2,400
Explore
1,680
Subscribe
1,120
Engage
720
Purchase
360
Return
160
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>
);
} On this page