Skip to content
Drivn
7 min read

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.

A form field without a label is a form field nobody can fill in confidently. Drivn's Label is the smallest component in the library: one <label> element carrying text-sm font-medium text-foreground, merged with whatever className you pass through cn(). Its props type is React.LabelHTMLAttributes<HTMLLabelElement> spread directly onto the element, so htmlFor, id, form, title, every aria-*, and every data-* attribute pass through with full TypeScript checking and no wrapper prop to memorize.

That small surface is the design. Click-to-focus — the behavior most label components advertise — is not library code at all; it is the browser matching a htmlFor value against an element id. Drivn never intercepts it, so the same three lines work against an Input, a Checkbox, a plain <textarea>, or a control you wrote yourself last week.

The examples on this page are the field shapes that actually appear in shipped product forms: a stacked Label and Input, an inline Label and Checkbox for opt-ins, a Label above a Select, a labeled radio group, a required marker, and a disabled field. Each snippet compiles against the @/components/ui/label path the Drivn CLI installs to, and each one 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.

1import { Label } from "@/components/ui/label"
2import { Input } from "@/components/ui/input"
3
4export 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.

1import { Label } from "@/components/ui/label"
2import { Checkbox } from "@/components/ui/checkbox"
3
4export 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.

1import { Label } from "@/components/ui/label"
2import { Select } from "@/components/ui/select"
3import { useState } from "react"
4
5export 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.

1import { Label } from "@/components/ui/label"
2import { RadioGroup, Radio } from "@/components/ui/radio-group"
3import { useState } from "react"
4
5export 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.

1import { Label } from "@/components/ui/label"
2import { Input } from "@/components/ui/input"
3
4export 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's Label carries no disabled styling. Its className is text-sm font-medium text-foreground and nothing more, which means a disabled field does not fade its label until you say so. shadcn/ui takes the other route and bakes peer-disabled:cursor-not-allowed peer-disabled:opacity-50 into the recipe — that works while the disabled control is a sibling carrying the peer class, and quietly does nothing the moment your markup nests differently.

The explicit version is a conditional className at the call site: className={isDisabled ? "opacity-50 cursor-not-allowed" : undefined}. Drive it from the same boolean that disables the Input so the two can never disagree — a useState flag, a formState.isSubmitting value from react-hook-form, or a request-in-flight boolean. When a form grows to a dozen fields, lift that flag into a small field context and read it inside a wrapper component instead of repeating the ternary on every row. Because the Label source lives in your repo after install, adding a real disabled prop to the file is equally fair game. See Drivn vs shadcn Label for how the two defaults compare in practice.

1import { Label } from "@/components/ui/label"
2import { Input } from "@/components/ui/input"
3
4export 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}
Get started

Install Drivn in one command

Copy the source into your project and own every line. Zero runtime dependencies, pure React + Tailwind.

npx drivn@latest create

Requires Node 18+. Works with npm, pnpm, and yarn.

Enjoying Drivn?
Star the repo on GitHub to follow new component releases.
Star →

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.