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

PasswordField

PasswordField — составной компонент на основе TextField.
password-field.tsx · 19 строк
import {
  Label,
  PasswordField,
  PasswordFieldHiddenIcon,
  PasswordFieldInput,
  PasswordFieldShownIcon,
  PasswordFieldToggleIconButton,
} from '@vega-ui/react';

<div>
  <Label htmlFor='preview-password'>Password</Label>
  <PasswordField>
    <PasswordFieldInput id='preview-password' defaultValue='hunter22' />
    <PasswordFieldToggleIconButton aria-label='Show password'>
      <PasswordFieldShownIcon />
      <PasswordFieldHiddenIcon />
    </PasswordFieldToggleIconButton>
  </PasswordField>
</div>
Поддержка браузеров
Недавно доступно, 2024
  • Chrome 125
  • Edge 125
  • Firefox 128
  • Safari 18
  • Safari на iOS 18
Размер в бандле
История версий
Добавлен в v1.0.0, последнее изменение в v3.0.0

Кратко

PasswordField — составной компонент на основе TextField. Он добавляет состояние видимости пароля, кнопку-переключатель и части-иконки, сохраняя нативное поле ввода. Это слой для ввода секретов, когда проверка скрытого значения входит в контракт поля.

Импорт

import {
  PasswordField,
  PasswordFieldHiddenIcon,
  PasswordFieldInput,
  PasswordFieldShownIcon,
  PasswordFieldToggleIconButton,
  type PasswordFieldHiddenIconProps,
  type PasswordFieldInputProps,
  type PasswordFieldProps,
  type PasswordFieldShownIconProps,
  type PasswordFieldToggleIconButtonProps,
} from '@vega-ui/react';

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

  • PasswordFieldProps
  • PasswordFieldInputProps
  • PasswordFieldToggleIconButtonProps
  • PasswordFieldShownIconProps
  • PasswordFieldHiddenIconProps

Минимальная композиция

sign-in-password.tsx · 26 строк
import {
  Label,
  PasswordField,
  PasswordFieldHiddenIcon,
  PasswordFieldInput,
  PasswordFieldShownIcon,
  PasswordFieldToggleIconButton,
} from '@vega-ui/react';
import { useId } from 'react';

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

  return (
    <div style={{ display: 'grid', gap: 'var(--spacing-3)', width: '100%' }}>
      <Label htmlFor={id}>Password</Label>
      <PasswordField>
        <PasswordFieldInput id={id} name='password' autoComplete='current-password' />
        <PasswordFieldToggleIconButton aria-label='Show password'>
          <PasswordFieldShownIcon />
          <PasswordFieldHiddenIcon />
        </PasswordFieldToggleIconButton>
      </PasswordField>
    </div>
  );
}

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

  • PasswordField: корневой контейнер состояния
  • PasswordFieldInput: нативное поле ввода пароля
  • PasswordFieldToggleIconButton: переключатель видимости

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

  • PasswordFieldShownIcon: иконка состояния «показан»
  • PasswordFieldHiddenIcon: иконка состояния «скрыт»

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

  1. PasswordField
  2. PasswordFieldInput
  3. PasswordFieldToggleIconButton
  4. части-иконки «показан» и «скрыт»

Варианты

  • Размер: наследуется от TextField
  • Состояние: пароль скрыт или показан, отключённое состояние и ошибка — через пропсы корневого поля
  • Композиция: стандартные иконки или собственное содержимое иконок

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

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

Типичные ошибки

  • Переключатель без доступного имени.
  • Предположение, что подсказки о надёжности пароля — то же самое, что настоящая политика валидации.
  • Замена частей составного компонента собственными обёртками до того, как заработает базовая композиция.

Примеры

Базовый

Базовый: пароль для входа

id, name и autoComplete='current-password' ставятся на PasswordFieldInput, чтобы Label и менеджеры паролей нашли настоящий инпут. У переключателя нет встроенного имени: дайте ему постоянный aria-label, например «Показать пароль», — состояние он уже сообщает через aria-pressed.

