Label
An accessible form label built on radix-ui that auto-dims when the associated control is disabled.
import { Checkbox } from "@/components/ui/checkbox"
import { Label } from "@/components/ui/label"
export default function LabelDemo() {
return (
<div className="flex flex-col gap-3">
<Label>
<Checkbox tone="brand" defaultChecked /> Accept terms and conditions
</Label>
<Label size="sm">
<Checkbox size="sm" tone="brand" defaultChecked /> Remember this device
</Label>
</div>
)
}Installation
Run the following command
npx shadcn@latest add @ai2/labelDependencies, the @ai2/tokens theme and the component file are installed together.
Install dependencies
npm install radix-ui@^1.6.1Add the cn util
lib/utils.tsimport { clsx, type ClassValue } from "clsx"
import { twMerge } from "tailwind-merge"
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs))
}
/* Adds a source-attribution ref param to a URL (the inspiration exports mark their
outbound links with an ai2.design attribution). An invalid URL is returned as is.
This file is SHOWN TO THE CONSUMER: the docs component pages render the source of
`cn` in a code block, so a Turkish comment here would reach every one of those
pages. Keep it English. */
export function withRef(url: string, ref = "ai2.design"): string {
try {
const u = new URL(url)
u.searchParams.set("ref", ref)
return u.toString()
} catch {
return url
}
}Copy the source code
components/ui/label.tsx"use client"
import type * as React from "react"
import { Label as LabelPrimitive } from "@/components/ui/primitives"
import { cn } from "@/lib/utils"
interface LabelProps
extends React.ComponentProps<typeof LabelPrimitive.Root> {
size?: "sm" | "md"
}
function Label({ className, size = "md", ...props }: LabelProps) {
return (
<LabelPrimitive.Root
data-slot="label"
data-size={size}
className={cn(
"flex items-center gap-2 font-medium leading-none select-none group-data-[disabled=true]:pointer-events-none group-data-[disabled=true]:opacity-50 peer-disabled:cursor-not-allowed peer-disabled:opacity-50",
size === "sm" ? "text-xs" : "text-sm",
className
)}
{...props}
/>
)
}
export { Label, type LabelProps }Manual installs skip the @ai2/tokens theme, so add the token CSS from the theming guide or the theme colors will be missing.
Usage
import { Label } from "@/components/ui/label"
<Label htmlFor="email">Work email</Label>Pair it with any form control via htmlFor, or wrap the control directly. Both associate the click target with the control.
Examples
With input
Wrapping a control
The label lays out as a flex row with a built-in gap, so an inline checkbox needs no extra wrapper.
Sizes
size="sm" drops the text from text-sm to text-xs for dense rows and secondary labels; md is the default.
Disabled control
When the label follows a disabled peer control (ai2 checkboxes ship the peer class) or sits inside a group with data-disabled="true", it dims automatically, with no extra classes on the label itself.
Props
Label adds a single size prop. It also accepts every prop of the radix-ui Label primitive, which itself forwards all native <label> props such as htmlFor.
| Prop | Type | Default | Description |
|---|---|---|---|
size | "sm" | "md" | "md" | Text size: md renders text-sm, sm renders text-xs for dense rows and secondary labels. |
ai2 Label: accessible form labels for React, built on radix-ui
The ai2 Label is a shadcn-compatible label component for React, built on the radix-ui Label primitive and styled with Tailwind CSS v4 on the shared ai2 tokens. It names form controls the way HTML intends, with htmlFor association or by wrapping the control, and adds one thing most hand-rolled labels miss: it dims itself automatically when the control it belongs to is disabled.
Because it ships through the shadcn registry format, you install it with one CLI command, an MCP agent, or a copy-paste, and the source lands in your own project. You own the file; there is no runtime dependency on ai2 itself. The live example above is the exact component you get.
What is the ai2 Label?
It is a single part: one Label component wrapping LabelPrimitive.Root from radix-ui. The primitive renders a native <label> element and prevents the double-click text selection that plain labels suffer from, while keeping every native prop like htmlFor.
The styling makes it a flex row with a built-in gap, so a label that wraps a checkbox or switch lays out correctly with no extra wrapper. A size prop picks the text size: md (default) renders text-sm, sm renders text-xs for dense rows. Disabled awareness is CSS-only: Tailwind peer-disabled selectors react to a disabled sibling control, and group-data-disabled selectors react to a parent marked disabled.
Why use it
- Real label semantics: Built on the radix-ui Label primitive, which renders a native label element: clicking it focuses or toggles the associated control, for screen readers and everyone else.
- Auto-dims for disabled controls: peer-disabled styles mute the label and switch the cursor to not-allowed when the sibling control is disabled. The state stays visually in sync with zero JavaScript.
- Group-level disabling too: Inside a container with the group class and data-disabled="true", the label dims and stops receiving pointer events, covering composite fields.
- Wraps controls cleanly: The flex row layout with a built-in gap means <Label><Checkbox /> Accept terms</Label> aligns correctly without a wrapper div.
- Agent-readable metadata: The registry item describes the association patterns in plain words, so an MCP agent can wire labels to controls without guessing.
Features
- shadcn registry install: One command adds the component, the radix-ui dependency and the @ai2/tokens theme to your project.
- radix-ui Label primitive: Native label rendering plus primitive niceties like guarding against accidental text selection on double click.
- Two association modes: Point at a control with htmlFor and a matching id, or wrap the control as a child; both make the label a click target for it.
- CSS-only disabled sync: peer-disabled and group-data-[disabled=true] selectors handle the muted state; no state props or context providers involved.
- Data attribute for styling: The element exposes data-slot="label", so you can adjust every form label from one CSS rule without forking.
- TypeScript source: The installed file is typed end to end and forwards the full radix Label API, so autocomplete covers everything including htmlFor.
Production tips
- Prefer htmlFor for text inputs: Explicit htmlFor and id association keeps the DOM flat and works with every form library. Save the wrapping pattern for checkboxes and switches.
- Lean on peer for disabled states: ai2 controls ship the peer class, so a Label placed after a disabled control dims automatically. Keep the label after the control in the DOM for peer selectors to apply.
- Do not replace labels with placeholders: Placeholders vanish on input and are not accessible names. Every input should keep a visible Label even when the design looks minimal.
- Keep label text short and specific: One or two words like "Work email" beat sentences. Longer guidance belongs in a FieldDescription below the control.
- Use Field for full anatomy: When you need description and error text too, reach for the ai2 Field composition; its FieldLabel is this same Label with the layout handled.
Works with the rest of ai2
Label pairs with every form control in the registry: an ai2 Input via htmlFor, a wrapped ai2 Checkbox or ai2 Switch for inline consent rows, and items inside an ai2 Radio Group.
For complete form rows, the Field composition builds on this exact Label and adds description and error placement. One token source keeps typography and disabled states consistent across every form in both modes.