FoundationsLabel

A real <label> with peer-aware disabled styling.

Preview

The thinking

A label exists to name a control, so I built it as a real <label> — Radix's LabelPrimitive.Root renders the element itself rather than a styled span, so the native semantics arrive with the tag instead of being rebuilt by hand. The one load-bearing decision is that the label never holds disabled state of its own: it dims through peer-disabled, reading the state off the field it names instead of being told. That mirrors the real world — a label on a switch isn't the thing that's off, the switch is — so the styling follows the same direction the meaning does. Everything else is refusal: no color of its own, no motion, no state it owns. The name stays quiet.

Interface design

Three classes do the typography: text-sm, font-normal, leading-none. The weight is the decision — I keep it at normal because in a form the interesting text is what the user types, not the name of the field, so the emphasis stays with the value. It declares no color at all; the label inherits whatever the surrounding text does, which is how one component sits correctly on a card, a dialog, and a settings row without variants. The disabled look is two rules, peer-disabled:opacity-50 and peer-disabled:cursor-not-allowed — half opacity to match the faded field, and the cursor so the name gives the same refusal the control would. That's the whole surface. Five classes.

Interaction

Nothing here animates — a name has no press state — but the interaction is real. Because Radix renders an actual <label>, the htmlFor wiring makes clicking the name focus the field: the word itself becomes part of the control's hit target, forgiveness for a pointer that lands on the label instead of the box. The one piece of intent-reading is Radix's mousedown guard: when event.detail > 1 it prevents default, so double-clicking a label never selects its text. A fast second click on a name is someone hammering at the control, not starting a text selection, and the primitive reads it that way — stepping aside only when the press lands on a nested button, input, select, textarea, where the control should behave like itself.

Sound design

Silent, and on purpose. The SoundLayer speaks through one capturing click listener keyed to interactive roles — button, [role="switch"], a[href] and their kin — and a plain <label> matches none of them, so a click on the name plays nothing. That feels right to me: the label isn't the action, it's the address of one. When the click forwards to the control it names, whatever sound plays belongs to the control. There's no data-silent here either, because there is nothing to hush.

Empathy

There is no disabled label, only a disabled field — peer-disabled:opacity-50 fades the name to half alongside the control, and peer-disabled:cursor-not-allowed makes it answer the pointer the way the field does. For screen readers the pairing is the native one: a real <label> with htmlFor hands the control its accessible name, so the field is announced with its name and the label never needs to enter the tab order itself. Reduced motion costs nothing because nothing moves. Long text gets no clamp — it wraps like the plain text it is, though leading-none packs wrapped lines tight, so I keep labels to a few words. The honest caveat is the peer contract: peer-disabled reads a sibling that comes before the label in the DOM, so the control goes first or the dimming never fires — the class is a promise the composer has to keep.
label.tsx
"use client";

import * as React from "react";
import * as LabelPrimitive from "@radix-ui/react-label";

import { cn } from "@/lib/utils";

// labels are never disabled — controls are. the label dims through
// peer-disabled, inheriting state from the field it names.
const Label = ({
	ref,
	className,
	...props
}: React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root> & {
	ref?: React.Ref<React.ComponentRef<typeof LabelPrimitive.Root>>;
}) => (
	<LabelPrimitive.Root
		ref={ref}
		className={cn(
			"text-sm font-normal leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-50",
			className
		)}
		{...props}
	/>
);
Label.displayName = LabelPrimitive.Root.displayName;

export { Label };