Label
Renders an accessible label associated with controls. Built with Base UI and Tailwind CSS. Copy-paste ready.
import { useId } from "react";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
export default function Particle() {
const id = useId();
return (
<div className="flex flex-col items-start gap-2">
<Label htmlFor={id}>Email</Label>
<Input
aria-label="Email"
id={id}
placeholder="you@example.com"
type="email"
/>
</div>
);
}
Installation
pnpm dlx shadcn@latest add @cnippet/label
Usage
import { Label } from "@/components/ui/label"<Label htmlFor="email">Email</Label>Examples
With Checkbox
Wraps a Checkbox and its label text inside Label so clicking the label text also toggles the checkbox.
import { Checkbox } from "@/components/ui/checkbox";
import { Field } from "@/components/ui/field";
import { Label } from "@/components/ui/label";
export function Pattern() {
return (
<Field className="mx-auto flex w-auto flex-row">
<Checkbox id="label-demo-terms" />
<Label htmlFor="label-demo-terms">Accept terms and conditions</Label>
</Field>
);
}
With Textarea
Associates a Label with a Textarea for accessible multi-line text input labelling.
import { Field } from "@/components/ui/field";
import { Label } from "@/components/ui/label";
import { Textarea } from "@/components/ui/textarea";
export function Pattern() {
return (
<Field className="w-full max-w-xs">
<Label htmlFor="label-demo-message">Message</Label>
<Textarea id="label-demo-message" placeholder="Type your message here…" />
</Field>
);
}
With Required
Appends a red asterisk to the label text to signal that the associated field must be filled.
import { Field } from "@/components/ui/field";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
export function Pattern() {
return (
<Field className="w-full max-w-xs">
<Label htmlFor="label-required">
Email address
<span className="text-destructive">*</span>
</Label>
<Input
id="label-required"
placeholder="you@example.com"
required
type="email"
/>
</Field>
);
}
With Optional
Appends an "(Optional)" or similar tag to distinguish non-required fields from mandatory ones.
import { Field } from "@/components/ui/field";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
export function Pattern() {
return (
<Field className="w-full max-w-xs">
<Label htmlFor="label-optional">
Phone number
<span className="text-muted-foreground">(optional)</span>
</Label>
<Input id="label-optional" placeholder="+1 (555) 000-0000" type="tel" />
</Field>
);
}
With Info Tooltip
Adds a small info icon button next to the label text that opens a Tooltip with additional context on hover or focus.
import { InfoIcon } from "lucide-react";
import { Field } from "@/components/ui/field";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "@/components/ui/tooltip";
export function Pattern() {
return (
<Field className="w-full max-w-xs">
<Label className="gap-1" htmlFor="label-tooltip">
API Key
<TooltipProvider>
<Tooltip>
<TooltipTrigger className="inline-flex items-center">
<span className="inline-flex cursor-help text-muted-foreground">
<InfoIcon className="size-3.5" />
</span>
</TooltipTrigger>
<TooltipContent>
<p>Your API key can be found in the developer settings.</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
</Label>
<Input
className="font-mono"
id="label-tooltip"
placeholder="sk_live_..."
/>
</Field>
);
}
With Badge
Appends an inline Badge (e.g. "New", "Beta", "Pro") to the label to communicate feature availability or access tier.
import { Badge } from "@/components/ui/badge";
import { Field } from "@/components/ui/field";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
export function Pattern() {
return (
<Field className="w-full max-w-xs">
<Label className="gap-2" htmlFor="label-badge">
Webhook URL
<Badge size="sm" variant="success">
Active
</Badge>
</Label>
<Input
className="font-mono text-xs"
defaultValue="https://api.example.com/webhooks"
id="label-badge"
type="url"
/>
</Field>
);
}
With Counter
Shows a live character count inline with the label that updates as the user types into the associated input or textarea.
"use client";
import { useState } from "react";
import { Field } from "@/components/ui/field";
import { Label } from "@/components/ui/label";
import { Textarea } from "@/components/ui/textarea";
export function Pattern() {
const [length, setLength] = useState(0);
return (
<Field className="w-full max-w-xs">
<Label className="justify-between" htmlFor="label-counter">
Bio
<span className="text-muted-foreground">{length}/200</span>
</Label>
<Textarea
id="label-counter"
maxLength={200}
onChange={(e) => setLength(e.target.value.length)}
placeholder="Tell us about yourself…"
/>
</Field>
);
}
With Helper Text
Places a short description paragraph below the label to provide additional guidance about what to enter.
Your secret key for API authentication
import { Field, FieldDescription } from "@/components/ui/field";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
export function Pattern() {
return (
<Field className="w-full max-w-xs">
<div className="flex flex-col gap-1">
<Label htmlFor="label-helper">API Key</Label>
<FieldDescription>
Your secret key for API authentication
</FieldDescription>
</div>
<Input
className="font-mono"
id="label-helper"
placeholder="sk_live_..."
/>
</Field>
);
}
With Status Dot
A colored dot prepended to the label text to indicate the current status of the associated field or resource.
import { Field } from "@/components/ui/field";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
export function Pattern() {
return (
<Field className="w-full max-w-xs">
<Label className="gap-1.5" htmlFor="label-status">
Server Status
<span className="relative flex size-2">
<span className="absolute inline-flex size-full animate-ping rounded-full bg-green-400 opacity-75" />
<span className="relative inline-flex size-2 rounded-full bg-green-500" />
</span>
</Label>
<Input defaultValue="Online" disabled id="label-status" />
</Field>
);
}
With Keyboard Shortcut
An inline Kbd badge aligned to the trailing edge of the label shows the keyboard shortcut that focuses or activates the field.
import { useId } from "react";
import { Field } from "@/components/ui/field";
import { Input } from "@/components/ui/input";
import { Kbd } from "@/components/ui/kbd";
import { Label } from "@/components/ui/label";
export function Pattern() {
const id = useId();
return (
<Field className="w-full max-w-xs">
<Label className="justify-between" htmlFor={id}>
Quick search
<Kbd className="font-normal text-muted-foreground">⌘K</Kbd>
</Label>
<Input id={id} placeholder="Type to search…" type="search" />
</Field>
);
}
With Inline Link
A secondary action link (e.g. "Forgot password?") placed inline at the label's trailing edge — common for password fields.
import Link from "next/link";
import { useId } from "react";
import { Field } from "@/components/ui/field";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
export function Pattern() {
const id = useId();
return (
<Field className="w-full max-w-xs">
<Label className="justify-between" htmlFor={id}>
Password
<Link
className="font-normal text-muted-foreground text-xs underline-offset-4 hover:underline"
href="/forgot-password"
>
Forgot password?
</Link>
</Label>
<Input id={id} placeholder="••••••••" type="password" />
</Field>
);
}
With Icon Prefix
A small icon placed before the label text to visually reinforce the field's purpose at a glance.
import { MailIcon } from "lucide-react";
import { useId } from "react";
import { Field } from "@/components/ui/field";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
export function Pattern() {
const id = useId();
return (
<Field className="w-full max-w-xs">
<Label className="gap-1.5" htmlFor={id}>
<MailIcon aria-hidden="true" className="size-3.5" />
Email address
</Label>
<Input id={id} placeholder="you@example.com" type="email" />
</Field>
);
}
Horizontal Layout
Labels right-aligned in a fixed-width column with inputs filling the remaining space — suitable for compact settings or profile forms.
import { useId } from "react";
import { Field } from "@/components/ui/field";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
const fields: {
key: string;
label: string;
placeholder: string;
type?: string;
}[] = [
{ key: "first", label: "First name", placeholder: "Jane" },
{ key: "last", label: "Last name", placeholder: "Smith" },
{
key: "email",
label: "Email",
placeholder: "jane@example.com",
type: "email",
},
];
export function Pattern() {
return (
<div className="w-full max-w-sm space-y-3">
{fields.map(({ key, label, placeholder, type }) => (
<HorizontalField
key={key}
label={label}
placeholder={placeholder}
type={type}
/>
))}
</div>
);
}
function HorizontalField({
label,
placeholder,
type,
}: {
label: string;
placeholder: string;
type?: string;
}) {
const id = useId();
return (
<Field className="grid grid-cols-[100px_1fr] items-center gap-4">
<Label className="text-right" htmlFor={id}>
{label}
</Label>
<Input id={id} placeholder={placeholder} type={type} />
</Field>
);
}
With Error State
A controlled email field where a validation error shows an aria-invalid border and an inline FieldDescription error message below the label.
This email is already registered.
"use client";
import { useId, useState } from "react";
import { Field, FieldDescription } from "@/components/ui/field";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
export function Pattern() {
const [value, setValue] = useState("existing@taken.com");
const id = useId();
const hasError = value.length > 0 && value.includes("taken");
return (
<Field className="w-full max-w-xs">
<Label htmlFor={id}>
Email address
<span className="text-destructive">*</span>
</Label>
<Input
aria-describedby={hasError ? `${id}-error` : undefined}
aria-invalid={hasError}
className={
hasError ? "border-destructive focus-visible:ring-destructive/20" : ""
}
id={id}
onChange={(e) => setValue(e.target.value)}
type="email"
value={value}
/>
{hasError && (
<FieldDescription className="text-destructive" id={`${id}-error`}>
This email is already registered.
</FieldDescription>
)}
</Field>
);
}

