Перейти к содержимому
v3.0.0
English
Русский
Документация

Label

Label — семантический примитив подписи для полей формы.
label.tsx · 6 строк
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>
Поддержка браузеров
Широко доступно
  • Chrome 111
  • Edge 111
  • Firefox 113
  • Safari 15.4
  • Safari на iOS 15.4
Размер в бандле
История версий
Добавлен в v1.0.0

Кратко

Label — семантический примитив подписи для полей формы. Он переиспользует типографический контракт Text, но рендерит нативный элемент <label>, поэтому задание имени элементу управления остаётся отделённым от общих стилей текста.

Импорт

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

Экспортируемые типы

  • LabelProps

Базовое использование

field-label.tsx · 15 строк
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>
    </div>
  );
}

Варианты

  • Размер: следует контракту размеров подписи из LabelProps
  • Композиция: нативный элемент подписи со стилями текста VegaUI
  • Интеграция: прямое использование htmlFor или обёртывание элемента управления

Пограничные случаи

  • Одновременно оборачивать элемент управления и использовать htmlFor обычно не нужно.
  • Label наследует типографическую модель Text, поэтому визуальные изменения могут нарушить единообразие подписей полей.
  • Интерактивное содержимое внутри подписи может сделать поведение клика неоднозначным.
  • Label должен задавать имя одному элементу управления или одному обёрнутому полю ввода; подписи групп — задача FieldsetLegend.

Частые ошибки

  • Использовать текст плейсхолдера как единственную видимую подпись.
  • Использовать Label для статичных заголовков разделов вместо подписей элементов управления формы.
  • Помещать несколько несвязанных элементов управления в одну подпись-обёртку.

Примеры

Базовый

Базовый: подпись текстового поля

htmlFor должен совпадать с id нативного инпута — TextFieldInput, а не обёртки TextField. useId сохраняет id уникальным, даже если форма отрисована несколько раз. Клик по подписи ставит фокус в инпут.

field-label.tsx · 15 строк
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>
    </div>
  );
}

Форма/интеграция

Форма/интеграция: подписи составных полей

В составных полях id ставится на фокусируемую часть: SelectCombobox, NumberFieldInput или сам TextArea. Тогда подпись называет именно её, и клик по подписи переносит туда фокус.

labels-for-composite-fields.tsx · 52 строки
import {
  Label,
  NumberField,
  NumberFieldDecrementButton,
  NumberFieldIncrementButton,
  NumberFieldInput,
  Select,
  SelectCombobox,
  SelectHiddenSelect,
  SelectIcon,
  SelectListbox,
  SelectOption,
  SelectValue,
  TextArea,
} from '@vega-ui/react';
import { useId } from 'react';

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

  return (
    <form style={{ display: 'grid', gap: 'var(--spacing-8)', width: '100%' }}>
      <div style={{ display: 'grid', gap: 'var(--spacing-3)' }}>
        <Label htmlFor={`${id}-region`}>Data region</Label>
        <Select defaultValue='eu-central'>
          <SelectHiddenSelect name='region' />
          <SelectCombobox id={`${id}-region`}>
            <SelectValue placeholder='Choose a region' />
            <SelectIcon />
          </SelectCombobox>
          <SelectListbox>
            <SelectOption value='eu-central'>Frankfurt (eu-central)</SelectOption>
            <SelectOption value='us-east'>Virginia (us-east)</SelectOption>
            <SelectOption value='ap-south'>Mumbai (ap-south)</SelectOption>
          </SelectListbox>
        </Select>
      </div>
      <div style={{ display: 'grid', gap: 'var(--spacing-3)' }}>
        <Label htmlFor={`${id}-replicas`}>Read replicas</Label>
        <NumberField min={0} max={5} defaultValue={1}>
          <NumberFieldDecrementButton aria-label='Remove a replica' />
          <NumberFieldInput id={`${id}-replicas`} name='replicas' />
          <NumberFieldIncrementButton aria-label='Add a replica' />
        </NumberField>
      </div>
      <div style={{ display: 'grid', gap: 'var(--spacing-3)' }}>
        <Label htmlFor={`${id}-notes`}>Notes for the on-call team</Label>
        <TextArea id={`${id}-notes`} name='notes' rows={3} fullWidth />
      </div>
    </form>
  );
}

Форма/интеграция: оборачивающая подпись без id

Если обернуть контрол в Label, связь возникает неявно — ни htmlFor, ни id не нужны. Подходит для контролов из одного нативного элемента, вроде TextArea и Radio; TextField рендерит обёртку <div>, которую подпись содержать не должна, а корни Checkbox и Switch уже сами являются подписями.

wrapping-label.tsx · 22 строки
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 />
          Morning, 8:00–12:00
        </Label>
        <Label fontWeight={400} style={{ display: 'flex', alignItems: 'center', gap: 'var(--spacing-4)' }}>
          <Radio name='window' value='evening' />
          Evening, 17:00–21:00
        </Label>
      </div>
    </div>
  );
}

