docs / core / chip
Chip
a compact pill for tags, filters and counts. a chip states something, it does not do something, the only interactive part is the optional dismiss. when the whole thing should toggle, reach for ToggleChip.
intensity · recedesrsc · client
provenance
shaped from personal-websitelayer
core / primitivesintensity
recedes · safe anywhere, including dense surfacescomposition
tag rows, filter bars, a count beside a headingvalue carries the number, the label stays textnot a click target. when the whole chip toggles, that is ToggleChipa danger chip states a fact. it does not warn or block anythinga11y
the chip is a plain span · the dismiss button is named byremoveLabel with a hit area past the glyph · the x icon is aria-hiddenjest-axedependencies
class-variance-authoritylive demo · try it out
Engineering
customize
tonesemantic color role
sizesm for dense rows
labelthe text inside
valuetrailing segment behind a divider
selectedglow-ring pressed look for active filters
removablerenders the dismiss button
usage
import { Chip } from "usva/primitives/chip";
<Chip tone="accent" onRemove={() => remove(id)}>Engineering</Chip>props
| prop | type | default | notes |
|---|---|---|---|
| tone | "default" | "accent" | "accent-alt" | "success" | "warning" | "danger" | "default" | the color role. tinted border, fill and text from one token. |
| size | "sm" | "md" | "md" | height and padding. sm for dense rows. |
| selected | boolean | false | the glow-ring pressed look, for chips acting as active filters. |
| value | ReactNode | — | a trailing segment behind a divider, tabular for counts. |
| onRemove | () => void | — | when provided, renders the dismiss button. |
| removeLabel | string | "Remove" | accessible name for the dismiss button. name the thing being removed. |
get it
npx shadcn add https://usva.build/r/chip.jsonsource · components/ui/chip.tsxexactly what this command copies
"use client";
import { cva, type VariantProps } from "class-variance-authority";
import * as React from "react";
import { cn } from "@/lib/utils";
export const chipVariants = cva(
cn(
"inline-flex items-center gap-1.5 rounded-full border font-mono font-medium uppercase tracking-wide",
"transition-control duration-fast ease-soft",
"hover:-translate-y-0.5 hover:border-border-strong motion-reduce:transform-none motion-reduce:transition-none",
),
{
variants: {
tone: {
default: "border-border bg-surface-2 text-muted",
accent:
"border-accent/25 bg-accent/12 text-accent hover:border-accent/40",
"accent-alt":
"border-accent-alt/25 bg-accent-alt/12 text-accent-alt hover:border-accent-alt/40",
success:
"border-success/25 bg-success/12 text-success hover:border-success/40",
warning:
"border-warning/25 bg-warning/12 text-warning hover:border-warning/40",
danger:
"border-danger/25 bg-danger/12 text-danger hover:border-danger/40",
},
size: {
sm: "h-6 px-2 text-[0.6875rem]",
md: "h-7 px-2.5 text-xs",
},
selected: {
true: "glow-ring border-transparent text-ink hover:border-transparent",
false: "",
},
},
defaultVariants: { tone: "default", size: "md", selected: false },
},
);
export interface ChipProps
extends Omit<React.HTMLAttributes<HTMLSpanElement>, "onError">,
VariantProps<typeof chipVariants> {
value?: React.ReactNode;
onRemove?: () => void;
removeLabel?: string;
}
export const Chip = React.forwardRef<HTMLSpanElement, ChipProps>(
(
{
className,
tone,
size,
selected,
value,
onRemove,
removeLabel = "Remove",
children,
...props
},
ref,
) => (
<span
ref={ref}
className={cn(chipVariants({ tone, size, selected }), className)}
{...props}
>
<span className="truncate">{children}</span>
{value !== undefined ? (
<span className="border-current/20 border-l pl-1.5 font-semibold text-ink tabular-nums">
{value}
</span>
) : null}
{onRemove ? (
<button
type="button"
aria-label={removeLabel}
onClick={onRemove}
className={cn(
"-mr-1 relative ml-0.5 inline-flex h-5 w-5 shrink-0 items-center justify-center rounded-full outline-none",
"text-current/70 transition-control duration-fast ease-soft",
"hover:bg-current/10 hover:text-current active:scale-90",
"focus-visible:ring-focus motion-reduce:transform-none motion-reduce:transition-none",
"before:absolute before:-inset-2.5 before:content-['']",
)}
>
<RemoveIcon />
</button>
) : null}
</span>
),
);
Chip.displayName = "Chip";
function RemoveIcon() {
return (
<svg
viewBox="0 0 12 12"
fill="none"
stroke="currentColor"
strokeWidth={1.5}
strokeLinecap="round"
strokeLinejoin="round"
className="h-3 w-3"
aria-hidden="true"
>
<path d="M3 3l6 6M9 3l-6 6" />
</svg>
);
}