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

Alert

Alert показывает короткие сообщения обратной связи или статуса с необязательными иконкой, заголовком и текстом.

Changes saved

alert.tsx · 6 строк
import { Alert, AlertContent, AlertIcon } from '@vega-ui/react';

<Alert variant='success' appearance='surface' style={{ width: '100%' }}>
  <AlertIcon />
  <AlertContent>Changes saved</AlertContent>
</Alert>
Поддержка браузеров
Недавно доступно, 2024
  • Chrome 125
  • Edge 125
  • Firefox 128
  • Safari 18
  • Safari на iOS 18
Размер в бандле
История версий
Добавлен в v1.0.0, последнее изменение в v3.0.0

Кратко

Alert показывает короткие сообщения обратной связи или статуса с необязательными иконкой, заголовком и текстом. Это поверхность обратной связи уровня сообщения: когда Badge слишком мал, а Card слишком нейтральна.

Импорт

import {
  Alert,
  AlertIcon,
  AlertMain,
  AlertTitle,
  AlertContent,
  type AlertProps,
} from '@vega-ui/react';

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

  • AlertProps
  • AlertIconProps
  • AlertMainProps
  • AlertTitleProps
  • AlertContentProps

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

minimal-composition.tsx · 19 строк
import {
  Alert,
  AlertContent,
  AlertIcon,
  AlertMain,
  AlertTitle,
} from '@vega-ui/react';

export default function MinimalComposition() {
  return (
    <Alert variant='success' style={{ width: '100%' }}>
      <AlertIcon />
      <AlertMain>
        <AlertTitle>Saved</AlertTitle>
        <AlertContent>Profile updated successfully.</AlertContent>
      </AlertMain>
    </Alert>
  );
}

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

  • Alert: корневая поверхность обратной связи
  • AlertMain: обёртка основного текста

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

  • AlertIcon: поверхность семантической иконки
  • AlertTitle: более выраженная визуальная иерархия
  • AlertContent: поясняющий текст сообщения

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

  1. Alert
  2. AlertIcon
  3. AlertMain
  4. AlertTitle
  5. AlertContent

Варианты

  • Семантические варианты: success, error, warning, info
  • Внешний вид: fill, surface
  • Композиционные варианты: с иконкой или без, с заголовком или без

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

  • Используйте заголовок, только когда алерту нужна более сильная иерархия.
  • Держите текст коротким; длинному содержимому место в другом компоненте.
  • Подбирайте вариант алерта по смыслу, а не только по цвету.
  • Если содержимое становится пошаговой инструкцией, многоэтапным или сильно интерактивным, алерт обычно уже не подходит.

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

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

Примеры

Базовый

Базовый: четыре варианта

Каждый вариант получает подходящую иконку по умолчанию из AlertIcon. appearance='surface' добавляет рамку для сообщений, которым нужен больший вес, как у отклонённой карты. Эти алерты присутствуют на странице с самого начала, поэтому роль живой области им не нужна.

variants.tsx · 36 строк
import { Alert, AlertContent, AlertIcon, AlertMain, AlertTitle } from '@vega-ui/react';

export default function Variants() {
  return (
    <div style={{ display: 'grid', gap: 'var(--spacing-6)', width: '100%', maxWidth: 560 }}>
      <Alert variant='info'>
        <AlertIcon />
        <AlertMain>
          <AlertTitle>Scheduled maintenance</AlertTitle>
          <AlertContent>The dashboard will be read-only on Sunday from 02:00 to 03:00 UTC.</AlertContent>
        </AlertMain>
      </Alert>
      <Alert variant='success'>
        <AlertIcon />
        <AlertMain>
          <AlertTitle>Domain verified</AlertTitle>
          <AlertContent>Emails from billing@northwind.io will now pass SPF and DKIM checks.</AlertContent>
        </AlertMain>
      </Alert>
      <Alert variant='warning'>
        <AlertIcon />
        <AlertMain>
          <AlertTitle>Storage almost full</AlertTitle>
          <AlertContent>You have used 46.2 GB of 50 GB. Uploads stop when the limit is reached.</AlertContent>
        </AlertMain>
      </Alert>
      <Alert variant='error' appearance='surface'>
        <AlertIcon />
        <AlertMain>
          <AlertTitle>Card declined</AlertTitle>
          <AlertContent>The payment for invoice INV-2048 was declined by the bank. Update the card to keep your plan.</AlertContent>
        </AlertMain>
      </Alert>
    </div>
  );
}

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

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

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

