Meter
Free, copy-and-go Meter components built on the SevenUI Meter primitive.Read the primitive docs.
"use client";
import { cn } from "cn";
import { Meter, MeterLabel, MeterValue } from "@/components/ui/meter";
const sizes = [
{
size: "Small",
label: "Build minutes",
value: 62,
className: "gap-1.5 [&>div]:h-1",
text: "text-xs",
},
{
size: "Default",
label: "Bandwidth this cycle",
value: 48,
className: "",
text: "text-sm",
},
{
size: "Large",
label: "Storage used",
value: 81,
className: "gap-2.5 [&>div]:h-3",
text: "text-base",
},
];
export default function Meter01() {
return (
<div className="flex w-full max-w-sm flex-col gap-6">
{sizes.map((item) => (
<div key={item.size} className="flex flex-col gap-1">
<span className="text-xs text-muted-foreground">{item.size}</span>
<Meter
value={item.value}
locale="en-US"
className={cn("grid-cols-[minmax(0,1fr)_auto] gap-x-4", item.className)}
>
<MeterLabel className={item.text}>{item.label}</MeterLabel>
<MeterValue className={cn("text-right tabular-nums", item.text)} />
</Meter>
</div>
))}
</div>
);
}
npx shadcn@latest add @sevenui/component/meter-01pnpm dlx shadcn@latest add @sevenui/component/meter-01yarn dlx shadcn@latest add @sevenui/component/meter-01bunx --bun shadcn@latest add @sevenui/component/meter-01Today's intake
Logged at 6:40 PM- Protein
- Fiber
- Carbs
- Water
"use client";
import { Meter, MeterLabel, MeterValue } from "@/components/ui/meter";
const nutrients = [
{ name: "Protein", value: 86, max: 120, unit: "g" },
{ name: "Fiber", value: 18, max: 30, unit: "g" },
{ name: "Carbs", value: 164, max: 250, unit: "g" },
{ name: "Water", value: 1.6, max: 2.5, unit: "L" },
];
export default function Meter02() {
return (
<section
aria-labelledby="meter-02-title"
className="w-full max-w-md rounded-lg border border-border"
>
<header className="flex items-baseline justify-between gap-4 border-b border-border px-4 py-3">
<h3 id="meter-02-title" className="text-sm font-medium">
Today's intake
</h3>
<span className="text-xs text-muted-foreground">Logged at 6:40 PM</span>
</header>
<ul className="divide-y divide-border">
{nutrients.map((nutrient) => (
<li key={nutrient.name} className="px-4 py-3">
<Meter
value={nutrient.value}
max={nutrient.max}
getAriaValueText={() =>
`${nutrient.value} of ${nutrient.max} ${nutrient.unit} daily target`
}
className="grid-cols-[4.5rem_minmax(0,1fr)_5.5rem] items-center gap-3 [&>div]:col-span-1 [&>div]:col-start-2 [&>div]:row-start-1 [&>div]:h-1.5"
>
<MeterLabel className="col-start-1 row-start-1 truncate font-normal">
{nutrient.name}
</MeterLabel>
<MeterValue className="col-start-3 row-start-1 text-right whitespace-nowrap text-xs tabular-nums">
{() => `${nutrient.value} / ${nutrient.max} ${nutrient.unit}`}
</MeterValue>
</Meter>
</li>
))}
</ul>
</section>
);
}
npx shadcn@latest add @sevenui/component/meter-02pnpm dlx shadcn@latest add @sevenui/component/meter-02yarn dlx shadcn@latest add @sevenui/component/meter-02bunx --bun shadcn@latest add @sevenui/component/meter-02Campaign budget resets on October 1.
Uploads pause when the library reaches its limit.
Counted per calendar month across all keys.
"use client";
import { Meter, MeterLabel, MeterValue } from "@/components/ui/meter";
const meters: {
label: string;
value: number;
max: number;
format: Intl.NumberFormatOptions;
note: string;
}[] = [
{
label: "Ad spend",
value: 3420,
max: 5000,
format: { style: "currency", currency: "USD", maximumFractionDigits: 0 },
note: "Campaign budget resets on October 1.",
},
{
label: "Media library",
value: 18.4,
max: 25,
format: { style: "unit", unit: "gigabyte", maximumFractionDigits: 1 },
note: "Uploads pause when the library reaches its limit.",
},
{
label: "API requests",
value: 842000,
max: 1000000,
format: { notation: "compact", maximumFractionDigits: 1 },
note: "Counted per calendar month across all keys.",
},
];
export default function Meter03() {
return (
<div className="flex w-full max-w-sm flex-col gap-6">
{meters.map((meter) => {
const formattedMax = new Intl.NumberFormat(
"en-US",
meter.format,
).format(meter.max);
return (
<div key={meter.label} className="flex flex-col gap-1.5">
<Meter
value={meter.value}
max={meter.max}
format={meter.format}
locale="en-US"
getAriaValueText={(formatted) =>
`${formatted} of ${formattedMax}`
}
className="grid-cols-[1fr_auto]"
>
<MeterLabel>{meter.label}</MeterLabel>
<MeterValue className="text-right tabular-nums">
{(formatted) => (
<>
<span className="font-medium text-foreground">
{formatted}
</span>{" "}
of {formattedMax}
</>
)}
</MeterValue>
</Meter>
<p className="text-xs text-muted-foreground">{meter.note}</p>
</div>
);
})}
</div>
);
}
npx shadcn@latest add @sevenui/component/meter-03pnpm dlx shadcn@latest add @sevenui/component/meter-03yarn dlx shadcn@latest add @sevenui/component/meter-03bunx --bun shadcn@latest add @sevenui/component/meter-03Within normal range
Approaching the 85% alert threshold
Above 90% — writes may start failing
"use client";
import { CircleCheck, OctagonAlert, TriangleAlert } from "lucide-react";
import { cn } from "cn";
import { Meter, MeterLabel, MeterValue } from "@/components/ui/meter";
const levels = {
healthy: {
icon: CircleCheck,
text: "text-success",
meter: "[&>div]:bg-success/15 [&>div>div]:bg-success",
message: "Within normal range",
},
warning: {
icon: TriangleAlert,
text: "text-warning",
meter: "[&>div]:bg-warning/20 [&>div>div]:bg-warning",
message: "Approaching the 85% alert threshold",
},
critical: {
icon: OctagonAlert,
text: "text-destructive",
meter: "[&>div]:bg-destructive/15 [&>div>div]:bg-destructive",
message: "Above 90% — writes may start failing",
},
};
const resources = [
{ id: "cpu", label: "CPU", value: 34 },
{ id: "memory", label: "Memory", value: 78 },
{ id: "disk", label: "Disk", value: 94 },
];
function getLevel(value: number) {
if (value >= 90) return levels.critical;
if (value >= 70) return levels.warning;
return levels.healthy;
}
export default function Meter04() {
return (
<div className="flex w-full max-w-sm flex-col gap-5">
{resources.map((resource) => {
const level = getLevel(resource.value);
const Icon = level.icon;
const messageId = `meter-04-${resource.id}-status`;
return (
<div key={resource.id} className="flex flex-col gap-1.5">
<Meter
value={resource.value}
locale="en-US"
aria-describedby={messageId}
className={cn("grid-cols-2", level.meter)}
>
<MeterLabel>{resource.label}</MeterLabel>
<MeterValue className="text-right tabular-nums" />
</Meter>
<p
id={messageId}
className="flex items-center gap-1.5 text-xs text-muted-foreground"
>
<Icon aria-hidden="true" className={cn("size-3.5", level.text)} />
{level.message}
</p>
</div>
);
})}
</div>
);
}
npx shadcn@latest add @sevenui/component/meter-04pnpm dlx shadcn@latest add @sevenui/component/meter-04yarn dlx shadcn@latest add @sevenui/component/meter-04bunx --bun shadcn@latest add @sevenui/component/meter-04Harbor Street Roasters
Every 10th drink is on us.
3 more drinks until a free one.
"use client";
import * as React from "react";
import { Coffee, Gift } from "lucide-react";
import { cn } from "cn";
import { Button } from "@/components/ui/button";
import { Meter, MeterLabel, MeterValue } from "@/components/ui/meter";
const STAMPS_FOR_REWARD = 10;
export default function Meter05() {
const [stamps, setStamps] = React.useState(7);
const [rewards, setRewards] = React.useState(0);
const complete = stamps >= STAMPS_FOR_REWARD;
const remaining = STAMPS_FOR_REWARD - stamps;
return (
<div className="flex w-full max-w-sm flex-col gap-4 rounded-xl border bg-card p-4 text-card-foreground">
<div className="flex items-start justify-between gap-3">
<div className="min-w-0">
<h3 className="font-medium">Harbor Street Roasters</h3>
<p className="text-sm text-muted-foreground">
Every 10th drink is on us.
</p>
</div>
{rewards > 0 ? (
<span className="shrink-0 rounded-full bg-muted px-2 py-0.5 text-xs tabular-nums">
{rewards} redeemed
</span>
) : null}
</div>
<Meter
value={stamps}
max={STAMPS_FOR_REWARD}
getAriaValueText={(_, value) =>
`${value} of ${STAMPS_FOR_REWARD} stamps collected`
}
className="grid-cols-[1fr_auto] gap-3 [&>div:last-of-type]:hidden"
>
<MeterLabel>Loyalty stamps</MeterLabel>
<MeterValue className="text-right tabular-nums">
{(_, value) => `${value} / ${STAMPS_FOR_REWARD}`}
</MeterValue>
<div aria-hidden="true" className="col-span-full grid grid-cols-5 gap-2">
{Array.from({ length: STAMPS_FOR_REWARD }, (_, index) => {
const filled = index < stamps;
const last = index === STAMPS_FOR_REWARD - 1;
const Icon = last ? Gift : Coffee;
return (
<span
// Stamps are positional and never reorder.
// biome-ignore lint/suspicious/noArrayIndexKey: static slots
key={index}
className={cn(
"flex aspect-square items-center justify-center rounded-full border transition-colors duration-300",
filled
? "border-primary bg-primary text-primary-foreground"
: "border-dashed border-border text-muted-foreground/60",
)}
>
<Icon className="size-4" />
</span>
);
})}
</div>
</Meter>
<div className="flex flex-wrap items-center justify-between gap-3">
<p aria-live="polite" className="text-sm text-muted-foreground">
{complete
? "Your next drink is free."
: `${remaining} more ${remaining === 1 ? "drink" : "drinks"} until a free one.`}
</p>
{complete ? (
<Button
size="sm"
onClick={() => {
setStamps(0);
setRewards((current) => current + 1);
}}
>
<Gift aria-hidden="true" />
Redeem reward
</Button>
) : (
<Button
size="sm"
variant="outline"
onClick={() => setStamps((current) => current + 1)}
>
<Coffee aria-hidden="true" />
Add stamp
</Button>
)}
</div>
</div>
);
}
npx shadcn@latest add @sevenui/component/meter-05pnpm dlx shadcn@latest add @sevenui/component/meter-05yarn dlx shadcn@latest add @sevenui/component/meter-05bunx --bun shadcn@latest add @sevenui/component/meter-05- Photos62.4 GB
- Backups41.7 GB
- Documents28.1 GB
- Other9.3 GB
"use client";
import { Meter, MeterLabel, MeterValue } from "@/components/ui/meter";
const capacity = 200;
const categories = [
{ name: "Photos", value: 62.4, color: "bg-chart-1" },
{ name: "Backups", value: 41.7, color: "bg-chart-2" },
{ name: "Documents", value: 28.1, color: "bg-chart-3" },
{ name: "Other", value: 9.3, color: "bg-chart-4" },
];
const gigabytes: Intl.NumberFormatOptions = {
style: "unit",
unit: "gigabyte",
maximumFractionDigits: 1,
};
const formatter = new Intl.NumberFormat("en-US", gigabytes);
export default function Meter06() {
const used = categories.reduce(
(total, category) => total + category.value,
0,
);
return (
<div className="flex w-full max-w-sm flex-col gap-4">
<Meter
value={used}
max={capacity}
format={gigabytes}
locale="en-US"
getAriaValueText={(formatted) =>
`${formatted} of ${formatter.format(capacity)}`
}
className="grid-cols-[1fr_auto] [&>div:last-of-type]:hidden"
>
<MeterLabel>Cloud storage</MeterLabel>
<MeterValue className="text-right tabular-nums">
{(formatted) => `${formatted} of ${formatter.format(capacity)}`}
</MeterValue>
<div
aria-hidden="true"
className="col-span-full flex h-2.5 gap-0.5 overflow-hidden rounded-full bg-muted"
>
{categories.map((category) => (
<span
key={category.name}
className={`h-full ${category.color}`}
style={{ width: `${(category.value / capacity) * 100}%` }}
/>
))}
</div>
</Meter>
<ul className="grid grid-cols-1 gap-x-6 sm:grid-cols-2 gap-y-2 text-sm">
{categories.map((category) => (
<li key={category.name} className="flex items-center gap-2">
<span
aria-hidden="true"
className={`size-2 shrink-0 rounded-full ${category.color}`}
/>
<span className="truncate">{category.name}</span>
<span className="ml-auto whitespace-nowrap text-muted-foreground tabular-nums">
{formatter.format(category.value)}
</span>
</li>
))}
</ul>
</div>
);
}
npx shadcn@latest add @sevenui/component/meter-06pnpm dlx shadcn@latest add @sevenui/component/meter-06yarn dlx shadcn@latest add @sevenui/component/meter-06bunx --bun shadcn@latest add @sevenui/component/meter-06$660 left · 74% used
"use client";
import * as React from "react";
import { cn } from "cn";
import { Meter, MeterLabel, MeterValue } from "@/components/ui/meter";
import { Slider } from "@/components/ui/slider";
const spend = 1840;
const currency: Intl.NumberFormatOptions = {
style: "currency",
currency: "USD",
maximumFractionDigits: 0,
};
const formatter = new Intl.NumberFormat("en-US", currency);
export default function Meter07() {
const [budget, setBudget] = React.useState(2500);
const ratio = spend / budget;
const overBudget = spend > budget;
return (
<div className="flex w-full max-w-sm flex-col gap-6">
<div className="flex flex-col gap-1.5">
<Meter
value={Math.min(spend, budget)}
max={budget}
format={currency}
locale="en-US"
getAriaValueText={() =>
`${formatter.format(spend)} spent of ${formatter.format(budget)} budget`
}
aria-describedby="meter-07-status"
className={cn(
"grid-cols-[1fr_auto]",
ratio >= 0.9 &&
"[&>div]:bg-destructive/15 [&>div>div]:bg-destructive",
ratio >= 0.75 &&
ratio < 0.9 &&
"[&>div]:bg-warning/20 [&>div>div]:bg-warning",
)}
>
<MeterLabel>September cloud spend</MeterLabel>
<MeterValue className="text-right tabular-nums">
{() => `${formatter.format(spend)} / ${formatter.format(budget)}`}
</MeterValue>
</Meter>
<p
id="meter-07-status"
aria-live="polite"
className={cn(
"text-xs",
overBudget ? "text-destructive" : "text-muted-foreground",
)}
>
{overBudget
? `Over budget by ${formatter.format(spend - budget)}`
: `${formatter.format(budget - spend)} left · ${Math.round(ratio * 100)}% used`}
</p>
</div>
<div className="flex flex-col gap-3 rounded-lg bg-muted/50 p-4">
<div className="flex items-center justify-between gap-4 text-sm">
<span id="meter-07-budget" className="font-medium">
Monthly budget
</span>
<span className="text-muted-foreground tabular-nums">
{formatter.format(budget)}
</span>
</div>
<Slider
value={[budget]}
min={1000}
max={5000}
step={100}
aria-labelledby="meter-07-budget"
onValueChange={(next) =>
setBudget(Array.isArray(next) ? next[0] : next)
}
/>
</div>
</div>
);
}
npx shadcn@latest add @sevenui/component/meter-07pnpm dlx shadcn@latest add @sevenui/component/meter-07yarn dlx shadcn@latest add @sevenui/component/meter-07bunx --bun shadcn@latest add @sevenui/component/meter-07"use client";
import * as React from "react";
import { CloudOff, HardDrive, RotateCw, Upload } from "lucide-react";
import { Button } from "@/components/ui/button";
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import { Meter, MeterLabel, MeterValue } from "@/components/ui/meter";
import { Skeleton } from "@/components/ui/skeleton";
import { ToggleGroup, ToggleGroupItem } from "@/components/ui/toggle-group";
type Status = "ready" | "loading" | "empty" | "error";
const states: { value: Status; label: string }[] = [
{ value: "ready", label: "Ready" },
{ value: "loading", label: "Loading" },
{ value: "empty", label: "Empty" },
{ value: "error", label: "Error" },
];
const storage: Intl.NumberFormatOptions = {
style: "unit",
unit: "gigabyte",
maximumFractionDigits: 1,
};
export default function Meter08() {
const [status, setStatus] = React.useState<Status>("ready");
React.useEffect(() => {
if (status !== "loading") return;
const timeout = window.setTimeout(() => setStatus("ready"), 1500);
return () => window.clearTimeout(timeout);
}, [status]);
return (
<div className="flex w-full max-w-sm flex-col items-center gap-4">
<ToggleGroup
variant="outline"
size="sm"
spacing={0}
aria-label="Preview state"
value={[status]}
onValueChange={(next) => {
if (next[0]) setStatus(next[0] as Status);
}}
>
{states.map((state) => (
<ToggleGroupItem key={state.value} value={state.value}>
{state.label}
</ToggleGroupItem>
))}
</ToggleGroup>
<Card className="w-full">
<CardHeader>
<CardTitle className="flex items-center gap-2">
<HardDrive
aria-hidden="true"
className="size-4 text-muted-foreground"
/>
Workspace storage
</CardTitle>
<CardDescription>Shared across 12 members</CardDescription>
</CardHeader>
<CardContent aria-live="polite" aria-busy={status === "loading"}>
{status === "ready" && (
<Meter
value={34.2}
max={50}
format={storage}
locale="en-US"
className="grid-cols-[1fr_auto]"
>
<MeterLabel>Used</MeterLabel>
<MeterValue className="text-right tabular-nums">
{(formatted) => `${formatted} of 50 GB`}
</MeterValue>
</Meter>
)}
{status === "loading" && (
<div className="grid gap-2">
<span className="sr-only">Loading storage usage</span>
<div className="flex justify-between">
<Skeleton className="h-4 w-12" />
<Skeleton className="h-4 w-24" />
</div>
<Skeleton className="h-2 w-full rounded-full" />
</div>
)}
{status === "empty" && (
<div className="grid gap-3">
<Meter
value={0}
max={50}
format={storage}
locale="en-US"
className="grid-cols-[1fr_auto]"
>
<MeterLabel>Used</MeterLabel>
<MeterValue className="text-right tabular-nums">
{() => "0 of 50 GB"}
</MeterValue>
</Meter>
<div className="flex flex-wrap items-center justify-between gap-3">
<p className="text-xs text-muted-foreground">
No files uploaded yet.
</p>
<Button
size="sm"
variant="outline"
onClick={() => setStatus("loading")}
>
<Upload aria-hidden="true" />
Upload files
</Button>
</div>
</div>
)}
{status === "error" && (
<div
role="alert"
className="flex flex-col items-start gap-3 rounded-md border border-destructive/30 bg-destructive/5 p-3"
>
<div className="flex gap-2 text-sm">
<CloudOff
aria-hidden="true"
className="mt-0.5 size-4 shrink-0 text-destructive"
/>
<div className="grid gap-0.5">
<p className="font-medium text-destructive">
Couldn't load storage usage
</p>
<p className="text-xs text-muted-foreground">
The usage service timed out. Your files are not affected.
</p>
</div>
</div>
<Button
size="sm"
variant="outline"
onClick={() => setStatus("loading")}
>
<RotateCw aria-hidden="true" />
Try again
</Button>
</div>
)}
</CardContent>
</Card>
</div>
);
}
npx shadcn@latest add @sevenui/component/meter-08pnpm dlx shadcn@latest add @sevenui/component/meter-08yarn dlx shadcn@latest add @sevenui/component/meter-08bunx --bun shadcn@latest add @sevenui/component/meter-08Team members
Seats are billed at $12 per month each.
1 open seat left.
- MCOwner
Maya Chen
maya.chen@northwind.io
- DOAdmin
Daniel Ortiz
daniel.ortiz@northwind.io
- PNMember
Priya Nair
priya.nair@northwind.io
- TBMember
Tom Becker
tom.becker@northwind.io
"use client";
import { UserMinus } from "lucide-react";
import * as React from "react";
import { Avatar, AvatarFallback } from "@/components/ui/avatar";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Meter, MeterLabel, MeterValue } from "@/components/ui/meter";
const initialMembers = [
{ email: "maya.chen@northwind.io", name: "Maya Chen", role: "Owner" },
{ email: "daniel.ortiz@northwind.io", name: "Daniel Ortiz", role: "Admin" },
{ email: "priya.nair@northwind.io", name: "Priya Nair", role: "Member" },
{ email: "tom.becker@northwind.io", name: "Tom Becker", role: "Member" },
];
function initials(name: string) {
return name
.split(" ")
.map((part) => part[0])
.join("");
}
function nameFromEmail(email: string) {
return email
.split("@")[0]
.split(/[._-]/)
.filter(Boolean)
.map((part) => part[0].toUpperCase() + part.slice(1))
.join(" ");
}
export default function Meter09() {
const [members, setMembers] = React.useState(initialMembers);
const [seats, setSeats] = React.useState(5);
const [email, setEmail] = React.useState("");
const full = members.length >= seats;
const validEmail = /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email.trim());
const duplicate = members.some(
(member) => member.email === email.trim().toLowerCase(),
);
function invite(event: React.FormEvent<HTMLFormElement>) {
event.preventDefault();
if (full || !validEmail || duplicate) return;
const address = email.trim().toLowerCase();
setMembers((current) => [
...current,
{ email: address, name: nameFromEmail(address), role: "Invited" },
]);
setEmail("");
}
return (
<section
aria-labelledby="meter-09-title"
className="w-full max-w-md rounded-xl border bg-card p-4 text-card-foreground"
>
<div className="flex flex-wrap items-start justify-between gap-3">
<div>
<h3 id="meter-09-title" className="font-medium">
Team members
</h3>
<p className="text-sm text-muted-foreground">
Seats are billed at $12 per month each.
</p>
</div>
<Button
variant="outline"
size="sm"
onClick={() => setSeats((current) => current + 5)}
>
Add 5 seats
</Button>
</div>
<Meter
value={members.length}
max={seats}
getAriaValueText={() => `${members.length} of ${seats} seats used`}
className={
full
? "mt-4 grid-cols-[1fr_auto] [&>div:last-of-type]:bg-warning/20 [&>div:last-of-type>div]:bg-warning"
: "mt-4 grid-cols-[1fr_auto]"
}
>
<MeterLabel>Seats used</MeterLabel>
<MeterValue className="tabular-nums">
{() => `${members.length} of ${seats}`}
</MeterValue>
</Meter>
<form onSubmit={invite} className="mt-4 flex gap-2">
<Input
type="email"
aria-label="Email address to invite"
placeholder="name@northwind.io"
value={email}
disabled={full}
onChange={(event) => setEmail(event.target.value)}
className="min-w-0 flex-1"
/>
<Button type="submit" disabled={full || !validEmail || duplicate}>
Invite
</Button>
</form>
<p aria-live="polite" className="mt-2 min-h-4 text-xs text-muted-foreground">
{full
? "All seats are taken. Add seats or remove a member to invite someone."
: duplicate
? "That person is already on the team."
: `${seats - members.length} open ${seats - members.length === 1 ? "seat" : "seats"} left.`}
</p>
<ul className="mt-3 divide-y border-t">
{members.map((member) => (
<li key={member.email} className="flex items-center gap-3 py-2.5">
<Avatar size="sm">
<AvatarFallback>{initials(member.name)}</AvatarFallback>
</Avatar>
<div className="min-w-0 flex-1">
<p className="truncate text-sm font-medium">{member.name}</p>
<p className="truncate text-xs text-muted-foreground">
{member.email}
</p>
</div>
<span className="text-xs text-muted-foreground">{member.role}</span>
<Button
variant="ghost"
size="icon-sm"
aria-label={`Remove ${member.name}`}
disabled={member.role === "Owner"}
onClick={() =>
setMembers((current) =>
current.filter((item) => item.email !== member.email),
)
}
>
<UserMinus aria-hidden="true" />
</Button>
</li>
))}
</ul>
</section>
);
}
npx shadcn@latest add @sevenui/component/meter-09pnpm dlx shadcn@latest add @sevenui/component/meter-09yarn dlx shadcn@latest add @sevenui/component/meter-09bunx --bun shadcn@latest add @sevenui/component/meter-09Finish setting up your account
A complete profile makes scheduling and hand-offs smoother.
"use client";
import * as React from "react";
import { Checkbox } from "@/components/ui/checkbox";
import { Meter, MeterLabel, MeterValue } from "@/components/ui/meter";
const steps = [
{
id: "photo",
title: "Upload a profile photo",
detail: "Helps teammates recognize you in threads.",
points: 15,
},
{
id: "email",
title: "Verify your work email",
detail: "Required before you can invite others.",
points: 20,
},
{
id: "calendar",
title: "Connect your calendar",
detail: "Shows your availability when people book time.",
points: 25,
},
{
id: "timezone",
title: "Set your working hours",
detail: "Mutes notifications outside of them.",
points: 15,
},
{
id: "security",
title: "Turn on two-factor authentication",
detail: "Protects your account with a second sign-in step.",
points: 25,
},
];
function levelFor(score: number) {
if (score >= 100) return "Profile complete";
if (score >= 60) return "Almost there";
if (score >= 30) return "Good start";
return "Just getting started";
}
export default function Meter10() {
const [done, setDone] = React.useState<string[]>(["email"]);
const score = steps
.filter((step) => done.includes(step.id))
.reduce((total, step) => total + step.points, 0);
const level = levelFor(score);
function toggle(id: string, checked: boolean) {
setDone((current) =>
checked ? [...current, id] : current.filter((item) => item !== id),
);
}
return (
<section
aria-labelledby="meter-10-title"
className="w-full max-w-md rounded-xl border bg-card p-4 text-card-foreground"
>
<h3 id="meter-10-title" className="font-medium">
Finish setting up your account
</h3>
<p className="text-sm text-muted-foreground">
A complete profile makes scheduling and hand-offs smoother.
</p>
<Meter
value={score}
getAriaValueText={(formatted) => `${formatted}, ${level}`}
className={
score >= 100
? "mt-4 grid-cols-[1fr_auto] [&>div:last-of-type]:bg-success/20 [&>div:last-of-type>div]:bg-success"
: "mt-4 grid-cols-[1fr_auto]"
}
>
<MeterLabel>Profile strength</MeterLabel>
<MeterValue className="tabular-nums">
{(formatted) => (
<>
{level} ·{" "}
<span className="font-medium text-foreground">{formatted}</span>
</>
)}
</MeterValue>
</Meter>
<ul className="mt-4 grid gap-1">
{steps.map((step) => {
const checked = done.includes(step.id);
return (
<li key={step.id}>
{/* biome-ignore lint/a11y/noLabelWithoutControl: the Checkbox renders the control inside the label */}
<label className="flex cursor-pointer items-start gap-3 rounded-lg p-2 transition-colors hover:bg-muted/60">
<Checkbox
className="mt-0.5"
checked={checked}
onCheckedChange={(value) => toggle(step.id, value)}
/>
<span className="min-w-0 flex-1">
<span
className={
checked
? "block text-sm text-muted-foreground line-through"
: "block text-sm font-medium"
}
>
{step.title}
</span>
<span className="block text-xs text-muted-foreground">
{step.detail}
</span>
</span>
<span className="text-xs text-muted-foreground tabular-nums">
+{step.points}%
</span>
</label>
</li>
);
})}
</ul>
</section>
);
}
npx shadcn@latest add @sevenui/component/meter-10pnpm dlx shadcn@latest add @sevenui/component/meter-10yarn dlx shadcn@latest add @sevenui/component/meter-10bunx --bun shadcn@latest add @sevenui/component/meter-10Free up space
Frees 4.8 GB across 1 item
"use client";
import { FileArchive, FileVideo, Folder, HardDrive } from "lucide-react";
import * as React from "react";
import { Button } from "@/components/ui/button";
import { Checkbox } from "@/components/ui/checkbox";
import { Meter, MeterLabel, MeterValue } from "@/components/ui/meter";
const QUOTA = 20;
const initialFiles = [
{
id: "launch-recording",
name: "Q3 launch recording.mov",
location: "Marketing / Video",
size: 4.8,
icon: FileVideo,
},
{
id: "design-backup",
name: "design-system-backup-2025.zip",
location: "Design / Archive",
size: 3.2,
icon: FileArchive,
},
{
id: "raw-photos",
name: "Offsite raw photos",
location: "Shared / Events",
size: 2.6,
icon: Folder,
},
{
id: "webinar",
name: "Customer webinar, uncut.mp4",
location: "Success / Recordings",
size: 1.9,
icon: FileVideo,
},
];
const OTHER_USAGE = 6.1;
function gb(value: number) {
return `${value.toFixed(1)} GB`;
}
export default function Meter11() {
const [files, setFiles] = React.useState(initialFiles);
const [selected, setSelected] = React.useState<string[]>([
"launch-recording",
]);
const used = OTHER_USAGE + files.reduce((total, file) => total + file.size, 0);
const freed = files
.filter((file) => selected.includes(file.id))
.reduce((total, file) => total + file.size, 0);
const after = used - freed;
const critical = used / QUOTA >= 0.9;
function toggle(id: string, checked: boolean) {
setSelected((current) =>
checked ? [...current, id] : current.filter((item) => item !== id),
);
}
function moveToTrash() {
setFiles((current) => current.filter((file) => !selected.includes(file.id)));
setSelected([]);
}
return (
<section
aria-labelledby="meter-11-title"
className="w-full max-w-md rounded-xl border bg-card text-card-foreground"
>
<div className="grid gap-4 p-4">
<div className="flex items-center gap-2">
<HardDrive
aria-hidden="true"
className="size-4 text-muted-foreground"
/>
<h3 id="meter-11-title" className="font-medium">
Free up space
</h3>
</div>
<Meter
value={used}
max={QUOTA}
getAriaValueText={() => `${gb(used)} of ${QUOTA} GB used`}
className={
critical
? "grid-cols-[1fr_auto] [&>div:last-of-type]:bg-destructive/15 [&>div:last-of-type>div]:bg-destructive"
: "grid-cols-[1fr_auto]"
}
>
<MeterLabel>Workspace storage</MeterLabel>
<MeterValue className="tabular-nums">
{() => `${gb(used)} of ${QUOTA} GB`}
</MeterValue>
</Meter>
<Meter
value={after}
max={QUOTA}
getAriaValueText={() =>
`${gb(after)} of ${QUOTA} GB after removing selected files`
}
className="grid-cols-[1fr_auto] [&>div:last-of-type]:h-1.5 [&>div:last-of-type>div]:bg-primary/60"
>
<MeterLabel className="font-normal text-muted-foreground">
After cleanup
</MeterLabel>
<MeterValue className="tabular-nums">
{() => gb(after)}
</MeterValue>
</Meter>
</div>
<ul aria-label="Largest files" className="divide-y border-t">
{files.map((file) => {
const Icon = file.icon;
return (
<li key={file.id}>
{/* biome-ignore lint/a11y/noLabelWithoutControl: the Checkbox renders the control inside the label */}
<label className="flex cursor-pointer items-center gap-3 px-4 py-2.5 transition-colors hover:bg-muted/50">
<Checkbox
checked={selected.includes(file.id)}
onCheckedChange={(checked) => toggle(file.id, checked)}
/>
<Icon
aria-hidden="true"
className="size-4 shrink-0 text-muted-foreground"
/>
<span className="min-w-0 flex-1">
<span className="block truncate text-sm font-medium">
{file.name}
</span>
<span className="block truncate text-xs text-muted-foreground">
{file.location}
</span>
</span>
<span className="text-sm text-muted-foreground tabular-nums">
{gb(file.size)}
</span>
</label>
</li>
);
})}
{files.length === 0 ? (
<li className="px-4 py-6 text-center text-sm text-muted-foreground">
No files over 1 GB left in this workspace.
</li>
) : null}
</ul>
<div className="flex flex-wrap items-center justify-between gap-3 border-t p-4">
<p aria-live="polite" className="text-sm text-muted-foreground">
{selected.length === 0
? "Select files to see what you would free."
: `Frees ${gb(freed)} across ${selected.length} ${selected.length === 1 ? "item" : "items"}`}
</p>
<Button
variant="destructive"
size="sm"
disabled={selected.length === 0}
onClick={moveToTrash}
>
Move to trash
</Button>
</div>
</section>
);
}
npx shadcn@latest add @sevenui/component/meter-11pnpm dlx shadcn@latest add @sevenui/component/meter-11yarn dlx shadcn@latest add @sevenui/component/meter-11bunx --bun shadcn@latest add @sevenui/component/meter-11Rate limits
Window resets in 42s
Requests per minute for key sk_live_…8f2c
Throttled requests return 429 with a Retry-After header.
"use client";
import { RotateCw } from "lucide-react";
import * as React from "react";
import { Badge } from "@/components/ui/badge";
import { Meter, MeterLabel, MeterValue } from "@/components/ui/meter";
import {
Tabs,
TabsContent,
TabsList,
TabsTrigger,
} from "@/components/ui/tabs";
const WINDOW_SECONDS = 60;
const environments = [
{
value: "production",
label: "Production",
key: "sk_live_…8f2c",
limits: [
{ endpoint: "POST /v1/charges", used: 472, limit: 500 },
{ endpoint: "GET /v1/customers", used: 1180, limit: 2000 },
{ endpoint: "POST /v1/webhooks/test", used: 20, limit: 20 },
],
},
{
value: "staging",
label: "Staging",
key: "sk_test_…41ad",
limits: [
{ endpoint: "POST /v1/charges", used: 38, limit: 100 },
{ endpoint: "GET /v1/customers", used: 212, limit: 500 },
{ endpoint: "POST /v1/webhooks/test", used: 4, limit: 20 },
],
},
];
const numberFormat = new Intl.NumberFormat("en-US");
function statusFor(ratio: number) {
if (ratio >= 1) {
return {
badge: <Badge variant="destructive">Throttled</Badge>,
className:
"[&>div:last-of-type]:bg-destructive/15 [&>div:last-of-type>div]:bg-destructive",
};
}
if (ratio >= 0.8) {
return {
badge: <Badge variant="outline">Near limit</Badge>,
className:
"[&>div:last-of-type]:bg-warning/20 [&>div:last-of-type>div]:bg-warning",
};
}
return { badge: null, className: "" };
}
export default function Meter12() {
const [secondsLeft, setSecondsLeft] = React.useState(42);
React.useEffect(() => {
const id = window.setInterval(() => {
setSecondsLeft((current) => (current <= 1 ? WINDOW_SECONDS : current - 1));
}, 1000);
return () => window.clearInterval(id);
}, []);
return (
<section
aria-labelledby="meter-12-title"
className="w-full max-w-md rounded-xl border bg-card p-4 text-card-foreground"
>
<div className="flex flex-wrap items-center justify-between gap-2">
<h3 id="meter-12-title" className="font-medium">
Rate limits
</h3>
<p className="flex items-center gap-1.5 text-xs text-muted-foreground tabular-nums">
<RotateCw aria-hidden="true" className="size-3.5" />
Window resets in {secondsLeft}s
</p>
</div>
<Tabs defaultValue="production" className="mt-3">
<TabsList className="w-full">
{environments.map((environment) => (
<TabsTrigger
key={environment.value}
value={environment.value}
className="flex-1"
>
{environment.label}
</TabsTrigger>
))}
</TabsList>
{environments.map((environment) => (
<TabsContent
key={environment.value}
value={environment.value}
className="grid gap-4 pt-2"
>
<p className="text-xs text-muted-foreground">
Requests per minute for key{" "}
<code className="whitespace-nowrap rounded bg-muted px-1 py-0.5 font-mono text-foreground">
{environment.key}
</code>
</p>
{environment.limits.map((item) => {
const status = statusFor(item.used / item.limit);
return (
<Meter
key={item.endpoint}
value={item.used}
max={item.limit}
getAriaValueText={() =>
`${numberFormat.format(item.used)} of ${numberFormat.format(item.limit)} requests this minute`
}
className={`grid-cols-[minmax(0,1fr)_auto] items-center gap-x-3 ${status.className}`}
>
<MeterLabel className="flex min-w-0 items-center gap-2 font-mono text-xs font-normal">
<span className="truncate">{item.endpoint}</span>
{status.badge}
</MeterLabel>
<MeterValue className="text-xs tabular-nums">
{() =>
`${numberFormat.format(item.used)} / ${numberFormat.format(item.limit)}`
}
</MeterValue>
</Meter>
);
})}
<p className="text-xs text-muted-foreground">
Throttled requests return{" "}
<code className="font-mono text-foreground">429</code> with a{" "}
<code className="font-mono text-foreground">Retry-After</code>{" "}
header.
</p>
</TabsContent>
))}
</Tabs>
</section>
);
}
npx shadcn@latest add @sevenui/component/meter-12pnpm dlx shadcn@latest add @sevenui/component/meter-12yarn dlx shadcn@latest add @sevenui/component/meter-12bunx --bun shadcn@latest add @sevenui/component/meter-12Meeting load
24h in meetings · 16h left for focus
Mon
Sep 22
Mon Sep 22 meetingsTue
Sep 23
Tue Sep 23 meetings2h of optional meetings
Wed
Sep 24
Wed Sep 24 meetingsThu
Sep 25
Thu Sep 25 meetings1.5h of optional meetings
Fri
Sep 26
Fri Sep 26 meetings
"use client";
import * as React from "react";
import { Button } from "@/components/ui/button";
import { Meter, MeterLabel, MeterValue } from "@/components/ui/meter";
import { ToggleGroup, ToggleGroupItem } from "@/components/ui/toggle-group";
const WORKDAY = 8;
const HEAVY = 6;
type Day = {
day: string;
date: string;
booked: number;
optional: number;
};
const weeks: Record<string, Day[]> = {
current: [
{ day: "Mon", date: "Sep 22", booked: 5.5, optional: 1 },
{ day: "Tue", date: "Sep 23", booked: 7, optional: 2 },
{ day: "Wed", date: "Sep 24", booked: 3, optional: 0.5 },
{ day: "Thu", date: "Sep 25", booked: 6.5, optional: 1.5 },
{ day: "Fri", date: "Sep 26", booked: 2, optional: 0 },
],
next: [
{ day: "Mon", date: "Sep 29", booked: 4, optional: 1 },
{ day: "Tue", date: "Sep 30", booked: 8, optional: 2.5 },
{ day: "Wed", date: "Oct 1", booked: 6, optional: 1 },
{ day: "Thu", date: "Oct 2", booked: 2.5, optional: 0 },
{ day: "Fri", date: "Oct 3", booked: 1.5, optional: 0.5 },
],
};
function hours(value: number) {
return `${value % 1 === 0 ? value : value.toFixed(1)}h`;
}
export default function Meter13() {
const [week, setWeek] = React.useState("current");
const [declined, setDeclined] = React.useState<string[]>([]);
const days = weeks[week].map((day) => {
const key = `${week}-${day.day}`;
const isDeclined = declined.includes(key);
return {
...day,
key,
isDeclined,
load: isDeclined ? day.booked - day.optional : day.booked,
};
});
const total = days.reduce((sum, day) => sum + day.load, 0);
const focus = days.reduce((sum, day) => sum + (WORKDAY - day.load), 0);
return (
<section
aria-labelledby="meter-13-title"
className="w-full max-w-md rounded-xl border bg-card p-4 text-card-foreground"
>
<div className="flex flex-wrap items-center justify-between gap-3">
<div>
<h3 id="meter-13-title" className="font-medium">
Meeting load
</h3>
<p className="text-sm text-muted-foreground tabular-nums">
{hours(total)} in meetings · {hours(focus)} left for focus
</p>
</div>
<ToggleGroup
aria-label="Week"
variant="outline"
size="sm"
value={[week]}
onValueChange={(value) => {
if (value[0]) setWeek(value[0] as string);
}}
>
<ToggleGroupItem value="current">This week</ToggleGroupItem>
<ToggleGroupItem value="next">Next week</ToggleGroupItem>
</ToggleGroup>
</div>
<ul className="mt-4 grid gap-3">
{days.map((day) => {
const heavy = day.load >= HEAVY;
const canDecline = day.optional > 0 && !day.isDeclined;
return (
<li
key={day.key}
className="grid grid-cols-[3rem_minmax(0,1fr)] items-center gap-x-3"
>
<div className="text-sm leading-tight">
<p className="font-medium">{day.day}</p>
<p className="text-xs text-muted-foreground">{day.date}</p>
</div>
<div className="grid gap-1">
<Meter
value={day.load}
max={WORKDAY}
getAriaValueText={() =>
`${hours(day.load)} of ${WORKDAY} working hours booked`
}
className={
heavy
? "grid-cols-[1fr_auto] gap-1.5 [&>div:last-of-type]:bg-warning/20 [&>div:last-of-type>div]:bg-warning"
: "grid-cols-[1fr_auto] gap-1.5"
}
>
<MeterLabel className="sr-only">
{day.day} {day.date} meetings
</MeterLabel>
<MeterValue className="col-start-2 text-xs tabular-nums">
{() => `${hours(day.load)} / ${WORKDAY}h`}
</MeterValue>
</Meter>
{heavy && canDecline ? (
<div className="flex items-center justify-between gap-2">
<p className="text-xs text-muted-foreground">
{hours(day.optional)}{" "}
<span className="max-sm:hidden">of </span>optional
<span className="max-sm:hidden"> meetings</span>
</p>
<Button
variant="link"
size="xs"
className="h-auto px-0"
onClick={() =>
setDeclined((current) => [...current, day.key])
}
>
Decline optional
<span className="sr-only">
{" "}
on {day.day} {day.date}
</span>
</Button>
</div>
) : null}
{day.isDeclined ? (
<div className="flex items-center justify-between gap-2">
<p className="text-xs text-muted-foreground">
Optional meetings declined
</p>
<Button
variant="link"
size="xs"
className="h-auto px-0"
onClick={() =>
setDeclined((current) =>
current.filter((item) => item !== day.key),
)
}
>
Undo
<span className="sr-only">
{" "}
for {day.day} {day.date}
</span>
</Button>
</div>
) : null}
</div>
</li>
);
})}
</ul>
</section>
);
}
npx shadcn@latest add @sevenui/component/meter-13pnpm dlx shadcn@latest add @sevenui/component/meter-13yarn dlx shadcn@latest add @sevenui/component/meter-13bunx --bun shadcn@latest add @sevenui/component/meter-13First response queue
Sorted by time left before the SLA is breached.
- LH
Checkout returns 502 for EU customers
#4821 · Lena Hoffmann, Brightpath · Urgent
First response · 15 min target - MW
SSO login loops back to the sign-in page
#4817 · Marcus Webb, Fieldnote · High
First response · 60 min target - AT
How do I export invoices as CSV?
#4809 · Aiko Tanaka, Parcelly · Normal
First response · 240 min target
"use client";
import { CheckCircle2, Clock } from "lucide-react";
import * as React from "react";
import { Avatar, AvatarFallback } from "@/components/ui/avatar";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import { Meter, MeterLabel, MeterValue } from "@/components/ui/meter";
type Ticket = {
id: string;
customer: string;
company: string;
subject: string;
priority: "Urgent" | "High" | "Normal";
targetMinutes: number;
waitedSeconds: number;
repliedAt: number | null;
};
const initialTickets: Ticket[] = [
{
id: "4821",
customer: "Lena Hoffmann",
company: "Brightpath",
subject: "Checkout returns 502 for EU customers",
priority: "Urgent",
targetMinutes: 15,
waitedSeconds: 14 * 60 + 38,
repliedAt: null,
},
{
id: "4817",
customer: "Marcus Webb",
company: "Fieldnote",
subject: "SSO login loops back to the sign-in page",
priority: "High",
targetMinutes: 60,
waitedSeconds: 49 * 60 + 12,
repliedAt: null,
},
{
id: "4809",
customer: "Aiko Tanaka",
company: "Parcelly",
subject: "How do I export invoices as CSV?",
priority: "Normal",
targetMinutes: 240,
waitedSeconds: 72 * 60,
repliedAt: null,
},
];
function formatDuration(totalSeconds: number) {
const minutes = Math.floor(totalSeconds / 60);
const seconds = totalSeconds % 60;
if (minutes >= 60) {
return `${Math.floor(minutes / 60)}h ${minutes % 60}m`;
}
return `${minutes}:${seconds.toString().padStart(2, "0")}`;
}
function initials(name: string) {
return name
.split(" ")
.map((part) => part[0])
.join("");
}
function toneFor(ratio: number, replied: boolean) {
if (replied) {
return "[&>div:last-of-type]:bg-success/20 [&>div:last-of-type>div]:bg-success";
}
if (ratio >= 1) {
return "[&>div:last-of-type]:bg-destructive/15 [&>div:last-of-type>div]:bg-destructive";
}
if (ratio >= 0.75) {
return "[&>div:last-of-type]:bg-warning/20 [&>div:last-of-type>div]:bg-warning";
}
return "";
}
export default function Meter14() {
const [tickets, setTickets] = React.useState(initialTickets);
const [elapsed, setElapsed] = React.useState(0);
const open = tickets.filter((ticket) => ticket.repliedAt === null).length;
React.useEffect(() => {
if (open === 0) return;
const id = window.setInterval(() => {
setElapsed((current) => current + 1);
}, 1000);
return () => window.clearInterval(id);
}, [open]);
const rows = tickets
.map((ticket) => {
const waited = ticket.waitedSeconds + (ticket.repliedAt ?? elapsed);
const target = ticket.targetMinutes * 60;
const replied = ticket.repliedAt !== null;
return {
...ticket,
waited,
target,
replied,
ratio: waited / target,
breached: waited >= target,
};
})
.sort((a, b) => {
if (a.replied !== b.replied) return a.replied ? 1 : -1;
return b.ratio - a.ratio;
});
const breached = rows.filter((row) => row.breached && !row.replied).length;
function reply(id: string) {
setTickets((current) =>
current.map((ticket) =>
ticket.id === id ? { ...ticket, repliedAt: elapsed } : ticket,
),
);
}
return (
<section
aria-labelledby="meter-14-title"
className="w-full max-w-lg rounded-xl border bg-card text-card-foreground"
>
<div className="flex flex-wrap items-center justify-between gap-2 border-b p-4">
<div>
<h3 id="meter-14-title" className="font-medium">
First response queue
</h3>
<p className="text-sm text-muted-foreground">
Sorted by time left before the SLA is breached.
</p>
</div>
<div aria-live="polite" className="flex gap-1.5">
<Badge variant="secondary">{open} waiting</Badge>
{breached > 0 ? (
<Badge variant="destructive">{breached} breached</Badge>
) : null}
</div>
</div>
<ul className="divide-y">
{rows.map((row) => {
const status = row.replied
? `Replied in ${formatDuration(row.waited)}`
: row.breached
? `Breached by ${formatDuration(row.waited - row.target)}`
: `${formatDuration(row.target - row.waited)} left`;
return (
<li key={row.id} className="grid gap-3 p-4">
<div className="flex items-start gap-3">
<Avatar size="sm" className="mt-0.5">
<AvatarFallback>{initials(row.customer)}</AvatarFallback>
</Avatar>
<div className="min-w-0 flex-1">
<p
className={
row.replied
? "truncate text-sm text-muted-foreground"
: "truncate text-sm font-medium"
}
>
{row.subject}
</p>
<p className="truncate text-xs text-muted-foreground">
#{row.id} · {row.customer}, {row.company} · {row.priority}
</p>
</div>
{row.replied ? (
<CheckCircle2
role="img"
aria-label="Replied"
className="size-4 shrink-0 text-success"
/>
) : (
<Button
size="xs"
variant={row.breached ? "default" : "outline"}
onClick={() => reply(row.id)}
>
Reply
<span className="sr-only"> to ticket {row.id}</span>
</Button>
)}
</div>
<Meter
value={Math.min(row.waited, row.target)}
max={row.target}
getAriaValueText={() =>
`${formatDuration(row.waited)} waited of a ${row.targetMinutes} minute target, ${status}`
}
className={`grid-cols-[1fr_auto] gap-1.5 ${toneFor(row.ratio, row.replied)}`}
>
<MeterLabel className="flex items-center gap-1.5 text-xs font-normal text-muted-foreground">
<Clock aria-hidden="true" className="size-3.5" />
First response · {row.targetMinutes} min target
</MeterLabel>
<MeterValue
className={
row.breached && !row.replied
? "text-xs font-medium text-destructive tabular-nums"
: "text-xs tabular-nums"
}
>
{() => status}
</MeterValue>
</Meter>
</li>
);
})}
</ul>
</section>
);
}
npx shadcn@latest add @sevenui/component/meter-14pnpm dlx shadcn@latest add @sevenui/component/meter-14yarn dlx shadcn@latest add @sevenui/component/meter-14bunx --bun shadcn@latest add @sevenui/component/meter-14