Skip to content
v2.5.0
English
Русский
Documentation

Label

Label is the semantic label primitive for form fields.
label.tsx · 6 lines
import { Label, TextField, TextFieldInput } from '@vega-ui/react';

<div>
  <Label htmlFor='preview-label'>Work email</Label>
  <TextField><TextFieldInput id='preview-label' type='email' placeholder='maria@northwind.io' /></TextField>
</div>
Browser support
Widely available
  • Chrome 111
  • Edge 111
  • Firefox 113
  • Safari 15.4
  • Safari on iOS 15.4
Bundle size
Tested in
10 tests

Summary

Label is the semantic label primitive for form fields. It reuses the Text typography contract while rendering a native <label> element, so naming a control stays separate from generic text styling.

Imports

import { Label, type LabelProps } from '@vega-ui/react';

Exported Types

  • LabelProps

Basic Usage

field-label.tsx · 15 lines
import { Label, TextField, TextFieldInput } from '@vega-ui/react';
import { useId } from 'react';

export default function FieldLabel() {
  const id = useId();

  return (
    <div style={{ display: 'grid', gap: 'var(--spacing-3)', width: '100%' }}>
      <Label htmlFor={id}>Company name</Label>
      <TextField>
        <TextFieldInput id={id} name='company' autoComplete='organization' />
      </TextField>

Variants

  • Size: follows the label size contract from LabelProps
  • Composition: native label element with VegaUI text styling
  • Integration: direct htmlFor usage or wrapping a control

Edge Cases

  • Wrapping a control and using htmlFor at the same time is usually unnecessary.
  • Label inherits the Text typographic model, so visual changes can affect field-label consistency.
  • Interactive content inside a label can create ambiguous click behavior.
  • Label should name one control or one wrapped input concept; group labeling belongs to FieldsetLegend.

Common Mistakes

  • Using placeholder text as the only visible label.
  • Using Label for static section headings instead of form controls.
  • Putting multiple unrelated controls inside one label wrapper.

Examples

Basic

Basic: label for a text field

htmlFor must match the id of the native input — TextFieldInput, not the TextField wrapper. useId keeps the id unique when the same form renders more than once. Clicking the label focuses the input.

field-label.tsx · 15 lines
import { Label, TextField, TextFieldInput } from '@vega-ui/react';
import { useId } from 'react';

export default function FieldLabel() {
  const id = useId();

  return (
    <div style={{ display: 'grid', gap: 'var(--spacing-3)', width: '100%' }}>
      <Label htmlFor={id}>Company name</Label>
      <TextField>
        <TextFieldInput id={id} name='company' autoComplete='organization' />
      </TextField>

Form/Integration

Form/Integration: labels for composite fields

In composite fields the id goes on the focusable part: SelectCombobox, NumberFieldInput, or the TextArea itself. The label then names that part, and clicking it moves focus there.

labels-for-composite-fields.tsx · 52 lines
import {
  Label,
  NumberField,
  NumberFieldDecrementButton,
  NumberFieldIncrementButton,
  NumberFieldInput,
  Select,
  SelectCombobox,
  SelectHiddenSelect,
  SelectIcon,
  SelectListbox,
  SelectOption,

Form/Integration: wrapping label without ids

Wrapping a control inside Label associates them implicitly, so neither htmlFor nor id is needed. It fits controls that render a single native element, like TextArea and Radio; TextField renders a <div> wrapper that a label must not contain, and Checkbox and Switch roots are labels already.

wrapping-label.tsx · 22 lines
import { Label, Radio, TextArea } from '@vega-ui/react';

export default function WrappingLabel() {
  return (
    <div style={{ display: 'grid', gap: 'var(--spacing-8)', width: '100%' }}>
      <Label style={{ display: 'grid', gap: 'var(--spacing-3)' }}>
        Delivery instructions
        <TextArea name='instructions' rows={3} fullWidth placeholder='Gate code, floor, where to leave the parcel' />
      </Label>
      <div style={{ display: 'grid', gap: 'var(--spacing-3)' }}>
        <Label fontWeight={400} style={{ display: 'flex', alignItems: 'center', gap: 'var(--spacing-4)' }}>
          <Radio name='window' value='morning' defaultChecked />

Layout/Overlay

Layout/Overlay: labels beside the fields

A two-column grid puts labels to the left of their fields. display: contents on each row keeps label and field as direct grid items, and long labels wrap inside their column instead of pushing the field.

side-by-side-labels.tsx · 34 lines
import { Label, TextField, TextFieldInput } from '@vega-ui/react';
import { useId } from 'react';

const FIELDS = [
  { name: 'firstName', label: 'First name', autoComplete: 'given-name' },
  { name: 'lastName', label: 'Last name', autoComplete: 'family-name' },
  { name: 'jobTitle', label: 'Job title shown on your public profile', autoComplete: 'organization-title' },
];

export default function SideBySideLabels() {
  const id = useId();

Error

Error: required marker and error message

The asterisk is decorative and hidden from screen readers; required on the input is what announces the field as required. The label stays unchanged on error — the message under the field carries it, linked with aria-describedby.

required-field-error.tsx · 40 lines
import { Button, HelperText, Label, TextField, TextFieldInput } from '@vega-ui/react';
import { FormEvent, useId, useState } from 'react';

export default function RequiredFieldError() {
  const id = useId();
  const messageId = `${id}-message`;
  const [taxId, setTaxId] = useState('');
  const [submitted, setSubmitted] = useState(false);
  const invalid = submitted && taxId.trim() === '';

  const onSubmit = (event: FormEvent<HTMLFormElement>) => {
    event.preventDefault();

Disabled

Disabled: label of a disabled field

Label has no disabled style of its own, so dim it together with the field it names. Here the checkbox disables the address field, and the label follows.

label-for-disabled-field.tsx · 38 lines
import {
  Checkbox,
  CheckboxCheckedIcon,
  CheckboxIndicator,
  CheckboxInput,
  Label,
  TextField,
  TextFieldInput,
} from '@vega-ui/react';
import { useId, useState } from 'react';

export default function LabelForDisabledField() {

Edge

Edge: visually hidden label

When the design has no room for a visible label, keep a real <label> and hide it with VisuallyHidden. Screen readers and voice control still get the name, and the placeholder stays only an example.

visually-hidden-label.tsx · 18 lines
import { Button, Label, TextField, TextFieldInput, VisuallyHidden } from '@vega-ui/react';
import { useId } from 'react';

export default function VisuallyHiddenLabel() {
  const id = useId();

  return (
    <form role='search' style={{ display: 'flex', gap: 'var(--spacing-4)', width: '100%' }}>
      <VisuallyHidden asChild>
        <Label htmlFor={id}>Search the help center</Label>
      </VisuallyHidden>
      <TextField style={{ flex: 1 }}>

Anatomy

Overview

Label is a single-part primitive. It renders a semantic <label> and borrows typography behavior from Text.

Required Parts

Label

Required. It is both the semantic root and the visible label surface.

Optional Parts

There are no exported child parts.

Composition Order

  1. Label
  2. label content

Valid Composition Patterns

field-label.tsx · 15 lines
import { Label, TextField, TextFieldInput } from '@vega-ui/react';
import { useId } from 'react';

export default function FieldLabel() {
  const id = useId();

  return (
    <div style={{ display: 'grid', gap: 'var(--spacing-3)', width: '100%' }}>
      <Label htmlFor={id}>Company name</Label>
      <TextField>
        <TextFieldInput id={id} name='company' autoComplete='organization' />
      </TextField>

Invalid Composition Patterns

Placeholder used instead of a label

This removes a stable visible label.

Multiple unrelated controls wrapped by one label

Click behavior and semantics become unclear.