dismissible.tsx · 43 строки
import { useRef, useState } from 'react';
import type { CSSProperties } from 'react';
import { Alert, AlertContent, AlertIcon, AlertMain, AlertTitle, Button, Heading, Icon, IconButton } from '@vega-ui/react';
import { XIcon } from '@vega-ui/icons';

export default function Dismissible() {
  const [visible, setVisible] = useState(true);
  const headingRef = useRef<HTMLHeadingElement>(null);

  const dismiss = () => {
    setVisible(false);
    headingRef.current?.focus();
  };

  return (
    <section aria-labelledby='billing-heading' style={{ display: 'grid', gap: 'var(--spacing-6)', width: '100%', maxWidth: 560 }}>
      <Heading ref={headingRef} id='billing-heading' as='h2' size={4} fontWeight={600} tabIndex={-1}>Billing</Heading>
      {visible && (
        <Alert variant='info'>
          <AlertIcon />
          <AlertMain>
            <AlertTitle>New invoice layout</AlertTitle>
            <AlertContent>Invoices now group usage by project. Old invoices keep their original layout.</AlertContent>
          </AlertMain>
          <IconButton
            aria-label='Dismiss'
            variant='secondary'
            appearance='ghost'
            onClick={dismiss}
            style={{ '--icon-button-size': '24px', '--icon-button-br-ratio': 0.5 } as CSSProperties}
          >
            <Icon size='xs'><XIcon strokeWidth={2.5} /></Icon>
          </IconButton>
        </Alert>
      )}
      {!visible && (
        <Button size='sm' variant='secondary' appearance='ghost' onClick={() => setVisible(true)} style={{ justifySelf: 'start' }}>
          Show the announcement again
        </Button>
      )}
    </section>
  );
}

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

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

Форма читает значения через FormData и имитирует запрос. Успех рендерится в контейнер role='status', который существует до появления сообщения, поэтому скринридер его озвучивает; ошибка — это настойчивый role='alert'.

save-feedback.tsx · 83 строки
import { FormEvent, useState } from 'react';
import {
  Alert,
  AlertContent,
  AlertIcon,
  AlertMain,
  AlertTitle,
  Button,
  Checkbox,
  CheckboxCheckedIcon,
  CheckboxIndicator,
  CheckboxInput,
  Label,
  TextField,
  TextFieldInput,
} from '@vega-ui/react';

type Status = 'idle' | 'saving' | 'saved' | 'failed';

