React Label Component Examples
Drop-in React Label examples: paired with Input, Checkbox, Radio, Select, required marker, and disabled state. Zero runtime deps, native <label>, TypeScript.
Every form ships labels — the "Email" above an email input, the terms-of-service line next to a checkbox, the framework picker title above a select. The Label component in Drivn is a small wrapper around the native HTML <label> element styled with text-sm font-medium text-foreground. The file is about twelve lines of TypeScript, ships zero runtime UI dependencies, and forwards every standard HTML label attribute through ...props so htmlFor, id, aria-*, title, and data-* work without any prop drilling or type override.
This page collects the patterns that come up in shipped product forms. Each example is copy-paste ready and uses the same @/components/ui/label import path the Drivn CLI installs under, so the snippets compile the moment you run npx drivn add label. Click forwarding from the Label to its associated control is a native browser feature of the <label> element — pass htmlFor on the Label and a matching id on the Input, Checkbox, or Select, and the browser handles focus and click-to-toggle without any JavaScript.
The examples below cover a stacked Label + Input pair, an inline Label + Checkbox for opt-in flows, a Label + Select for typed dropdowns, a Label + Radio group, a Label with a required marker, and a Label paired with a disabled control. Every snippet is TypeScript — Drivn ships TypeScript-only.
Stacked Label + Input
The most common form field is a Label above an Input. Wrap both in a flex flex-col gap-2 container so the Label sits on its own line eight pixels above the Input. Pass htmlFor="email" on the Label and id="email" on the Input, and the browser focuses the Input when the user clicks the Label text — no JavaScript runs, no React handler fires, the focus forwarding is part of the platform. The Drivn Label's default classes — text-sm font-medium text-foreground — give the Label a quiet visual weight that does not compete with the Input below it.
The pattern scales to any stacked field — a textarea, a date picker, a Select, a custom file uploader. The Label sits at the top, the control sits below, the wrapper handles spacing. If the form grows to multiple stacked fields, wrap the whole form in a flex flex-col gap-6 container so each field group has consistent vertical rhythm between rows.
1 import { Label } from "@/components/ui/label" 2 import { Input } from "@/components/ui/input" 3 4 export default function Page() { 5 return ( 6 <div className="flex flex-col gap-2"> 7 <Label htmlFor="email">Email</Label> 8 <Input 9 id="email" 10 type="email" 11 placeholder="you@example.com" 12 /> 13 </div> 14 ) 15 }
Inline Label + Checkbox
For opt-in patterns like terms acceptance or newsletter subscription, the Label sits next to the Checkbox on the same line. Wrap both in a flex items-center gap-2 container — the inline-flex layout centers the Label text vertically against the Checkbox box, and the eight-pixel gap separates them. Pass htmlFor="terms" on the Label and id="terms" on the Checkbox, and clicking the Label toggles the Checkbox just like clicking the box directly.
This is the standard pattern for terms-of-service rows, marketing opt-ins, "remember me" toggles on sign-in forms, and feature flags inside settings pages. The Label's text-sm font weight keeps the line readable without shouting, and the click target extends across the whole row because the native <label> element receives clicks for its entire bounding box. The same wrapping pattern works with the Drivn Switch component for opt-in flows that prefer a toggle aesthetic over a checkbox.
1 import { Label } from "@/components/ui/label" 2 import { Checkbox } from "@/components/ui/checkbox" 3 4 export default function Page() { 5 return ( 6 <div className="flex items-center gap-2"> 7 <Checkbox id="terms" /> 8 <Label htmlFor="terms"> 9 Accept terms and conditions 10 </Label> 11 </div> 12 ) 13 }
Label + Select for typed dropdowns
A typed dropdown like a framework picker pairs a Label above a Select. The same stacked layout pattern applies — flex flex-col gap-2 wrapper, Label on top, Select below — but the htmlFor/id pair attaches to the Select.Trigger rather than the root Select component because the Trigger is the focusable element that owns the field. Pass id="framework" on the Select.Trigger and htmlFor="framework" on the Label, and the click forwarding lands on the Trigger button.
The Drivn Select uses dot notation — Select.Trigger, Select.Menu, Select.Option — so the whole field reads as one cohesive group from a single Select import. The Label sits outside the Select component, which keeps the field description visually distinct from the dropdown surface and gives screen readers a clean <label for="framework">Framework</label> reference to the focusable Trigger. Pair this with the Combobox component when the dropdown needs typeahead search.
1 import { Label } from "@/components/ui/label" 2 import { Select } from "@/components/ui/select" 3 import { useState } from "react" 4 5 export default function Page() { 6 const [value, setValue] = useState("") 7 8 return ( 9 <div className="flex flex-col gap-2"> 10 <Label htmlFor="framework">Framework</Label> 11 <Select value={value} onChange={setValue}> 12 <Select.Trigger id="framework" placeholder="Choose a framework..."> 13 {value} 14 </Select.Trigger> 15 <Select.Menu> 16 <Select.Option value="React">React</Select.Option> 17 <Select.Option value="Vue">Vue</Select.Option> 18 <Select.Option value="Angular">Angular</Select.Option> 19 <Select.Option value="Svelte">Svelte</Select.Option> 20 </Select.Menu> 21 </Select> 22 </div> 23 ) 24 }
Label + Radio group
A radio group needs one Label per option plus one heading Label for the whole group. The heading Label sits at the top with no htmlFor because it labels a fieldset rather than a single control — wrap the group in a <fieldset> element and pair the heading with a <legend> for semantic correctness, or render the heading Label as a plain group title above the options. Each option Label sits next to its Radio item with htmlFor pointing at the radio's id.
The layout is two nested flex containers — an outer flex flex-col gap-3 for the heading and the option list, and an inner flex flex-col gap-2 for the stacked option rows. Each option row is flex items-center gap-2 with the radio on the left and the option Label on the right. The pattern works for plan pickers, theme selectors, payment method choices, and any single-choice form field where the options stack vertically. Use the Drivn Tabs component instead when the options act as filters rather than form input.
1 import { Label } from "@/components/ui/label" 2 import { RadioGroup, Radio } from "@/components/ui/radio-group" 3 import { useState } from "react" 4 5 export default function Page() { 6 const [plan, setPlan] = useState("pro") 7 8 return ( 9 <div className="flex flex-col gap-3"> 10 <Label>Choose a plan</Label> 11 <RadioGroup value={plan} onChange={setPlan}> 12 <div className="flex items-center gap-2"> 13 <Radio id="free" value="free" /> 14 <Label htmlFor="free">Free</Label> 15 </div> 16 <div className="flex items-center gap-2"> 17 <Radio id="pro" value="pro" /> 18 <Label htmlFor="pro">Pro</Label> 19 </div> 20 </RadioGroup> 21 </div> 22 ) 23 }
Required marker
A required-field marker reads as a red * next to the Label text. Add the marker as a child of the Label rather than as a separate sibling — the marker stays visually attached to the Label, the click target on the marker also forwards focus to the Input, and the screen reader announces the glyph as part of the Label text. Wrap the marker in a <span className="text-destructive" aria-hidden="true"> so the visual contrast is clear and the assistive technology relies on the explicit aria-required="true" on the Input rather than parsing the character.
The Label's default classes already use text-foreground, so the marker picks up the destructive HSL token without affecting the rest of the Label text. Pair this with the form's validation logic in react-hook-form or zod to enforce the required constraint at submit time. The marker is purely a visual cue — the actual validation lives in the form schema and surfaces as an error message below the Input.
1 import { Label } from "@/components/ui/label" 2 import { Input } from "@/components/ui/input" 3 4 export default function Page() { 5 return ( 6 <div className="flex flex-col gap-2"> 7 <Label htmlFor="email"> 8 Email 9 <span className="text-destructive ml-1" aria-hidden="true"> 10 * 11 </span> 12 </Label> 13 <Input 14 id="email" 15 type="email" 16 required 17 aria-required="true" 18 placeholder="you@example.com" 19 /> 20 </div> 21 ) 22 }
Disabled state
Drivn keeps the Label layout-neutral and does not bake in peer-disabled styling. To fade the Label when its associated control is disabled, pass a conditional class at the call site — className={isDisabled ? "opacity-50 cursor-not-allowed" : undefined} — or wire a disabled prop through your own extended Label file. Both approaches are explicit edits in your repo because the Label source lives there after install. The trade is a small amount of repetition at the call site versus an assumed sibling-relationship convention.
The Input itself handles its own disabled visuals via the disabled attribute, so the Label only needs to mirror the visual state. Pair this with a useState boolean or a form state value from react-hook-form to drive the disabled flag from one source of truth. For a more centralized approach across many fields, wrap the field in a parent that exposes a disabled context and have the Label read from it. See the Drivn vs shadcn Label comparison for how the two libraries differ on this exact decision.
1 import { Label } from "@/components/ui/label" 2 import { Input } from "@/components/ui/input" 3 4 export default function Page({ isDisabled = true }) { 5 return ( 6 <div className="flex flex-col gap-2"> 7 <Label 8 htmlFor="email" 9 className={isDisabled ? "opacity-50 cursor-not-allowed" : undefined} 10 > 11 Email 12 </Label> 13 <Input 14 id="email" 15 type="email" 16 disabled={isDisabled} 17 placeholder="you@example.com" 18 /> 19 </div> 20 ) 21 }
Install Drivn in one command
Copy the source into your project and own every line. Zero runtime dependencies, pure React + Tailwind.
npx drivn@latest createRequires Node 18+. Works with npm, pnpm, and yarn.
Frequently asked questions
Yes — the click forwarding is a native browser feature of the <label> element. Pass htmlFor="email" on the Label and id="email" on the Input, and the browser focuses the Input when the user clicks anywhere inside the Label text. No JavaScript runs, no React handler fires, no library code mediates the focus. The behavior is identical for <input>, <select>, <textarea>, and any custom control that wires its focusable element to the matching id. Drivn does not add any extra logic on top of the native behavior because the native behavior already handles every common case.
Render the * as a child of the Label component rather than a sibling. Wrap the glyph in a <span className="text-destructive ml-1" aria-hidden="true">*</span> so the visual color uses the destructive HSL token and the screen reader skips the decorative character. Set aria-required="true" on the associated Input so assistive technology announces the required constraint through the explicit ARIA attribute rather than parsing the glyph. The visual marker and the semantic marker live in two different attributes, which keeps the accessibility tree clean.
Yes, but you lose click-to-focus on the associated control. Without htmlFor, the Label renders as a styled <label> element that has no semantic relationship to any input. The pattern is sometimes useful for section headings inside a form ("Account details" above a group of fields) where the Label acts as a visual group title rather than a per-field label. For per-field labels, always set htmlFor and a matching id on the control so screen readers announce the field name when the input receives focus, and so the click target extends across the Label text.
Drivn keeps the Label layout-neutral so the consumer chooses how disabled state surfaces visually. shadcn/ui bakes peer-disabled:cursor-not-allowed peer-disabled:opacity-50 into the Label className, which assumes a sibling peer class on the disabled input. The convention works for shadcn's broader form patterns but breaks when the input is not a direct sibling or does not carry the peer class. Drivn asks the consumer to pass a conditional className at the call site or extend the local Label file with a disabled prop, which trades a small amount of repetition for explicit control.
Yes. The Label file has no "use client" directive because the component uses no client-only APIs — no useState, no useEffect, no event handlers. You can render Label elements inside a Server Component for static form layouts, marketing forms, or documentation tables, and the component streams as part of the HTML response without a client bundle hit. Only the surrounding form state (a useState for input value, a useForm from react-hook-form) needs the client boundary, and that lives in its own Client Component file.

