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

HelperText

HelperText рендерит поясняющий текст к полю: подсказки, указания по использованию и сообщения рядом с валидацией.

At least 8 characters.

helper-text.tsx · 14 строк
import {
  HelperText,
  Label,
  TextField,
  TextFieldInput,
} from '@vega-ui/react';

<div>
  <Label htmlFor='preview-helper-password'>Password</Label>
  <TextField>
    <TextFieldInput id='preview-helper-password' type='password' defaultValue='secret' aria-describedby='preview-helper-hint' />
  </TextField>
  <HelperText id='preview-helper-hint'>At least 8 characters.</HelperText>
</div>
Поддержка браузеров
Широко доступно
  • Chrome 111
  • Edge 111
  • Firefox 113
  • Safari 15.4
  • Safari на iOS 15.4
Размер в бандле
История версий
Добавлен в v1.0.0

Кратко

HelperText рендерит поясняющий текст к полю: подсказки, указания по использованию и сообщения рядом с валидацией. Он переиспользует типографический слой Text и добавляет семантический вспомогательный цвет или цвет ошибки для поясняющего содержимого на уровне поля.

Импорт

import { HelperText, type HelperTextProps } from '@vega-ui/react';

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

  • HelperTextProps

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

field-hint.tsx · 17 строк
import { HelperText, Label, TextField, TextFieldInput } from '@vega-ui/react';
import { useId } from 'react';

export default function FieldHint() {
  const id = useId();
  const hintId = `${id}-hint`;

  return (
    <div style={{ display: 'grid', gap: 'var(--spacing-3)', width: '100%' }}>
      <Label htmlFor={id}>Recovery email</Label>
      <TextField>
        <TextFieldInput id={id} type='email' autoComplete='email' aria-describedby={hintId} />
      </TextField>
      <HelperText id={hintId}>Used only if you lose access to your main inbox.</HelperText>
    </div>
  );
}

Варианты

  • Размер: sm, md, lg
  • Состояние: обычный вспомогательный текст или error
  • Размещение: под полями, селекторами или группами элементов

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

  • error меняет тон, но самому полю всё равно нужно собственное невалидное состояние.
  • Вспомогательный текст должен быть достаточно коротким, чтобы его можно было охватить взглядом вместе с элементом управления.
  • Длинному поясняющему тексту обычно место в основном тексте, а не во вспомогательном.
  • Компонент обслуживает один элемент или одно сгруппированное поле; он не должен превращаться в свалку для несвязанных инструкций к форме.

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

  • Повтор содержимого, которое уже есть в подписи.
  • Использование вспомогательного текста как единственного индикатора ошибки.
  • Абзацы политики или юридического текста в стиле вспомогательного текста.

Примеры

Базовый

Базовый: подсказка, связанная с полем

Дайте HelperText id и укажите его в aria-describedby инпута — иначе скринридер не свяжет подсказку с полем. Подсказка зачитывается после подписи, когда инпут получает фокус.

field-hint.tsx · 17 строк
import { HelperText, Label, TextField, TextFieldInput } from '@vega-ui/react';
import { useId } from 'react';

export default function FieldHint() {
  const id = useId();
  const hintId = `${id}-hint`;

  return (
    <div style={{ display: 'grid', gap: 'var(--spacing-3)', width: '100%' }}>
      <Label htmlFor={id}>Recovery email</Label>
      <TextField>
        <TextFieldInput id={id} type='email' autoComplete='email' aria-describedby={hintId} />
      </TextField>
      <HelperText id={hintId}>Used only if you lose access to your main inbox.</HelperText>
    </div>
  );
}

Контролируемый/с состоянием

Контролируемый/с состоянием: оставшиеся символы

Счётчик вычисляется из значения на каждое нажатие и на лимите переходит в тон ошибки. Озвучивать каждое нажатие было бы шумно, поэтому отдельная скрытая статусная область говорит только на последних десяти символах.

remaining-characters.tsx · 30 строк
import { HelperText, Label, TextField, TextFieldInput, VisuallyHidden } from '@vega-ui/react';
import { useId, useState } from 'react';

const MAX_LENGTH = 50;

export default function RemainingCharacters() {
  const id = useId();
  const [subject, setSubject] = useState('Your March invoice is ready');
  const remaining = MAX_LENGTH - subject.length;
  const message = remaining === 1 ? '1 character left' : `${remaining} characters left`;

  return (
    <div style={{ display: 'grid', gap: 'var(--spacing-3)', width: '100%' }}>
      <Label htmlFor={id}>Email subject</Label>
      <TextField>
        <TextFieldInput
          id={id}
          value={subject}
          maxLength={MAX_LENGTH}
          aria-describedby={`${id}-count`}
          onChange={(event) => setSubject(event.currentTarget.value)}
        />
      </TextField>
      <HelperText id={`${id}-count`} error={remaining === 0} style={{ fontVariantNumeric: 'tabular-nums' }}>{message}</HelperText>
      <VisuallyHidden>
        <div role='status'>{remaining <= 10 ? message : ''}</div>
      </VisuallyHidden>
    </div>
  );
}

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

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