export default function SaveFeedback() {
  const [status, setStatus] = useState<Status>('idle');
  const [savedName, setSavedName] = useState('');

  const submit = (event: FormEvent<HTMLFormElement>) => {
    event.preventDefault();
    const data = new FormData(event.currentTarget);
    const name = String(data.get('workspace'));
    const offline = data.get('offline') === 'on';
    setStatus('saving');
    setTimeout(() => {
      if (offline) {
        setStatus('failed');
        return;
      }
      setSavedName(name);
      setStatus('saved');
    }, 800);
  };

  return (
    <form onSubmit={submit} style={{ display: 'grid', gap: 'var(--spacing-12)', width: '100%', maxWidth: 420 }}>
      <div style={{ display: 'grid', gap: 'var(--spacing-8)' }}>
        <div style={{ display: 'grid', gap: 'var(--spacing-3)' }}>
          <Label htmlFor='workspace-name'>Workspace name</Label>
          <TextField>
            <TextFieldInput id='workspace-name' name='workspace' defaultValue='Northwind Studio' required />
          </TextField>
        </div>
        <Checkbox style={{ display: 'flex', alignItems: 'center', gap: 'var(--spacing-4)' }}>
          <CheckboxInput name='offline' />
          <CheckboxIndicator>
            <CheckboxCheckedIcon />
          </CheckboxIndicator>
          Simulate a network error
        </Checkbox>
      </div>
      <Button type='submit' disabled={status === 'saving'} style={{ justifySelf: 'start' }}>
        {status === 'saving' ? 'Saving…' : 'Save'}
      </Button>
      <div style={{ display: 'grid' }}>
        <div role='status'>
          {status === 'saved' && (
            <Alert variant='success'>
              <AlertIcon />
              <AlertMain>
                <AlertContent>Workspace renamed to “{savedName}”.</AlertContent>
              </AlertMain>
            </Alert>
          )}
        </div>
        {status === 'failed' && (
          <Alert variant='error' role='alert'>
            <AlertIcon />
            <AlertMain>
              <AlertTitle>Changes not saved</AlertTitle>
              <AlertContent>We could not reach the server. Check your connection and save again.</AlertContent>
            </AlertMain>
          </Alert>
        )}
      </div>
    </form>
  );
}

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

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

Предупреждение объясняет последствия разрушительного действия и связано с диалогом через aria-describedby, поэтому зачитывается вместе с заголовком диалога.

delete-dialog.tsx · 63 строки
import type { CSSProperties } from 'react';
import {
  Alert,
  AlertContent,
  AlertIcon,
  AlertMain,
  AlertTitle,
  Button,
  Dialog,
  DialogClose,
  DialogContent,
  DialogTrigger,
  Heading,
  Icon,
  IconButton,
  Text,
} from '@vega-ui/react';
import { XIcon } from '@vega-ui/icons';

const destructive = {
  '--button-background-color': 'var(--color-red-accent-500)',
  '--button-background-color-hover': 'var(--color-red-accent-600)',
  '--button-background-color-active': 'var(--color-red-accent-700)',
} as CSSProperties;

export default function DeleteDialog() {
  return (
    <Dialog>
      <DialogTrigger asChild>
        <Button variant='secondary' appearance='ghost'>Delete project</Button>
      </DialogTrigger>
      <DialogContent aria-labelledby='delete-project-title' aria-describedby='delete-project-warning' style={{ maxWidth: 460 }}>
        <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 'var(--spacing-8)' }}>
          <Heading id='delete-project-title' as='h2' size={4} fontWeight={600}>Delete “Mobile app”?</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 size={3} asChild>
          <p style={{ marginBlock: 'var(--spacing-4) var(--spacing-8)', color: 'var(--label-secondary)' }}>The project, its 14 environments and all deployment history will be removed.</p>
        </Text>
        <Alert id='delete-project-warning' variant='warning'>
          <AlertIcon />
          <AlertMain>
            <AlertTitle>3 services still use this project</AlertTitle>
            <AlertContent>Billing API, Notifications and Search will stop receiving configuration updates.</AlertContent>
          </AlertMain>
        </Alert>
        <form method='dialog' style={{ display: 'flex', gap: 'var(--spacing-4)', justifyContent: 'flex-end', marginBlockStart: 'var(--spacing-12)' }}>
          <Button type='submit' variant='secondary' appearance='ghost'>Cancel</Button>
          <Button type='submit' style={destructive}>Delete project</Button>
        </form>
      </DialogContent>
    </Dialog>
  );
}

Ошибка

Ошибка: сводка ошибок над формой

При отправке невалидные поля получают aria-invalid и связанный HelperText, а алерт перечисляет все ошибки со ссылками, которые переводят фокус на поле. Ошибки обновляются по мере ввода, и сводка исчезает, когда всё исправлено.

error-summary.tsx · 98 строк
import { FormEvent, MouseEvent, useState } from 'react';
import {
  Alert,
  AlertIcon,
  AlertMain,
  AlertTitle,
  Button,
  HelperText,
  Label,
  Text,
  TextField,
  TextFieldInput,
} from '@vega-ui/react';

