Label

Renders an accessible caption for a form control.

import * as stylex from "@stylexjs/stylex"
import { Input } from "@/components/ui/input"
import { Label } from "@/components/ui/label"
import { VStack } from "@/components/ui/stack"

const styles = stylex.create({
  field: { width: "100%", maxWidth: "20rem" },
})

export default function LabelDemo() {
  return (
    <VStack gap={2} xstyle={styles.field}>
      <Label htmlFor="label-demo-email">Email</Label>
      <Input id="label-demo-email" type="email" placeholder="me@example.com" />
    </VStack>
  )
}

Installation

Install the following dependencies:

pnpm add @base-ui/react @stylexjs/stylex clsx

Set up the token layer and the StyleX compiler, if you have not already. See Installation, and paste states.ts from the same page, which is where the dimmed disabled label comes from. The label also needs interop.css, which sizes an icon the label does not render.

Copy and paste the following code into your project.

components/ui/label.tsx
"use client"

import { useRender } from "@base-ui/react/use-render"
import * as stylex from "@stylexjs/stylex"
import type { StyleXStyles } from "@stylexjs/stylex"
import { clsx } from "clsx"

import { states } from "@/theme/states"
import { space, text } from "@/theme/tokens.stylex"

const styles = stylex.create({
  base: {
    display: "flex",
    alignItems: "center",
    gap: space.s2,
    userSelect: "none",
    fontSize: text.sm,
    lineHeight: 1,
    fontWeight: 500,
    "--carbon-icon-size": space.s4,
  },
})

const labelStyles = { base: styles.base, disabled: states.disabled }

type LabelProps = useRender.ComponentProps<"label", { disabled: boolean | undefined }> & {
  disabled?: boolean
  xstyle?: StyleXStyles
  className?: string
}

function Label({ render, ref, style, disabled, xstyle, className, ...props }: LabelProps) {
  const sx = stylex.props(styles.base, disabled && states.disabled, xstyle)

  return (
    useRender({
      defaultTagName: "label",
      render,
      ref,
      state: { disabled },
      props: {
        "data-slot": "label",
        ...sx,
        ...props,
        className: clsx("carbon-icons", sx.className, className),
        style: { ...sx.style, ...style },
      },
    })
  )
}

export { Label, labelStyles }

Update the import paths to match your project setup.

Usage

import { Label } from "@/components/ui/label"
<Label htmlFor="email">Email</Label>
<Input id="email" type="email" />

Disabled

Use the disabled prop to dim the label and take it out of the hit test. It is a prop rather than an inherited state. Inside a Field the FieldLabel reads it from the field; a bare label has nothing to read it from.

import * as stylex from "@stylexjs/stylex"
import { Input } from "@/components/ui/input"
import { Label } from "@/components/ui/label"
import { VStack } from "@/components/ui/stack"

const styles = stylex.create({
  field: { width: "100%", maxWidth: "20rem" },
})

export default function LabelDisabled() {
  return (
    <VStack gap={2} xstyle={styles.field}>
      <Label htmlFor="label-disabled-plan" disabled>
        Billing plan
      </Label>
      <Input id="label-disabled-plan" defaultValue="Enterprise" disabled />
    </VStack>
  )
}

Content

The label is a flex row with a 0.5rem gap, so an icon or a Badge sits beside the text without extra markup.

import * as stylex from "@stylexjs/stylex"
import { Badge } from "@/components/ui/badge"
import { Input } from "@/components/ui/input"
import { Label } from "@/components/ui/label"
import { VStack } from "@/components/ui/stack"
import { Lock } from "lucide-react"

const styles = stylex.create({
  field: { width: "100%", maxWidth: "20rem" },
})

export default function LabelContent() {
  return (
    <VStack gap={2} xstyle={styles.field}>
      <Label htmlFor="label-content-secret">
        <Lock />
        Signing secret
        <Badge variant="secondary">Recommended</Badge>
      </Label>
      <Input id="label-content-secret" type="password" placeholder="whsec_..." />
    </VStack>
  )
}

Give the label either an htmlFor matching the control's id or the control itself as a child. Without one of the two the label is decoration, and clicking it does not focus anything.

API reference

Pass render to replace a part's element or compose it with another component.

Label renders a <label> and accepts every <label> attribute.

Label

PropTypeDefault
disabledbooleanfalse
htmlForstring—
xstyleStyleXStyles—
classNamestring—

The line height is 1 rather than the sm leading the type scale pairs with text.sm. A label is one line. With the tight box, the stack's gap alone sets the distance to the control below.

labelStyles

The style maps behind the component. Spread them onto any element.

KeyWhat it is
baseThe row: gap, type, weight, icon size
disabledThe dimmed, non-interactive state

disabled is states.disabled from theme/states.ts, the same dimmed style every control uses.

Data attributes

AttributeOnEffect
data-slot="label"the labelTargets the label from a parent
data-disabledthe labelPresent when disabled is set