sign-in-password.tsx · 26 строк
import {
  Label,
  PasswordField,
  PasswordFieldHiddenIcon,
  PasswordFieldInput,
  PasswordFieldShownIcon,
  PasswordFieldToggleIconButton,
} from '@vega-ui/react';
import { useId } from 'react';

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

  return (
    <div style={{ display: 'grid', gap: 'var(--spacing-3)', width: '100%' }}>
      <Label htmlFor={id}>Password</Label>
      <PasswordField>
        <PasswordFieldInput id={id} name='password' autoComplete='current-password' />
        <PasswordFieldToggleIconButton aria-label='Show password'>
          <PasswordFieldShownIcon />
          <PasswordFieldHiddenIcon />
        </PasswordFieldToggleIconButton>
      </PasswordField>
    </div>
  );
}

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

Контролируемый/с состоянием: живой список требований

Значением владеет родитель и проверяет каждое правило на каждое нажатие. Список связан с инпутом через aria-describedby, а счётчик ниже — статусная область.

new-password-checklist.tsx · 72 строки
import {
  Icon,
  Label,
  PasswordField,
  PasswordFieldHiddenIcon,
  PasswordFieldInput,
  PasswordFieldShownIcon,
  PasswordFieldToggleIconButton,
  Text,
} from '@vega-ui/react';
import { CheckIcon, CircleIcon } from '@vega-ui/icons';
import { useId, useState } from 'react';

const RULES = [
  { label: 'At least 12 characters', test: (value: string) => value.length >= 12 },
  { label: 'Upper and lower case letters', test: (value: string) => /[a-z]/.test(value) && /[A-Z]/.test(value) },
  { label: 'At least one number', test: (value: string) => /\d/.test(value) },
  { label: 'At least one symbol', test: (value: string) => /[^A-Za-z0-9]/.test(value) },
];

export default function NewPasswordChecklist() {
  const id = useId();
  const rulesId = `${id}-rules`;
  const [password, setPassword] = useState('');
  const passed = RULES.filter((rule) => rule.test(password)).length;

  return (
    <div style={{ display: 'grid', gap: 'var(--spacing-6)', width: '100%' }}>
      <div style={{ display: 'grid', gap: 'var(--spacing-3)' }}>
        <Label htmlFor={id}>New password</Label>
        <PasswordField>
          <PasswordFieldInput
            id={id}
            name='newPassword'
            autoComplete='new-password'
            value={password}
            aria-describedby={rulesId}
            onChange={(event) => setPassword(event.currentTarget.value)}
          />
          <PasswordFieldToggleIconButton aria-label='Show password'>
            <PasswordFieldShownIcon />
            <PasswordFieldHiddenIcon />
          </PasswordFieldToggleIconButton>
        </PasswordField>
      </div>
      <ul id={rulesId} style={{ display: 'grid', gap: 'var(--spacing-3)', margin: 0, padding: 0, listStyle: 'none' }}>
        {RULES.map((rule) => {
          const ok = rule.test(password);
          return (
            <li
              key={rule.label}
              style={{
                display: 'flex',
                alignItems: 'center',
                gap: 'var(--spacing-4)',
                color: ok ? 'var(--label-primary)' : 'var(--label-tertiary)',
              }}
            >
              <Icon size='xs' aria-hidden style={{ flexShrink: 0 }}>
                {ok ? <CheckIcon strokeWidth={2.5} /> : <CircleIcon />}
              </Icon>
              <Text size={2} style={{ color: 'inherit' }}>{rule.label}</Text>
            </li>
          );
        })}
      </ul>
      <Text size={2} fontWeight={500} role='status' style={{ fontVariantNumeric: 'tabular-nums' }}>
        {passed} of {RULES.length} requirements met
      </Text>
    </div>
  );
}