type Errors = { email?: string; seats?: string };

const validate = (email: string, seats: string): Errors => {
  const errors: Errors = {};
  if (!email.includes('@')) errors.email = 'Enter an email address like name@company.com.';
  const count = Number(seats);
  if (!Number.isInteger(count) || count < 1 || count > 500) errors.seats = 'Enter a number of seats from 1 to 500.';
  return errors;
};

export default function ErrorSummary() {
  const [email, setEmail] = useState('');
  const [seats, setSeats] = useState('0');
  const [submitted, setSubmitted] = useState(false);

  const errors = submitted ? validate(email, seats) : {};
  const messages = Object.entries(errors);

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

  const focusField = (event: MouseEvent<HTMLAnchorElement>, id: string) => {
    event.preventDefault();
    document.getElementById(id)?.focus();
  };

  return (
    <form noValidate onSubmit={submit} style={{ display: 'grid', gap: 'var(--spacing-12)', width: '100%', maxWidth: 420 }}>
      {messages.length > 0 && (
        <Alert variant='error' role='alert'>
          <AlertIcon />
          <AlertMain>
            <AlertTitle>Fix {messages.length === 1 ? 'one error' : `${messages.length} errors`} to continue</AlertTitle>
            <ul style={{ margin: 0, paddingInlineStart: 'var(--spacing-9)' }}>
              {messages.map(([field, message]) => (
                <li key={field}>
                  <Text size={2}>
                    <a href={`#order-${field}`} onClick={(event) => focusField(event, `order-${field}`)} style={{ color: 'inherit' }}>{message}</a>
                  </Text>
                </li>
              ))}
            </ul>
          </AlertMain>
        </Alert>
      )}
      <div style={{ display: 'grid', gap: 'var(--spacing-8)' }}>
        <div style={{ display: 'grid', gap: 'var(--spacing-3)' }}>
          <Label htmlFor='order-email'>Billing email</Label>
          <TextField error={Boolean(errors.email)}>
            <TextFieldInput
              id='order-email'
              type='email'
              value={email}
              onChange={(event) => setEmail(event.target.value)}
              aria-invalid={Boolean(errors.email)}
              aria-describedby={errors.email ? 'order-email-error' : undefined}
            />
          </TextField>
          {errors.email && <HelperText id='order-email-error' error>{errors.email}</HelperText>}
        </div>
        <div style={{ display: 'grid', gap: 'var(--spacing-3)' }}>
          <Label htmlFor='order-seats'>Seats</Label>
          <TextField error={Boolean(errors.seats)}>
            <TextFieldInput
              id='order-seats'
              inputMode='numeric'
              value={seats}
              onChange={(event) => setSeats(event.target.value)}
              aria-invalid={Boolean(errors.seats)}
              aria-describedby={errors.seats ? 'order-seats-error' : undefined}
            />
          </TextField>
          {errors.seats && <HelperText id='order-seats-error' error>{errors.seats}</HelperText>}
        </div>
      </div>
      <div style={{ display: 'flex', flexWrap: 'wrap', gap: 'var(--spacing-6)', alignItems: 'center' }}>
        <Button type='submit'>Continue to payment</Button>
        <Text size={2} role='status' style={{ color: 'var(--label-secondary)' }}>{submitted && messages.length === 0 ? 'Order details are valid.' : ''}</Text>
      </div>
    </form>
  );
}

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

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

В домене и TXT-записи нет пробелов, поэтому им нужен overflow-wrap: anywhere, а AlertMain — min-width: 0, чтобы текст не выходил за алерт. Действия находятся в AlertMain и сообщают о результате через область статуса.

domain-verification.tsx · 36 строк
import { useState } from 'react';
import { Alert, AlertContent, AlertIcon, AlertMain, AlertTitle, Button, Text } from '@vega-ui/react';

const record = 'vega-site-verification=8c1f0b6e2d9a4f7b93e5c2a1d0f4e6b8a7c3d9e1f2b4a6c8d0e2f4a6b8c0d2e4';

