How to Add a Label to a React App
Step-by-step guide to adding an accessible form label to any React project with the Drivn CLI — native htmlFor association and zero runtime deps.
A form label is the smallest accessibility win most React apps ship — the word above an input that names the field for a screen reader and gives every user a bigger click target that focuses the control. Building one is trivial until you want it to theme with the rest of your UI, associate cleanly with a checkbox or a select, and carry a consistent weight across every form. Most teams either restyle a raw <label> on every screen or pull in a form library for a single element.
Drivn writes the component into your repository instead. The CLI copies a label.tsx file that is about twelve lines of TypeScript: one function around a native <label>, with its text-sm font-medium text-foreground classes held in a single cn() call and every native attribute forwarded through {...props}. It has no component dependencies, imports no icons, and — because it holds no state and runs no effect — no 'use client' directive, so it renders on the server as happily as in the browser.
This guide adds the Label to an existing React app in a few minutes — install the CLI, render a labeled input, associate a label with a checkbox and a select, and restyle the one className it reads from. It works the same in Vite + React or a Next.js App Router project. For the framework-specific walkthrough see the Next.js Label guide.
Prerequisites
Before installing the Label, confirm your React project has the three things Drivn assumes: Tailwind CSS v4 installed and processing your styles, TypeScript configured (the component ships as a .tsx file), and a @/ path alias pointing at your source directory. If you scaffolded with create-next-app, npm create vite, or npx drivn@latest create, all three are already wired. For a custom setup, check the compilerOptions.paths entry in tsconfig.json; the installation page lists the minimal config. The Label pulls in no other Drivn components and imports no icons, so it is a single self-contained file — one of the simplest components to add first when you are trying Drivn in an existing app.
Step 1 — Install Drivn via the CLI
Run the CLI from your project root to add the Label source file. The command prompts once for your install directory (defaulting to src/components/ui/), writes label.tsx, and finishes — there are no dependencies to resolve, so nothing else is written to your project and nothing is added to package.json. No global config file is created; the result is one TypeScript file you edit like any other component. Confirm it landed in your editor, then commit it for a clean baseline. If your project uses a monorepo layout or a non-standard source path, the CLI docs cover the flags for targeting a custom location during install.
1 # add the Label — a single self-contained file 2 npx drivn add label
Step 2 — Render a labeled input
Import Label from your UI directory and pair it with an input in a flex flex-col gap-2 column so the label sits just above the field. The one attribute that does the accessibility work is htmlFor: set it to the input's id and the browser links the two, so a screen reader announces the label when the field is focused and a click on the label word focuses the input. The component wraps your text in a native <label> styled to text-sm font-medium text-foreground — a small, medium-weight caption that reads its color from your Tailwind tokens. Every native label attribute passes straight through {...props}, so id, aria-*, and data-* work exactly as they would on a raw element. Pair it with the Drivn Input for a matching field, and reach for React's useId hook when you render the pair inside a reusable component to avoid id collisions.
1 import { Label } from '@/components/ui/label' 2 import { Input } from '@/components/ui/input' 3 4 export function EmailField() { 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 }
Step 3 — Associate a label with a checkbox and select
The same htmlFor/id pairing associates a label with any form control, not just a text input. For a Checkbox, place the label beside the control in a flex items-center gap-2 row rather than above it, point htmlFor at the checkbox id, and clicking the label toggles the box — the enlarged hit target matches native form behavior. For a Select, keep the stacked flex flex-col gap-2 layout and point htmlFor at the trigger's id. Because the Label carries no positional classes of its own — only its text-sm font-medium typography — the surrounding wrapper owns the layout in every arrangement, which is why the component ships one className and no variants. That composability lets the same Label sit above an input, beside a checkbox, or over a select without any change to the component itself.
1 import { Label } from '@/components/ui/label' 2 import { Checkbox } from '@/components/ui/checkbox' 3 4 export function TermsRow() { 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 }
Step 4 — Add a required marker and restyle
A required-field marker is a red * rendered as a child of the label rather than a sibling, so the glyph stays attached to the label text and the click target still forwards focus to the input. Wrap it in a <span className="text-destructive" aria-hidden="true"> so the color reads from the destructive token and the screen reader skips the decorative character, and set aria-required="true" on the input so assistive technology announces the constraint through the explicit attribute. Restyling is just as direct: every class the label renders lives in one cn() call in the file you own — text-sm font-medium text-foreground. Edit that string and every label updates at once; pass a className like text-base or text-destructive for a one-off override, which merges through cn() so your override wins. Because the color is a Tailwind token, changing it in theming re-themes every label for dark and light mode. The Label source below is copied verbatim from label.ts.
1 import * as React from 'react' 2 import { cn } from '@/utils/cn' 3 4 export function Label({ 5 className, 6 ...props 7 }: React.LabelHTMLAttributes<HTMLLabelElement>) { 8 return ( 9 <label 10 className={cn( 11 'text-sm font-medium text-foreground', 12 className 13 )} 14 {...props} 15 /> 16 ) 17 }
Install Drivn in one command
Copy the source into your project and own every line. Zero runtime dependencies, pure React + Tailwind.
Frequently asked questions
Yes. Set the project up with TypeScript and Tailwind v4 first, then run npx drivn add label. The Label has no dependency on Next.js or any router — it is a plain function around a native <label>, so it renders anywhere React and Tailwind reach the DOM. It pulls in no other components and no icon package, so the CLI writes a single label.tsx file and nothing else, with nothing extra to configure after install.
Set the Label's htmlFor prop to the same string as the input's id. That pairing is standard HTML: the browser links the label to the control, so a screen reader announces the label text when the field is focused, and clicking anywhere on the label focuses the input. Drivn passes htmlFor straight through {...props} to the native <label>, so it works exactly as it would on a raw element. Use React's useId hook to generate a stable id when the field lives in a reusable component.
No — the Label source has no 'use client' directive because it holds no state and runs no effect. It is a plain function around a native <label>, so it renders fine inside a Server Component, a static React tree, or a client island. Nothing about a label needs the browser: the htmlFor association is plain HTML that renders once and never hydrates. Only the surrounding form state — a useState value or a useForm hook — needs a client boundary, and that lives in its own file.
Render the * as a child of the Label rather than a sibling, wrapped in a <span className="text-destructive" aria-hidden="true"> so the color uses the destructive token and the screen reader skips the decorative glyph. Set aria-required="true" on the associated input so assistive technology announces the constraint through the explicit ARIA attribute instead of parsing the character. The visual marker and the semantic marker live in two different attributes, which keeps the accessibility tree clean while the asterisk still forwards clicks to the field.
All of it lives in the single cn() call at the top of label.tsx: text-sm font-medium text-foreground. Edit that string and every label updates at once — bump text-sm to text-base, font-medium to font-semibold, or swap the color token. For a one-off change, pass a className like text-base or text-destructive; it merges through cn() so your override wins over the base. Because the color reads from a Tailwind token, changing it in theming re-themes every label for dark and light mode.