Контролируемый/с состоянием: один переключатель для двух полей

shown и onChangeShown поднимают видимость в родителя. Переключатель в первом поле показывает оба пароля, а у поля подтверждения своего переключателя нет.

one-toggle-for-two-fields.tsx · 35 строк
import {
  Label,
  PasswordField,
  PasswordFieldHiddenIcon,
  PasswordFieldInput,
  PasswordFieldShownIcon,
  PasswordFieldToggleIconButton,
} from '@vega-ui/react';
import { useId, useState } from 'react';

export default function OneToggleForTwoFields() {
  const id = useId();
  const [shown, setShown] = useState(false);

  return (
    <div style={{ display: 'grid', gap: 'var(--spacing-8)', width: '100%' }}>
      <div style={{ display: 'grid', gap: 'var(--spacing-3)' }}>
        <Label htmlFor={`${id}-new`}>New password</Label>
        <PasswordField shown={shown} onChangeShown={setShown}>
          <PasswordFieldInput id={`${id}-new`} autoComplete='new-password' />
          <PasswordFieldToggleIconButton aria-label='Show passwords'>
            <PasswordFieldShownIcon />
            <PasswordFieldHiddenIcon />
          </PasswordFieldToggleIconButton>
        </PasswordField>
      </div>
      <div style={{ display: 'grid', gap: 'var(--spacing-3)' }}>
        <Label htmlFor={`${id}-confirm`}>Repeat new password</Label>
        <PasswordField shown={shown}>
          <PasswordFieldInput id={`${id}-confirm`} autoComplete='new-password' />
        </PasswordField>
      </div>
    </div>
  );
}

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

Форма/интеграция: форма входа

Поле email с autoComplete='username' рядом с паролем позволяет менеджерам паролей сохранять и подставлять оба значения. Форма читает их через FormData и при отправке снова скрывает пароль, чтобы он не оставался на экране открытым текстом.

sign-in-form.tsx · 50 строк
import {
  Button,
  Label,
  PasswordField,
  PasswordFieldHiddenIcon,
  PasswordFieldInput,
  PasswordFieldShownIcon,
  PasswordFieldToggleIconButton,
  Text,
  TextField,
  TextFieldInput,
} from '@vega-ui/react';
import { FormEvent, useId, useState } from 'react';

export default function SignInForm() {
  const id = useId();
  const [shown, setShown] = useState(false);
  const [status, setStatus] = useState('');

  const onSubmit = (event: FormEvent<HTMLFormElement>) => {
    event.preventDefault();
    setShown(false);
    const data = new FormData(event.currentTarget);
    const password = String(data.get('password'));
    setStatus(`Signing in as ${data.get('email')} with a ${password.length}-character password…`);
  };

  return (
    <form onSubmit={onSubmit} style={{ display: 'grid', gap: 'var(--spacing-8)', width: '100%' }}>
      <div style={{ display: 'grid', gap: 'var(--spacing-3)' }}>
        <Label htmlFor={`${id}-email`}>Email</Label>
        <TextField>
          <TextFieldInput id={`${id}-email`} name='email' type='email' autoComplete='username' required />
        </TextField>
      </div>
      <div style={{ display: 'grid', gap: 'var(--spacing-3)' }}>
        <Label htmlFor={`${id}-password`}>Password</Label>
        <PasswordField shown={shown} onChangeShown={setShown}>
          <PasswordFieldInput id={`${id}-password`} name='password' autoComplete='current-password' required />
          <PasswordFieldToggleIconButton aria-label='Show password'>
            <PasswordFieldShownIcon />
            <PasswordFieldHiddenIcon />
          </PasswordFieldToggleIconButton>
        </PasswordField>
      </div>
      <Button type='submit' style={{ marginBlockStart: 'var(--spacing-4)' }}>Sign in</Button>
      <Text size={2} role='status' style={{ color: 'var(--label-secondary)', overflowWrap: 'anywhere' }}>{status}</Text>
    </form>
  );
}

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

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