export default function DomainVerification() {
  const [message, setMessage] = useState('');

  const copy = async () => {
    await navigator.clipboard.writeText(record);
    setMessage('Copied to clipboard.');
  };

  const check = () => {
    setMessage('Checking DNS…');
    setTimeout(() => setMessage('Record not found yet. Try again in a few minutes.'), 1000);
  };

  return (
    <Alert variant='warning' style={{ width: '100%', maxWidth: 560 }}>
      <AlertIcon />
      <AlertMain style={{ minWidth: 0, gap: 'var(--spacing-4)' }}>
        <AlertTitle style={{ overflowWrap: 'anywhere' }}>Verify northwind-logistics-international.example.com</AlertTitle>
        <AlertContent>Add this TXT record at your DNS provider. Changes can take up to 48 hours to apply.</AlertContent>
        <Text size={1} asChild>
          <code style={{ display: 'block', fontFamily: 'var(--code-font-family)', wordBreak: 'break-all', padding: 'var(--spacing-4) var(--spacing-6)', borderRadius: 'var(--radius-2)', background: 'var(--fills-tertiary)', color: 'var(--label-primary)' }}>{record}</code>
        </Text>
        <div style={{ display: 'flex', flexWrap: 'wrap', gap: 'var(--spacing-4)', alignItems: 'center', marginBlockStart: 'var(--spacing-2)' }}>
          <Button size='sm' onClick={copy}>Copy record</Button>
          <Button size='sm' variant='secondary' appearance='ghost' onClick={check}>Check again</Button>
          <Text size={2} role='status' style={{ color: 'var(--label-secondary)' }}>{message}</Text>
        </div>
      </AlertMain>
    </Alert>
  );
}

Анатомия

Обзор

Alert — составной компонент: корень, иконка, заголовок и текст работают вместе как единый паттерн сообщения с общим контекстом семантического варианта.

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

Alert

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

AlertMain

Обязательна в большинстве продуктовых алертов. Оборачивает иерархию сообщения.

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

AlertIcon

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

AlertTitle

Необязательный заголовок для более сильного акцента.

AlertContent

Необязательный поясняющий текст сообщения.

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

  1. Alert
  2. AlertIcon
  3. AlertMain
  4. AlertTitle
  5. AlertContent

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

variants.tsx · 36 строк
import { Alert, AlertContent, AlertIcon, AlertMain, AlertTitle } from '@vega-ui/react';

export default function Variants() {
  return (
    <div style={{ display: 'grid', gap: 'var(--spacing-6)', width: '100%', maxWidth: 560 }}>
      <Alert variant='info'>
        <AlertIcon />
        <AlertMain>
          <AlertTitle>Scheduled maintenance</AlertTitle>
          <AlertContent>The dashboard will be read-only on Sunday from 02:00 to 03:00 UTC.</AlertContent>
        </AlertMain>
      </Alert>
      <Alert variant='success'>
        <AlertIcon />
        <AlertMain>
          <AlertTitle>Domain verified</AlertTitle>
          <AlertContent>Emails from billing@northwind.io will now pass SPF and DKIM checks.</AlertContent>
        </AlertMain>
      </Alert>
      <Alert variant='warning'>
        <AlertIcon />
        <AlertMain>
          <AlertTitle>Storage almost full</AlertTitle>
          <AlertContent>You have used 46.2 GB of 50 GB. Uploads stop when the limit is reached.</AlertContent>
        </AlertMain>
      </Alert>
      <Alert variant='error' appearance='surface'>
        <AlertIcon />
        <AlertMain>
          <AlertTitle>Card declined</AlertTitle>
          <AlertContent>The payment for invoice INV-2048 was declined by the bank. Update the card to keep your plan.</AlertContent>
        </AlertMain>
      </Alert>
    </div>
  );
}

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

Alert в роли каркаса всей страницы

Алерты — это сообщения обратной связи, а не замена структуре страницы.

Длинный сценарий с множеством действий внутри алерта

Когда содержимое превращается в полноценную задачу, обычно лучше подходит другой паттерн.