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

Badge

Badge — компактная метка статуса или категории для счётчиков, состояний и небольших метаданных.
Active
Beta
badge.tsx · 6 строк
import { Badge } from '@vega-ui/react';

<div style={{ display: 'flex', alignItems: 'center', gap: 10 }}>
  <Badge variant='success'>Active</Badge>
  <Badge variant='warning' appearance='surface'>Beta</Badge>
</div>
Поддержка браузеров
Недавно доступно, 2024
  • Chrome 125
  • Edge 125
  • Firefox 128
  • Safari 18
  • Safari на iOS 18
Размер в бандле
История версий
Добавлен в v1.0.0, последнее изменение в v3.0.0

Кратко

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

Импорт

import {
  Badge,
  type BadgeProps,
  type BadgeSize,
  type BadgeAppearance,
  type BadgeVariant,
} from '@vega-ui/react';

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

  • BadgeProps
  • BadgeSize
  • BadgeAppearance
  • BadgeVariant

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

basic-usage.tsx · 22 строки
import { Badge, Text } from '@vega-ui/react';

const releases = [
  { version: 'v2.8.0', date: 'Nov 3, 2026', status: 'Live', variant: 'success' },
  { version: 'v2.8.0-rc.2', date: 'Oct 30, 2026', status: 'Rolling back', variant: 'warning' },
  { version: 'v2.7.4', date: 'Oct 21, 2026', status: 'Failed', variant: 'error' },
  { version: 'v2.7.3', date: 'Oct 14, 2026', status: 'Archived', variant: 'info' },
];

export default function BasicUsage() {
  return (
    <ul style={{ listStyle: 'none', margin: 0, padding: 0, display: 'grid', gap: 'var(--spacing-6)', width: '100%', maxWidth: 420 }}>
      {releases.map((release) => (
        <li key={release.version} style={{ display: 'flex', alignItems: 'center', gap: 'var(--spacing-4)' }}>
          <Text size={3} fontWeight={500}>{release.version}</Text>
          <Badge size='sm' variant={release.variant}>{release.status}</Badge>
          <Text size={2} style={{ marginInlineStart: 'auto', color: 'var(--label-secondary)', fontVariantNumeric: 'tabular-nums', whiteSpace: 'nowrap' }}>{release.date}</Text>
        </li>
      ))}
    </ul>
  );
}

Варианты

  • Размеры: xs, sm, md, lg, xl
  • Внешний вид: fill, outline, ghost, surface
  • Варианты: success, error, warning, info

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

  • Текст бейджа должен быть коротким: это не контейнер для абзаца в форме таблетки.
  • Используйте семантические варианты единообразно во всём продукте.
  • Пользовательские строковые токены работают, только если для них есть соответствующие стили.
  • Если пользователю нужно объяснение, а не лаконичная метка, обычно лучше подойдёт Alert или окружающий основной текст.

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

  • Длинные предложения внутри бейджа.
  • Бейдж как единственный индикатор статуса.
  • Собственные названия вариантов без соответствующих стилей.

Примеры

Базовый

Базовый: статусы релизов

Каждому статусу соответствует свой вариант, а текст всегда называет состояние, поэтому смысл не зависит только от цвета.

basic-usage.tsx · 22 строки
import { Badge, Text } from '@vega-ui/react';

const releases = [
  { version: 'v2.8.0', date: 'Nov 3, 2026', status: 'Live', variant: 'success' },
  { version: 'v2.8.0-rc.2', date: 'Oct 30, 2026', status: 'Rolling back', variant: 'warning' },
  { version: 'v2.7.4', date: 'Oct 21, 2026', status: 'Failed', variant: 'error' },
  { version: 'v2.7.3', date: 'Oct 14, 2026', status: 'Archived', variant: 'info' },
];

export default function BasicUsage() {
  return (
    <ul style={{ listStyle: 'none', margin: 0, padding: 0, display: 'grid', gap: 'var(--spacing-6)', width: '100%', maxWidth: 420 }}>
      {releases.map((release) => (
        <li key={release.version} style={{ display: 'flex', alignItems: 'center', gap: 'var(--spacing-4)' }}>
          <Text size={3} fontWeight={500}>{release.version}</Text>
          <Badge size='sm' variant={release.variant}>{release.status}</Badge>
          <Text size={2} style={{ marginInlineStart: 'auto', color: 'var(--label-secondary)', fontVariantNumeric: 'tabular-nums', whiteSpace: 'nowrap' }}>{release.date}</Text>
        </li>
      ))}
    </ul>
  );
}