При неверном пароле диалог остаётся открытым и показывает ошибку под полем; верный пароль закрывает его через контролируемый open. Ошибка сбрасывается при каждом закрытии. Попробуйте vega-demo.

confirm-in-dialog.tsx · 101 строка
import type { CSSProperties, FormEvent } from 'react';
import {
  Button,
  Dialog,
  DialogClose,
  DialogContent,
  DialogTrigger,
  Heading,
  HelperText,
  Icon,
  IconButton,
  Label,
  PasswordField,
  PasswordFieldHiddenIcon,
  PasswordFieldInput,
  PasswordFieldShownIcon,
  PasswordFieldToggleIconButton,
  Text,
} from '@vega-ui/react';
import { XIcon } from '@vega-ui/icons';
import { useId, useState } from 'react';

export default function ConfirmInDialog() {
  const id = useId();
  const [open, setOpen] = useState(false);
  const [error, setError] = useState('');
  const [status, setStatus] = useState('');

  const changeOpen = (next: boolean) => {
    setOpen(next);
    setError('');
  };

  const onSubmit = (event: FormEvent<HTMLFormElement>) => {
    event.preventDefault();
    const password = String(new FormData(event.currentTarget).get('password'));
    if (password !== 'vega-demo') {
      setError('Wrong password. Hint: vega-demo');
      return;
    }

    setStatus('Recovery codes regenerated.');
    changeOpen(false);
  };

  return (
    <div style={{ display: 'grid', gap: 'var(--spacing-6)', justifyItems: 'center', width: '100%' }}>
      <Dialog open={open} onChangeOpen={changeOpen}>
        <DialogTrigger asChild>
          <Button variant='secondary' appearance='ghost'>Regenerate recovery codes</Button>
        </DialogTrigger>
        <DialogContent aria-labelledby={`${id}-title`} aria-describedby={`${id}-text`} style={{ width: 'min(400px, 100%)' }}>
          <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 'var(--spacing-8)' }}>
            <Heading id={`${id}-title`} as='h2' size={4} fontWeight={600}>Confirm it’s you</Heading>
            <DialogClose asChild>
              <IconButton
                aria-label='Close'
                variant='secondary'
                appearance='ghost'
                style={{ flexShrink: 0, '--icon-button-size': '24px', '--icon-button-br-ratio': 0.5 } as CSSProperties}
              >
                <Icon size='xs'><XIcon strokeWidth={2.5} /></Icon>
              </IconButton>
            </DialogClose>
          </div>
          <Text id={`${id}-text`} size={3} style={{ color: 'var(--label-secondary)', marginBlockStart: 'var(--spacing-2)' }}>
            Old recovery codes stop working right away.
          </Text>
          <form onSubmit={onSubmit} style={{ display: 'grid', gap: 'var(--spacing-12)', marginBlockStart: 'var(--spacing-8)' }}>
            <div style={{ display: 'grid', gap: 'var(--spacing-3)' }}>
              <Label htmlFor={`${id}-password`}>Password</Label>
              <PasswordField error={error !== ''}>
                <PasswordFieldInput
                  id={`${id}-password`}
                  name='password'
                  autoComplete='current-password'
                  required
                  autoFocus
                  aria-invalid={error !== ''}
                  aria-describedby={`${id}-error`}
                />
                <PasswordFieldToggleIconButton aria-label='Show password'>
                  <PasswordFieldShownIcon />
                  <PasswordFieldHiddenIcon />
                </PasswordFieldToggleIconButton>
              </PasswordField>
              <HelperText id={`${id}-error`} error>{error}</HelperText>
            </div>
            <div style={{ display: 'flex', justifyContent: 'flex-end', gap: 'var(--spacing-4)' }}>
              <DialogClose asChild>
                <Button type='button' variant='secondary' appearance='ghost'>Cancel</Button>
              </DialogClose>
              <Button type='submit'>Confirm</Button>
            </div>
          </form>
        </DialogContent>
      </Dialog>
      <Text size={2} role='status' style={{ color: 'var(--label-secondary)', textAlign: 'center' }}>{status}</Text>
    </div>
  );
}

