Status items
Five list rows that report state through a different affordance each: a tone dot, a trailing badge, a full tone surface, a progress bar and a leading tone icon. Color comes only from the semantic tone tokens (info, success, warning, danger), and every tone-aware root carries data-tone.
Installation
The styled layer is free and installs like any other ai2 component.
Run the following command
npx shadcn@latest add @ai2/item-statusDependencies, the @ai2/tokens theme and the component file are installed together.
Install dependencies
npm install lucide-reactCopy the source
components/ui/item-status.tsx"use client"
import type * as React from "react"
import { CircleAlert, CircleCheck, Info, TriangleAlert } from "lucide-react"
import { cn } from "@/lib/utils"
/* Item status family: 5 list rows carrying state. Every variant expresses the state through a different device: a dot, a badge, a tone surface, a progress bar, an icon. Colour comes ONLY from the semantic tone tokens (info/success/warning/danger) plus `-foreground` / `-soft` / `-soft-foreground`; alpha via color-mix. The root carrying the tone gets `data-tone`. The state is purely visual, no animation. Every export renders without props too. */
export type StyledSize = "sm" | "md" | "lg" | "xl"
export type StyledTone = "info" | "success" | "warning" | "danger"
const box: Record<StyledSize, string> = {
sm: "gap-2.5 rounded-lg p-2 text-sm",
md: "gap-3 rounded-lg p-3 text-sm",
lg: "gap-3.5 rounded-xl p-4 text-base",
xl: "gap-4 rounded-xl p-5 text-base",
}
const tile: Record<StyledSize, string> = {
sm: "size-8 rounded-md [&_svg]:size-4 [&_i]:text-base",
md: "size-9 rounded-md [&_svg]:size-4 [&_i]:text-base",
lg: "size-10 rounded-lg [&_svg]:size-5 [&_i]:text-lg",
xl: "size-12 rounded-lg [&_svg]:size-5 [&_i]:text-lg",
}
const base =
"flex w-full items-center [&_svg]:shrink-0 [&_i]:not-italic [&_i]:leading-none"
const titleCls = "truncate font-medium text-foreground"
const descCls = "truncate text-muted-foreground text-[0.85em]"
/* Solid ton dolgusu (nokta / cubuk). */
const dotTone: Record<StyledTone, string> = {
info: "bg-info",
success: "bg-success",
warning: "bg-warning",
danger: "bg-danger",
}
/* Soft ton yuzeyi + okunur yazi rengi. */
const softTone: Record<StyledTone, string> = {
info: "bg-info-soft text-info-soft-foreground",
success: "bg-success-soft text-success-soft-foreground",
warning: "bg-warning-soft text-warning-soft-foreground",
danger: "bg-danger-soft text-danger-soft-foreground",
}
/* Solid ton rozeti. */
const solidTone: Record<StyledTone, string> = {
info: "bg-info text-info-foreground",
success: "bg-success text-success-foreground",
warning: "bg-warning text-warning-foreground",
danger: "bg-danger text-danger-foreground",
}
/* A tone border plus a very light tint for the root surface. */
const edgeTone: Record<StyledTone, string> = {
info: "border-[color-mix(in_oklab,var(--color-info)_35%,transparent)] bg-[color-mix(in_oklab,var(--color-info)_8%,transparent)]",
success:
"border-[color-mix(in_oklab,var(--color-success)_35%,transparent)] bg-[color-mix(in_oklab,var(--color-success)_8%,transparent)]",
warning:
"border-[color-mix(in_oklab,var(--color-warning)_35%,transparent)] bg-[color-mix(in_oklab,var(--color-warning)_8%,transparent)]",
danger:
"border-[color-mix(in_oklab,var(--color-danger)_35%,transparent)] bg-[color-mix(in_oklab,var(--color-danger)_8%,transparent)]",
}
const toneIcon: Record<StyledTone, React.ReactNode> = {
info: <Info />,
success: <CircleCheck />,
warning: <TriangleAlert />,
danger: <CircleAlert />,
}
const toneLabel: Record<StyledTone, string> = {
info: "Queued",
success: "Healthy",
warning: "Degraded",
danger: "Failing",
}
type Props = React.ComponentProps<"div"> & {
size?: StyledSize
tone?: StyledTone
title?: React.ReactNode
description?: React.ReactNode
trailing?: React.ReactNode
}
function Body({
title,
description,
}: {
title: React.ReactNode
description: React.ReactNode
}) {
return (
<div className="flex min-w-0 flex-1 flex-col">
<span className={titleCls}>{title}</span>
{description ? <span className={descCls}>{description}</span> : null}
</div>
)
}
/* Dot: bastaki kucuk ton noktasi. Durum metinle de tekrarlanir (nokta aria-hidden). */
export function DotItem({
className,
size = "md",
tone = "success",
title = "api.ai2.dev",
description = "Operational",
trailing,
...props
}: Props) {
return (
<div
data-slot="styled-item"
data-tone={tone}
className={cn(base, box[size], className)}
{...props}
>
<span aria-hidden="true" className="flex size-4 shrink-0 items-center justify-center">
<span className={cn("size-2 rounded-full", dotTone[tone])} />
</span>
<Body title={title} description={description} />
{trailing}
</div>
)
}
/* StatusBadge: sondaki solid ton rozeti durumu adlandirir. */
export function StatusBadgeItem({
className,
size = "md",
tone = "warning",
title = "Nightly build",
description = "Finished 12 minutes ago",
trailing,
...props
}: Props) {
return (
<div
data-slot="styled-item"
data-tone={tone}
className={cn(base, box[size], className)}
{...props}
>
<Body title={title} description={description} />
{trailing ?? (
<span
className={cn(
"inline-flex h-5 shrink-0 items-center rounded-md px-2 text-xs font-medium",
solidTone[tone]
)}
>
{toneLabel[tone]}
</span>
)}
</div>
)
}
/* Tone: satirin tamami ton yuzeyi - ton kenarligi + hafif tint + soft ikon karesi. */
export function ToneItem({
className,
size = "md",
tone = "danger",
title = "Payment declined",
description = "Update your card to keep the plan active",
trailing,
...props
}: Props) {
return (
<div
data-slot="styled-item"
data-tone={tone}
className={cn(base, box[size], "border", edgeTone[tone], className)}
{...props}
>
<span
aria-hidden="true"
className={cn("inline-flex shrink-0 items-center justify-center", tile[size], softTone[tone])}
>
{toneIcon[tone]}
</span>
<Body title={title} description={description} />
{trailing}
</div>
)
}
/* Progress: a tone-colored progress bar instead of a description. value is clamped
between 0 and 100. */
export function ProgressItem({
className,
size = "md",
tone = "info",
title = "Uploading assets",
description,
value = 64,
trailing,
...props
}: Props & { value?: number }) {
const pct = Math.max(0, Math.min(100, value))
return (
<div
data-slot="styled-item"
data-tone={tone}
className={cn(base, box[size], className)}
{...props}
>
<div className="flex min-w-0 flex-1 flex-col gap-1.5">
<div className="flex items-center justify-between gap-3">
<span className={titleCls}>{title}</span>
<span className="shrink-0 tabular-nums text-muted-foreground text-[0.85em]">
{pct}%
</span>
</div>
<div
role="progressbar"
aria-valuenow={pct}
aria-valuemin={0}
aria-valuemax={100}
aria-label={typeof title === "string" ? title : "Progress"}
className="h-1.5 w-full overflow-hidden rounded-full bg-[color-mix(in_oklab,var(--color-foreground)_10%,transparent)]"
>
<span
className={cn("block h-full rounded-full", dotTone[tone])}
style={{ width: `${pct}%` }}
/>
</div>
{description ? <span className={descCls}>{description}</span> : null}
</div>
{trailing}
</div>
)
}
/* StatusIcon: bastaki ciplak ton ikonu (kare yok) - en sade durum satiri. */
export function StatusIconItem({
className,
size = "md",
tone = "info",
title = "Region eu-west-1",
description = "Scheduled maintenance window",
trailing,
...props
}: Props) {
const toneText: Record<StyledTone, string> = {
info: "text-info",
success: "text-success",
warning: "text-warning",
danger: "text-danger",
}
return (
<div
data-slot="styled-item"
data-tone={tone}
className={cn(base, box[size], className)}
{...props}
>
<span
aria-hidden="true"
className={cn(
"inline-flex shrink-0 items-center justify-center [&_svg]:size-5 [&_i]:text-lg",
toneText[tone]
)}
>
{toneIcon[tone]}
</span>
<Body title={title} description={description} />
{trailing}
</div>
)
}Manual installs skip the @ai2/tokens theme, so add the token CSS from the theming guide or the tone colors will be missing.
Variations
5 takes on the same idea. Each is its own export, and every one accepts a size prop (sm, md, lg, xl) aligned to the base Button scale.
Dot
A small tone dot leads the row and marks health.
import { DotItem } from "@/components/ui/item-status"
<DotItem />Status badge
A trailing solid tone badge names the state.
import { StatusBadgeItem } from "@/components/ui/item-status"
<StatusBadgeItem />Tone
The whole row becomes a tone surface with a soft icon tile.
import { ToneItem } from "@/components/ui/item-status"
<ToneItem />Progress
A tone progress bar replaces the description line.
import { ProgressItem } from "@/components/ui/item-status"
<ProgressItem />Status icon
A bare tone icon leads the row, the quietest status form.
import { StatusIconItem } from "@/components/ui/item-status"
<StatusIconItem />ai2 Status items: 5 styled variations on the token system
The ai2 Status items are a set of 5 decorative button variations from the styled layer of the @ai2 design system, built around list rows that report health, state or progress. They are free and MIT licensed, and every color comes from a semantic token, so they theme with the rest of ai2 in light and dark.
Motion runs on framer-motion: there is no animation; the state is expressed through color and shape. The styled layer is opt-in, so the dependency only lands if you use it; the base components stay lean. Under reduced motion, nothing changes, since the rows do not animate.
What is in the ai2 Status items?
5 exports in one file: Dot, Status badge, Tone, Progress and Status icon. Each renders a native button and takes a size prop (sm, md, lg, xl) aligned to the base Button. They are separate from the base Button on purpose: the base keeps its clean variant, tone and size axes, while the styled layer carries the effects.
You own the file. Copy the one category file and you have all 5 variations, with no runtime dependency on ai2 itself.
Why use it
- On-system by construction: Every color resolves to an ai2 semantic token, so the buttons follow your theme in light and dark with no extra work.
- Effect without the sprawl: The decorations live in a dedicated styled file, so the base Button keeps its clean, predictable API.
- Accessible and honest: Each renders a real button element, keeps a visible focus ring, and respects prefers-reduced-motion.
Features
- Token-driven color: No hardcoded hex or oklch; the look recolors with your theme tokens.
- framer-motion: there is no animation; the state is expressed through color and shape.
- Reduced-motion aware: Under prefers-reduced-motion, nothing changes, since the rows do not animate.
- Size aligned to the base: Every variation takes sm, md, lg and xl matching the base Button height scale, so styled and base buttons line up in a row.
Production tips
- Use it for emphasis, not everywhere: Styled buttons draw the eye. Reserve them for the one action you want people to take on a screen, and use the base Button for the rest.
- Keep labels as verbs: The decoration adds weight, so a clear action label keeps the button scannable.
- Pick one variation per surface: The variations share a family; using two different ones in the same view competes for attention.
Works with the rest of ai2
The Status items sit alongside the base Button and the rest of the @ai2 registry. They share the same token file, so a styled action next to a base button or a badge stays visually consistent in both modes.