Базовый: метаданные рядом с заголовком

Неакцентные бейджи outline и surface размера size='xs' помечают функцию как Beta или доступную только на тарифе и не спорят с заголовком. Бейдж стоит рядом с заголовком, а не внутри него, поэтому текст заголовка остаётся чистым для скринридера и оглавления.

heading-flags.tsx · 22 строки
import { Badge, Heading, Text } from '@vega-ui/react';

export default function HeadingFlags() {
  return (
    <div style={{ display: 'grid', gap: 'var(--spacing-10)', width: '100%', maxWidth: 520 }}>
      <div style={{ display: 'grid', gap: 'var(--spacing-2)' }}>
        <div style={{ display: 'flex', alignItems: 'center', gap: 'var(--spacing-4)' }}>
          <Heading as='h3' size={4} fontWeight={600}>Usage-based billing</Heading>
          <Badge size='xs' variant='info' appearance='outline'>Beta</Badge>
        </div>
        <Text size={2} style={{ color: 'var(--label-secondary)' }}>Charge customers for API calls, seats or storage.</Text>
      </div>
      <div style={{ display: 'grid', gap: 'var(--spacing-2)' }}>
        <div style={{ display: 'flex', alignItems: 'center', gap: 'var(--spacing-4)' }}>
          <Heading as='h3' size={4} fontWeight={600}>Audit log export</Heading>
          <Badge size='xs' variant='warning' appearance='surface'>Enterprise</Badge>
        </div>
        <Text size={2} style={{ color: 'var(--label-secondary)' }}>Stream every admin action to your SIEM.</Text>
      </div>
    </div>
  );
}

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

Контролируемый/с состоянием: статус деплоя из состояния

Подпись и вариант вычисляются из текущего этапа, а бейдж находится в области role='status', поэтому каждое изменение озвучивается.

deploy-status.tsx · 44 строки
import { useEffect, useState } from 'react';
import { Badge, Button, Text } from '@vega-ui/react';

type Stage = 'idle' | 'building' | 'deploying' | 'live';

const stages: Record<Stage, { label: string; variant: string }> = {
  idle: { label: 'Not deployed', variant: 'info' },
  building: { label: 'Building', variant: 'warning' },
  deploying: { label: 'Deploying', variant: 'warning' },
  live: { label: 'Live', variant: 'success' },
};

export default function DeployStatus() {
  const [stage, setStage] = useState<Stage>('idle');

  useEffect(() => {
    if (stage === 'building') {
      const timer = setTimeout(() => setStage('deploying'), 1500);
      return () => clearTimeout(timer);
    }
    if (stage === 'deploying') {
      const timer = setTimeout(() => setStage('live'), 1500);
      return () => clearTimeout(timer);
    }
  }, [stage]);

  const busy = stage === 'building' || stage === 'deploying';

  return (
    <div style={{ display: 'flex', alignItems: 'center', gap: 'var(--spacing-6)', width: '100%' }}>
      <div style={{ display: 'flex', alignItems: 'center', gap: 'var(--spacing-4)', flex: 1, minWidth: 0 }}>
        <Text size={3} fontWeight={500}>checkout-service</Text>
        <span role='status'>
          <Badge size='sm' variant={stages[stage].variant} appearance={stage === 'live' ? 'fill' : 'ghost'}>
            {stages[stage].label}
          </Badge>
        </span>
      </div>
      <Button size='sm' variant='secondary' appearance='ghost' disabled={busy} onClick={() => setStage('building')}>
        {stage === 'live' ? 'Redeploy' : 'Deploy'}
      </Button>
    </div>
  );
}

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

Раскладка/оверлей: колонка статуса в таблице

Бейджи остаются в одну строку внутри ячеек; переносятся длинные имена клиентов в своей колонке. На узких экранах таблица прокручивается по горизонтали.

invoices-table.tsx · 47 строк
import { Badge, Table, TableBody, TableData, TableHead, TableHeading, TableRow, Text } from '@vega-ui/react';

const statusVariant: Record<string, string> = {
  Paid: 'success',
  Overdue: 'error',
  Draft: 'info',
  'Partially paid': 'warning',
};