Раскладка/оверлей

Раскладка/оверлей: подписи слева от полей

Сетка из двух колонок ставит подписи слева от полей. display: contents на каждой строке оставляет подпись и поле прямыми элементами сетки, а длинные подписи переносятся внутри своей колонки и не сдвигают поле.

side-by-side-labels.tsx · 34 строки
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();

  return (
    <div
      style={{
        display: 'grid',
        gridTemplateColumns: 'minmax(96px, 160px) minmax(0, 1fr)',
        alignItems: 'center',
        columnGap: 'var(--spacing-8)',
        rowGap: 'var(--spacing-6)',
        width: '100%',
      }}
    >
      {FIELDS.map((field) => (
        <div key={field.name} style={{ display: 'contents' }}>
          <Label htmlFor={`${id}-${field.name}`} size='sm'>{field.label}</Label>
          <TextField>
            <TextFieldInput id={`${id}-${field.name}`} name={field.name} autoComplete={field.autoComplete} />
          </TextField>
        </div>
      ))}
    </div>
  );
}

Ошибка

Ошибка: отметка обязательности и сообщение об ошибке

Звёздочка декоративна и скрыта от скринридера; обязательность озвучивает required на инпуте. Подпись при ошибке не меняется — ошибку несёт сообщение под полем, связанное через aria-describedby.

required-field-error.tsx · 40 строк
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();
    setSubmitted(true);
  };

  return (
    <form noValidate onSubmit={onSubmit} style={{ display: 'grid', gap: 'var(--spacing-12)', width: '100%' }}>
      <div style={{ display: 'grid', gap: 'var(--spacing-3)' }}>
        <Label htmlFor={id}>
          VAT number <span aria-hidden style={{ color: 'var(--color-error)' }}>*</span>
        </Label>
        <TextField error={invalid}>
          <TextFieldInput
            id={id}
            name='vat'
            required
            value={taxId}
            aria-invalid={invalid}
            aria-describedby={messageId}
            onChange={(event) => setTaxId(event.currentTarget.value)}
          />
        </TextField>
        <HelperText id={messageId} error={invalid}>
          {invalid ? 'Enter the VAT number to issue tax invoices.' : 'Printed on every invoice, for example DE123456789.'}
        </HelperText>
      </div>
      <Button type='submit' style={{ justifySelf: 'start' }}>Save billing details</Button>
    </form>
  );
}

Отключённый

Отключённый: подпись отключённого поля

У Label нет собственного отключённого стиля, поэтому приглушайте её вместе с полем, которое она называет. Здесь чекбокс отключает поле адреса, и подпись следует за ним.

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

export default function LabelForDisabledField() {
  const id = useId();
  const [sameAsCompany, setSameAsCompany] = useState(true);

  return (
    <div style={{ display: 'grid', gap: 'var(--spacing-8)', width: '100%' }}>
      <Checkbox style={{ display: 'flex', alignItems: 'center', gap: 'var(--spacing-4)' }}>
        <CheckboxInput checked={sameAsCompany} onChange={(event) => setSameAsCompany(event.currentTarget.checked)} />
        <CheckboxIndicator>
          <CheckboxCheckedIcon />
        </CheckboxIndicator>
        Ship to the company address
      </Checkbox>
      <div style={{ display: 'grid', gap: 'var(--spacing-3)' }}>
        <Label
          htmlFor={id}
          style={{ color: sameAsCompany ? 'var(--label-tertiary)' : undefined, cursor: sameAsCompany ? 'not-allowed' : undefined }}
        >
          Shipping address
        </Label>
        <TextField>
          <TextFieldInput id={id} name='shippingAddress' autoComplete='shipping street-address' disabled={sameAsCompany} />
        </TextField>
      </div>
    </div>
  );
}

Пограничный случай

Пограничный случай: визуально скрытая подпись

Если в дизайне нет места для видимой подписи, оставьте настоящий <label> и скройте его через VisuallyHidden. Скринридеры и голосовое управление по-прежнему получают имя, а плейсхолдер остаётся лишь примером.

visually-hidden-label.tsx · 18 строк
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 }}>
        <TextFieldInput id={id} type='search' name='q' placeholder='How do I export invoices?' />
      </TextField>
      <Button type='submit'>Search</Button>
    </form>
  );
}

Анатомия

Обзор

Label — примитив из одной части. Он рендерит семантический <label> и заимствует типографику у Text.

Обязательные части

Label

Обязательна. Это одновременно семантический корень и видимая подпись.

Необязательные части

Экспортируемых дочерних частей нет.

Порядок композиции

  1. Label
  2. содержимое подписи

Допустимые варианты композиции

field-label.tsx · 15 строк
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>
    </div>
  );
}

Недопустимые варианты композиции

Плейсхолдер вместо подписи

Так пропадает постоянная видимая подпись.

Несколько несвязанных элементов управления в одной подписи

Поведение клика и семантика становятся неясными.