Одна подсказка для нескольких чекбоксов относится к группе: Fieldset ссылается на неё через aria-describedby. При отправке FormData.getAll собирает все отмеченные каналы.

group-hint.tsx · 47 строк
import {
  Button,
  Checkbox,
  CheckboxCheckedIcon,
  CheckboxIndicator,
  CheckboxInput,
  Fieldset,
  FieldsetLegend,
  HelperText,
  Text,
} from '@vega-ui/react';
import { FormEvent, useId, useState } from 'react';

const CHANNELS = ['Email', 'Slack', 'SMS'];

export default function GroupHint() {
  const id = useId();
  const [saved, setSaved] = useState('');

  const onSubmit = (event: FormEvent<HTMLFormElement>) => {
    event.preventDefault();
    const channels = new FormData(event.currentTarget).getAll('channels');
    setSaved(channels.length === 0 ? 'Alerts turned off' : `Alerts via ${channels.join(', ')}`);
  };

  return (
    <form onSubmit={onSubmit} style={{ display: 'grid', gap: 'var(--spacing-12)', width: '100%' }}>
      <Fieldset aria-describedby={`${id}-hint`}>
        <FieldsetLegend>Incident alerts</FieldsetLegend>
        <HelperText id={`${id}-hint`}>We send each alert to every channel you pick.</HelperText>
        {CHANNELS.map((channel) => (
          <Checkbox key={channel} style={{ display: 'flex', alignItems: 'center', gap: 'var(--spacing-4)' }}>
            <CheckboxInput name='channels' value={channel} defaultChecked={channel === 'Email'} />
            <CheckboxIndicator>
              <CheckboxCheckedIcon />
            </CheckboxIndicator>
            {channel}
          </Checkbox>
        ))}
      </Fieldset>
      <div style={{ display: 'flex', alignItems: 'center', gap: 'var(--spacing-6)' }}>
        <Button type='submit'>Save</Button>
        <Text size={2} role='status' style={{ color: 'var(--label-secondary)' }}>{saved}</Text>
      </div>
    </form>
  );
}

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

Раскладка/оверлей: зарезервированное место под сообщение

Место под сообщение держит minHeight в две строки, поэтому появившаяся ошибка не сдвигает следующую строку и не ломает выравнивание соседних полей.

reserved-message-space.tsx · 35 строк
import { HelperText, Label, TextField, TextFieldInput } from '@vega-ui/react';
import { useId, useState } from 'react';

export default function ReservedMessageSpace() {
  const id = useId();
  const [code, setCode] = useState('');
  const invalid = code !== '' && !/^[A-Z]{3}-\d{4}$/.test(code);

  return (
    <div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(160px, 1fr))', gap: 'var(--spacing-8)', width: '100%' }}>
      <div style={{ display: 'grid', gap: 'var(--spacing-3)', alignContent: 'start' }}>
        <Label htmlFor={`${id}-code`}>Booking code</Label>
        <TextField error={invalid}>
          <TextFieldInput
            id={`${id}-code`}
            value={code}
            placeholder='ABC-1234'
            aria-invalid={invalid}
            aria-describedby={`${id}-message`}
            onChange={(event) => setCode(event.currentTarget.value.toUpperCase())}
          />
        </TextField>
        <HelperText id={`${id}-message`} error={invalid} style={{ minHeight: '2lh' }}>
          {invalid ? 'Three letters, a hyphen and four digits.' : ''}
        </HelperText>
      </div>
      <div style={{ display: 'grid', gap: 'var(--spacing-3)', alignContent: 'start' }}>
        <Label htmlFor={`${id}-name`}>Last name</Label>
        <TextField>
          <TextFieldInput id={`${id}-name`} autoComplete='family-name' />
        </TextField>
      </div>
    </div>
  );
}

Ошибка

Ошибка: сообщение из нативной валидности

onInvalid превращает флаги validity браузера в конкретное сообщение и подавляет нативный пузырь. Ошибка идёт первой в aria-describedby, перед постоянной подсказкой, и перепроверяется при вводе, пока не исчезнет.

native-validity-message.tsx · 49 строк
import { Button, HelperText, Label, TextField, TextFieldInput } from '@vega-ui/react';
import { FormEvent, useId, useState } from 'react';