Ошибка

Ошибка: подтверждение не совпадает

Несовпадение вычисляется из обоих значений, но показывается только после потери фокуса полем подтверждения или отправки формы. error красит поле, а aria-invalid и aria-describedby сообщают скринридеру, что не так.

confirm-password-mismatch.tsx · 67 строк
import {
  Button,
  HelperText,
  Label,
  PasswordField,
  PasswordFieldHiddenIcon,
  PasswordFieldInput,
  PasswordFieldShownIcon,
  PasswordFieldToggleIconButton,
} from '@vega-ui/react';
import { FormEvent, useId, useState } from 'react';

export default function ConfirmPasswordMismatch() {
  const id = useId();
  const messageId = `${id}-message`;
  const [password, setPassword] = useState('Correct-horse-42');
  const [confirmation, setConfirmation] = useState('Correct-horse-24');
  const [touched, setTouched] = useState(false);
  const invalid = touched && confirmation !== password;

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

  return (
    <form onSubmit={onSubmit} style={{ display: 'grid', gap: 'var(--spacing-8)', width: '100%' }}>
      <div style={{ display: 'grid', gap: 'var(--spacing-3)' }}>
        <Label htmlFor={`${id}-new`}>New password</Label>
        <PasswordField>
          <PasswordFieldInput
            id={`${id}-new`}
            autoComplete='new-password'
            value={password}
            onChange={(event) => setPassword(event.currentTarget.value)}
          />
          <PasswordFieldToggleIconButton aria-label='Show password'>
            <PasswordFieldShownIcon />
            <PasswordFieldHiddenIcon />
          </PasswordFieldToggleIconButton>
        </PasswordField>
      </div>
      <div style={{ display: 'grid', gap: 'var(--spacing-3)' }}>
        <Label htmlFor={`${id}-confirm`}>Repeat new password</Label>
        <PasswordField error={invalid}>
          <PasswordFieldInput
            id={`${id}-confirm`}
            autoComplete='new-password'
            value={confirmation}
            aria-invalid={invalid}
            aria-describedby={messageId}
            onChange={(event) => setConfirmation(event.currentTarget.value)}
            onBlur={() => setTouched(true)}
          />
          <PasswordFieldToggleIconButton aria-label='Show password'>
            <PasswordFieldShownIcon />
            <PasswordFieldHiddenIcon />
          </PasswordFieldToggleIconButton>
        </PasswordField>
        <HelperText id={messageId} error={invalid}>
          {invalid ? 'Passwords do not match. Check the last characters.' : 'Type the same password again.'}
        </HelperText>
      </div>
      <Button type='submit' style={{ justifySelf: 'start', marginBlockStart: 'var(--spacing-4)' }}>Update password</Button>
    </form>
  );
}

Отключённый

Отключённый: пароль управляется через SSO

disabled на корне отключает и инпут, и переключатель. Подсказка объясняет, почему поле заблокировано и где сменить пароль.

managed-by-sso.tsx · 28 строк
import {
  HelperText,
  Label,
  PasswordField,
  PasswordFieldHiddenIcon,
  PasswordFieldInput,
  PasswordFieldShownIcon,
  PasswordFieldToggleIconButton,
} from '@vega-ui/react';
import { useId } from 'react';

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

  return (
    <div style={{ display: 'grid', gap: 'var(--spacing-3)', width: '100%' }}>
      <Label htmlFor={id}>Password</Label>
      <PasswordField disabled>
        <PasswordFieldInput id={id} placeholder='Managed by Okta' aria-describedby={`${id}-hint`} />
        <PasswordFieldToggleIconButton aria-label='Show password'>
          <PasswordFieldShownIcon />
          <PasswordFieldHiddenIcon />
        </PasswordFieldToggleIconButton>
      </PasswordField>
      <HelperText id={`${id}-hint`}>Your organization signs you in with Okta. Change the password there.</HelperText>
    </div>
  );
}

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