const invoices = [
  { id: 'INV-2051', customer: 'Northwind Traders', amount: '$4,200.00', status: 'Paid' },
  { id: 'INV-2050', customer: 'Contoso Pharmaceuticals International', amount: '$12,980.50', status: 'Overdue' },
  { id: 'INV-2049', customer: 'Fabrikam', amount: '$860.00', status: 'Partially paid' },
  { id: 'INV-2048', customer: 'Adventure Works', amount: '$1,315.75', status: 'Draft' },
];

export default function InvoicesTable() {
  return (
    <div style={{ width: '100%', overflowX: 'auto' }}>
      <Table aria-label='Recent invoices' fullWidth dataAlign='start'>
        <TableHead>
          <TableRow>
            <TableHeading><Text size={2} style={{ color: 'var(--label-secondary)' }}>Customer</Text></TableHeading>
            <TableHeading dataAlign='end'><Text size={2} style={{ color: 'var(--label-secondary)' }}>Amount</Text></TableHeading>
            <TableHeading dataAlign='end'><Text size={2} style={{ color: 'var(--label-secondary)' }}>Status</Text></TableHeading>
          </TableRow>
        </TableHead>
        <TableBody>
          {invoices.map((invoice) => (
            <TableRow key={invoice.id}>
              <TableData>
                <Text size={2} fontWeight={500} asChild><div>{invoice.customer}</div></Text>
                <Text size={1} style={{ color: 'var(--label-secondary)' }}>{invoice.id}</Text>
              </TableData>
              <TableData dataAlign='end'>
                <Text size={2} style={{ fontVariantNumeric: 'tabular-nums', whiteSpace: 'nowrap' }}>{invoice.amount}</Text>
              </TableData>
              <TableData dataAlign='end'>
                <Badge size='sm' variant={statusVariant[invoice.status]}>{invoice.status}</Badge>
              </TableData>
            </TableRow>
          ))}
        </TableBody>
      </Table>
    </div>
  );
}

Ошибка

Ошибка: статус сбоя со связанной причиной

Бейдж ошибки одним словом говорит, что не так, а полная причина — отдельный текст, связанный с кнопкой повтора через aria-describedby. Успешный повтор переключает бейдж на «Healthy».

webhook-health.tsx · 36 строк
import { useState } from 'react';
import { Badge, Button, Text } from '@vega-ui/react';

export default function WebhookHealth() {
  const [failing, setFailing] = useState(true);
  const [retrying, setRetrying] = useState(false);

  const retry = () => {
    setRetrying(true);
    setTimeout(() => {
      setRetrying(false);
      setFailing(false);
    }, 1200);
  };

  return (
    <div style={{ display: 'grid', gap: 'var(--spacing-4)', width: '100%', maxWidth: 520 }}>
      <div style={{ display: 'flex', alignItems: 'center', gap: 'var(--spacing-4)', flexWrap: 'wrap' }}>
        <Text size={3} fontWeight={500} style={{ overflowWrap: 'anywhere' }}>https://hooks.northwind.io/orders</Text>
        <span role='status'>
          <Badge size='sm' variant={failing ? 'error' : 'success'}>{failing ? 'Failing' : 'Healthy'}</Badge>
        </span>
      </div>
      {failing && (
        <>
          <Text id='webhook-error' size={2} style={{ color: 'var(--label-secondary)' }}>
            Last 12 deliveries returned 503 Service Unavailable. Deliveries are paused until the endpoint responds.
          </Text>
          <Button size='sm' variant='secondary' appearance='ghost' aria-describedby='webhook-error' disabled={retrying} onClick={retry} style={{ justifySelf: 'start', marginBlockStart: 'var(--spacing-4)' }}>
            {retrying ? 'Retrying…' : 'Retry deliveries'}
          </Button>
        </>
      )}
    </div>
  );
}

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

Пограничный случай: счётчик с ограничением и полной доступной подписью

Числа больше 99 показываются как «99+», ноль скрывает бейдж, а точное число скринридер читает из VisuallyHidden, тогда как короткий визуальный текст помечен aria-hidden.

unread-count.tsx · 33 строки
import { useState } from 'react';
import { Badge, Button, Text, VisuallyHidden } from '@vega-ui/react';

const max = 99;