const getMessage = (input: HTMLInputElement) => {
  if (input.validity.valueMissing) return 'Enter a postal code.';
  if (input.validity.patternMismatch) return 'Use 5 digits, for example 10115.';
  return '';
};

export default function NativeValidityMessage() {
  const id = useId();
  const [error, setError] = useState('');
  const invalid = error !== '';

  const onInvalid = (event: FormEvent<HTMLInputElement>) => {
    event.preventDefault();
    setError(getMessage(event.currentTarget));
  };

  const onInput = (event: FormEvent<HTMLInputElement>) => {
    if (!invalid) return;
    setError(getMessage(event.currentTarget));
  };

  return (
    <form onSubmit={(event) => event.preventDefault()} style={{ display: 'grid', gap: 'var(--spacing-12)', width: '100%' }}>
      <div style={{ display: 'grid', gap: 'var(--spacing-3)' }}>
        <Label htmlFor={id}>Postal code</Label>
        <TextField error={invalid}>
          <TextFieldInput
            id={id}
            name='postalCode'
            inputMode='numeric'
            autoComplete='postal-code'
            required
            pattern='\d{5}'
            aria-invalid={invalid}
            aria-describedby={invalid ? `${id}-error ${id}-hint` : `${id}-hint`}
            onInvalid={onInvalid}
            onInput={onInput}
          />
        </TextField>
        {invalid && <HelperText id={`${id}-error`} error>{error}</HelperText>}
        <HelperText id={`${id}-hint`}>We deliver within Germany only.</HelperText>
      </div>
      <Button type='submit' style={{ justifySelf: 'start' }}>Check delivery</Button>
    </form>
  );
}

Отключённый

Отключённый: объясните, почему поле заблокировано

Отключённый инпут нельзя сфокусировать, поэтому причина должна быть видимым текстом, а не тултипом. Подсказка объясняет, почему поле заблокировано, и ведёт к решению.

explain-disabled-field.tsx · 18 строк
import { HelperText, Label, Link, TextField, TextFieldInput } from '@vega-ui/react';
import { useId } from 'react';

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

  return (
    <div style={{ display: 'grid', gap: 'var(--spacing-3)', width: '100%' }}>
      <Label htmlFor={id} style={{ color: 'var(--label-tertiary)' }}>Custom domain</Label>
      <TextField>
        <TextFieldInput id={id} placeholder='docs.northwind.io' disabled aria-describedby={`${id}-hint`} />
      </TextField>
      <HelperText id={`${id}-hint`}>
        Custom domains are available on the Business plan. <Link href='#pricing'>Compare plans</Link>
      </HelperText>
    </div>
  );
}

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

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

HelperText — inline-block, поэтому длинный URL вылезет за узкую колонку. overflowWrap: 'anywhere' позволяет переносить его в любом месте.

long-unbroken-hint.tsx · 18 строк
import { HelperText, Label, TextField, TextFieldInput } from '@vega-ui/react';
import { useId } from 'react';

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

  return (
    <div style={{ display: 'grid', gap: 'var(--spacing-3)', width: '100%', maxWidth: 240 }}>
      <Label htmlFor={id}>Webhook URL</Label>
      <TextField>
        <TextFieldInput id={id} type='url' aria-describedby={`${id}-hint`} />
      </TextField>
      <HelperText id={`${id}-hint`} style={{ overflowWrap: 'anywhere' }}>
        Default: https://hooks.northwind-logistics.example.com/v2/events/shipments/status-changed
      </HelperText>
    </div>
  );
}

Анатомия

Обзор

HelperText — примитив из одной части. Он рендерит стилизованный абзац через общий контракт Text.

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

HelperText

Обязательна. Отвечает за размер вспомогательного текста и приглушённый цвет или цвет ошибки.

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

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

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

  1. HelperText
  2. короткий поясняющий текст

Корректные варианты композиции

field-hint.tsx · 17 строк
import { HelperText, Label, TextField, TextFieldInput } from '@vega-ui/react';
import { useId } from 'react';

export default function FieldHint() {
  const id = useId();
  const hintId = `${id}-hint`;

  return (
    <div style={{ display: 'grid', gap: 'var(--spacing-3)', width: '100%' }}>
      <Label htmlFor={id}>Recovery email</Label>
      <TextField>
        <TextFieldInput id={id} type='email' autoComplete='email' aria-describedby={hintId} />
      </TextField>
      <HelperText id={hintId}>Used only if you lose access to your main inbox.</HelperText>
    </div>
  );
}

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

Вспомогательный текст в роли основной подписи

Это ослабляет видимую подпись и семантику.

Вспомогательный текст для полного текста политики

Компонент слишком лёгкий для длинного поясняющего содержимого.