Пограничный случай: предупреждение о Caps Lock

Скрытый пароль прячет опечатки, поэтому предупредите, когда включён Caps Lock. getModifierState('CapsLock') читается в обработчиках клавиш, а предупреждение — вежливая live-область, связанная с инпутом.

caps-lock-warning.tsx · 42 строки
import {
  HelperText,
  Label,
  PasswordField,
  PasswordFieldHiddenIcon,
  PasswordFieldInput,
  PasswordFieldShownIcon,
  PasswordFieldToggleIconButton,
} from '@vega-ui/react';
import { KeyboardEvent, useId, useState } from 'react';

export default function CapsLockWarning() {
  const id = useId();
  const [capsLock, setCapsLock] = useState(false);

  const onKey = (event: KeyboardEvent<HTMLInputElement>) => {
    setCapsLock(event.getModifierState('CapsLock'));
  };

  return (
    <div style={{ display: 'grid', gap: 'var(--spacing-3)', width: '100%' }}>
      <Label htmlFor={id}>Password</Label>
      <PasswordField>
        <PasswordFieldInput
          id={id}
          autoComplete='current-password'
          aria-describedby={`${id}-caps`}
          onKeyDown={onKey}
          onKeyUp={onKey}
          onBlur={() => setCapsLock(false)}
        />
        <PasswordFieldToggleIconButton aria-label='Show password'>
          <PasswordFieldShownIcon />
          <PasswordFieldHiddenIcon />
        </PasswordFieldToggleIconButton>
      </PasswordField>
      <HelperText id={`${id}-caps`} aria-live='polite'>
        {capsLock ? 'Caps Lock is on.' : ''}
      </HelperText>
    </div>
  );
}

Анатомия

Обзор

PasswordField расширяет контракт композиции TextField одной дополнительной моделью состояния: показан ли пароль сейчас или скрыт.

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

PasswordField

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

PasswordFieldInput

Обязательная. Переключается между type='password' и type='text' в зависимости от контекста.

PasswordFieldToggleIconButton

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

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

PasswordFieldShownIcon

Необязательная иконка для состояния «показан».

PasswordFieldHiddenIcon

Необязательная иконка для состояния «скрыт».

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

  1. PasswordField
  2. PasswordFieldInput
  3. PasswordFieldToggleIconButton
  4. PasswordFieldShownIcon
  5. PasswordFieldHiddenIcon

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

sign-in-password.tsx · 26 строк
import {
  Label,
  PasswordField,
  PasswordFieldHiddenIcon,
  PasswordFieldInput,
  PasswordFieldShownIcon,
  PasswordFieldToggleIconButton,
} from '@vega-ui/react';
import { useId } from 'react';

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

  return (
    <div style={{ display: 'grid', gap: 'var(--spacing-3)', width: '100%' }}>
      <Label htmlFor={id}>Password</Label>
      <PasswordField>
        <PasswordFieldInput id={id} name='password' autoComplete='current-password' />
        <PasswordFieldToggleIconButton aria-label='Show password'>
          <PasswordFieldShownIcon />
          <PasswordFieldHiddenIcon />
        </PasswordFieldToggleIconButton>
      </PasswordField>
    </div>
  );
}

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

Переключатель без поля ввода

Контрол рендерит кнопку, но самого поля пароля нет.

Собственная кнопка без сохранения контракта переключателя

Корневое состояние есть, но пользователь теряет стандартный элемент «показать/скрыть».

Иконки состояний, отрендеренные вне переключателя

Иконки должны оставаться частью одного кликабельного контрола.