export default function UnreadCount() {
  const [count, setCount] = useState(7);

  let shown = String(count);
  if (count > max) shown = `${max}+`;

  let label = `${count} unread messages`;
  if (count === 1) label = '1 unread message';

  return (
    <div style={{ display: 'grid', gap: 'var(--spacing-8)', justifyItems: 'start', width: '100%' }}>
      <div style={{ display: 'flex', alignItems: 'center', gap: 'var(--spacing-4)' }}>
        <Text size={3} fontWeight={500}>Inbox</Text>
        {count > 0 && (
          <Badge size='xs' variant='error' appearance='fill' style={{ fontVariantNumeric: 'tabular-nums' }}>
            <span aria-hidden='true'>{shown}</span>
            <VisuallyHidden>{label}</VisuallyHidden>
          </Badge>
        )}
      </div>
      <div style={{ display: 'flex', flexWrap: 'wrap', gap: 'var(--spacing-4)' }}>
        <Button size='sm' variant='secondary' appearance='ghost' onClick={() => setCount(0)}>Mark all read</Button>
        <Button size='sm' variant='secondary' appearance='ghost' onClick={() => setCount(count + 1)}>+1 message</Button>
        <Button size='sm' variant='secondary' appearance='ghost' onClick={() => setCount(count + 100)}>+100 messages</Button>
      </div>
    </div>
  );
}

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

Бейдж не переносит текст. Длинному тегу нужны max-width, сжимаемый flex-элемент и многоточие, а полный текст — в title; кириллические и китайские подписи сохраняют ту же высоту.

long-labels.tsx · 30 строк
import { Badge, Text } from '@vega-ui/react';

const tags = [
  'enterprise-customer-success-escalation-q4',
  'Требует проверки юристом',
  '优先处理',
  'β-tester',
];

export default function LongLabels() {
  return (
    <div style={{ display: 'grid', gridTemplateColumns: 'minmax(0, 1fr)', gap: 'var(--spacing-6)', boxSizing: 'border-box', width: '100%', maxWidth: 280, padding: 'var(--spacing-8)', border: '1px solid var(--separator-opaque)', borderRadius: 'var(--radius-4)', background: 'var(--surface-primary)' }}>
      <Text size={2} style={{ color: 'var(--label-secondary)' }}>Ticket #88412 · tags</Text>
      <div style={{ display: 'flex', flexWrap: 'wrap', gap: 'var(--spacing-3)' }}>
        {tags.map((tag) => (
          <Badge
            key={tag}
            size='sm'
            variant='info'
            appearance='outline'
            title={tag}
            style={{ maxWidth: '100%', minWidth: 0, flexShrink: 1 }}
          >
            <span style={{ minWidth: 0, overflow: 'hidden', textOverflow: 'ellipsis' }}>{tag}</span>
          </Badge>
        ))}
      </div>
    </div>
  );
}

Анатомия

Обзор

Badge — единая компактная поверхность для короткого текста статуса, категории или счётчика.

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

Badge

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

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

Badge не предоставляет публичных дочерних частей.

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

  1. Badge
  2. короткая подпись или счётчик

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

basic-usage.tsx · 22 строки
import { Badge, Text } from '@vega-ui/react';

const releases = [
  { version: 'v2.8.0', date: 'Nov 3, 2026', status: 'Live', variant: 'success' },
  { version: 'v2.8.0-rc.2', date: 'Oct 30, 2026', status: 'Rolling back', variant: 'warning' },
  { version: 'v2.7.4', date: 'Oct 21, 2026', status: 'Failed', variant: 'error' },
  { version: 'v2.7.3', date: 'Oct 14, 2026', status: 'Archived', variant: 'info' },
];

export default function BasicUsage() {
  return (
    <ul style={{ listStyle: 'none', margin: 0, padding: 0, display: 'grid', gap: 'var(--spacing-6)', width: '100%', maxWidth: 420 }}>
      {releases.map((release) => (
        <li key={release.version} style={{ display: 'flex', alignItems: 'center', gap: 'var(--spacing-4)' }}>
          <Text size={3} fontWeight={500}>{release.version}</Text>
          <Badge size='sm' variant={release.variant}>{release.status}</Badge>
          <Text size={2} style={{ marginInlineStart: 'auto', color: 'var(--label-secondary)', fontVariantNumeric: 'tabular-nums', whiteSpace: 'nowrap' }}>{release.date}</Text>
        </li>
      ))}
    </ul>
  );
}

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

Длинный поясняющий текст внутри бейджа

Бейджи предназначены для коротких метаданных, а не для полноценных сообщений.

Бейдж как единственный способ сообщить статус

Если статус важен, его всё равно должны объяснять окружающий текст или структура.