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

Text

Text — базовый примитив основной типографики.
The quick brown fox jumps over the lazy dog.
text.tsx · 3 строки
import { Text } from '@vega-ui/react';

<Text size={3}>The quick brown fox jumps over the lazy dog.</Text>
Поддержка браузеров
Широко доступно
  • Chrome 111
  • Edge 111
  • Firefox 113
  • Safari 15.4
  • Safari на iOS 15.4
Размер в бандле
История версий
Добавлен в v1.0.0, последнее изменение в v3.0.0

Кратко

Text — базовый примитив основной типографики. Он управляет размером и начертанием и допускает семантическую композицию через asChild.

Импорты

import { Text, type TextProps, type TextSize } from '@vega-ui/react';

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

  • TextProps
  • TextSize = 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11

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

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

export default function BasicUsage() {
  return (
    <Text asChild size={3}>
      <p style={{ maxWidth: '60ch' }}>
        Your trial ends in 5 days. Add a payment method to keep your projects, history and integrations.
      </p>
    </Text>
  );
}

Варианты

  • Размер: от 1 до 11
  • Начертание: 400, 500, 600, 700, 900
  • Композиция: элемент по умолчанию или asChild

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

  • asChild меняет отрендеренный элемент, но сохраняет контракт стилей текста.
  • white-space: pre-line сохраняет переносы строк из строк содержимого.
  • Очень крупные размеры текста всё равно нужно проверять на раскладке и длине строки.

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

  • Использовать Text там, где заголовки документа должны быть семантическими Heading.
  • Переопределять начертание и размер вручную вместо дизайн-шкалы.
  • Забывать, что в многострочном содержимом сохраняются переносы строк.

Примеры

Базовый

Базовый: текст статьи

По умолчанию Text рендерит <span>; абзацы используют asChild с <p>, чтобы сохранить семантику абзаца. Вложенный Text с fontWeight выделяет фрагмент, а подпись автора приглушена через --t-color.

body-copy.tsx · 24 строки
import { Heading, Text } from '@vega-ui/react';
import type { CSSProperties } from 'react';

export default function BodyCopy() {
  return (
    <article style={{ display: 'grid', gap: 'var(--spacing-6)', width: '100%', maxWidth: '60ch' }}>
      <header style={{ display: 'grid', gap: 'var(--spacing-2)', marginBlockEnd: 'var(--spacing-2)' }}>
        <Heading as='h3' size={6}>Scheduled maintenance on March 18</Heading>
        <Text size={2} style={{ '--t-color': 'var(--label-secondary)' } as CSSProperties}>
          Posted by the Platform team
        </Text>
      </header>
      <Text asChild size={3}>
        <p>
          We are upgrading the primary database cluster. The dashboard stays available, but exports and
          webhooks are paused for <Text fontWeight={500}>up to 30 minutes</Text> starting at 02:00 UTC.
        </p>
      </Text>
      <Text asChild size={3}>
        <p>Queued webhooks are delivered automatically once the upgrade finishes. No action is needed.</p>
      </Text>
    </article>
  );
}

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

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

Фраза вычисляется из числа мест через Intl.PluralRules и на нуле сменяется другим сообщением. aria-live='polite' озвучивает изменение после каждого клика, не перемещая фокус.

dynamic-helper-copy.tsx · 33 строки
import { Button, Text } from '@vega-ui/react';
import { useState } from 'react';

const totalSeats = 5;
const plural = new Intl.PluralRules('en-US');

export default function DynamicHelperCopy() {
  const [assigned, setAssigned] = useState(3);
  const remaining = totalSeats - assigned;
  const seatWord = plural.select(remaining) === 'one' ? 'seat' : 'seats';

  return (
    <div style={{ display: 'grid', gap: 'var(--spacing-8)', width: '100%' }}>
      <Text size={3} aria-live='polite' style={{ fontVariantNumeric: 'tabular-nums' }}>
        {remaining === 0 ? 'All seats are taken. Upgrade to invite more people.' : `${remaining} ${seatWord} left on the Team plan.`}
      </Text>
      <div style={{ display: 'flex', gap: 'var(--spacing-4)' }}>
        <Button size='sm' disabled={remaining === 0} onClick={() => setAssigned(assigned + 1)}>
          Assign a seat
        </Button>
        <Button
          size='sm'
          variant='secondary'
          appearance='ghost'
          disabled={assigned === 0}
          onClick={() => setAssigned(assigned - 1)}
        >
          Free a seat
        </Button>
      </div>
    </div>
  );
}

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

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

И подсказка, и живой счётчик — элементы Text, на которые поле ссылается через aria-describedby, поэтому скринридер читает их вместе с полем. Значение отправляется через FormData, а сброс очищает счётчик вместе с полем.

helper-text-below-a-field.tsx · 58 строк
import { Button, Label, Text, TextField, TextFieldInput } from '@vega-ui/react';
import { useState, type CSSProperties, type FormEvent } from 'react';

const maxLength = 40;

export default function HelperTextBelowAField() {
  const [headline, setHeadline] = useState('');
  const [saved, setSaved] = useState('');

  const onSubmit = (event: FormEvent<HTMLFormElement>) => {
    event.preventDefault();
    const data = new FormData(event.currentTarget);
    setSaved(`Saved headline: ${data.get('headline')}`);
  };

  const onReset = () => {
    setHeadline('');
    setSaved('');
  };

  return (
    <form onSubmit={onSubmit} onReset={onReset} style={{ display: 'grid', gap: 'var(--spacing-12)', width: '100%' }}>
      <div style={{ display: 'grid', gap: 'var(--spacing-3)' }}>
        <Label htmlFor='profile-headline'>Profile headline</Label>
        <TextField>
          <TextFieldInput
            id='profile-headline'
            name='headline'
            maxLength={maxLength}
            value={headline}
            aria-describedby='profile-headline-hint profile-headline-count'
            onChange={(event) => setHeadline(event.target.value)}
            required
          />
        </TextField>
        <div style={{ display: 'flex', justifyContent: 'space-between', gap: 'var(--spacing-6)' }}>
          <Text id='profile-headline-hint' size={2} style={{ '--t-color': 'var(--label-secondary)' } as CSSProperties}>
            Shown under your name in comments.
          </Text>
          <Text
            id='profile-headline-count'
            size={2}
            style={{ '--t-color': 'var(--label-tertiary)', fontVariantNumeric: 'tabular-nums', flexShrink: 0 } as CSSProperties}
          >
            {headline.length}/{maxLength}
          </Text>
        </div>
      </div>
      <div style={{ display: 'grid', gap: 'var(--spacing-6)' }}>
        <div style={{ display: 'flex', justifyContent: 'flex-end', gap: 'var(--spacing-4)' }}>
          <Button type='reset' variant='secondary' appearance='ghost'>Reset</Button>
          <Button type='submit'>Save</Button>
        </div>
        {saved && <Text size={2} role='status' style={{ '--t-color': 'var(--label-secondary)', overflowWrap: 'anywhere' } as CSSProperties}>{saved}</Text>}
      </div>
    </form>
  );
}

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

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

Диалог получает доступное имя из заголовка, а описание — из абзаца Text через aria-labelledby и aria-describedby, поэтому скринридер сообщает о последствиях сразу при открытии.

descriptive-copy-in-a-dialog.tsx · 58 строк
import {
  Button,
  Dialog,
  DialogClose,
  DialogContent,
  DialogTrigger,
  Heading,
  Icon,
  IconButton,
  Text,
} from '@vega-ui/react';
import { XIcon } from '@vega-ui/icons';
import type { CSSProperties } from 'react';

export default function DescriptiveCopyInADialog() {
  return (
    <Dialog>
      <DialogTrigger asChild>
        <Button variant='secondary' appearance='ghost'>Transfer ownership</Button>
      </DialogTrigger>
      <DialogContent
        aria-labelledby='transfer-title'
        aria-describedby='transfer-description'
        style={{ display: 'grid', gap: 'var(--spacing-12)' }}
      >
        <div style={{ display: 'grid', gap: 'var(--spacing-4)' }}>
          <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 'var(--spacing-8)' }}>
            <Heading as='h2' size={5} id='transfer-title'>Transfer ownership?</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 asChild id='transfer-description' size={3} style={{ '--t-color': 'var(--label-secondary)' } as CSSProperties}>
            <p>
              Dana Novak becomes the owner of the Acme workspace and gets access to billing. You stay an admin
              and can be removed by the new owner.
            </p>
          </Text>
        </div>
        <div style={{ display: 'flex', justifyContent: 'flex-end', gap: 'var(--spacing-4)' }}>
          <DialogClose asChild>
            <Button variant='secondary' appearance='ghost'>Cancel</Button>
          </DialogClose>
          <DialogClose asChild>
            <Button>Transfer</Button>
          </DialogClose>
        </div>
      </DialogContent>
    </Dialog>
  );
}

Ошибка

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

Купон проверяется при отправке. Неизвестный код показывает сообщение Text цветом ошибки, связывает его через aria-describedby и выставляет aria-invalid; правка поля снимает ошибку. Цвет — не единственный сигнал: само сообщение объясняет, что не так.

inline-validation-copy.tsx · 49 строк
import { Button, Label, Text, TextField, TextFieldInput } from '@vega-ui/react';
import { useState, type CSSProperties, type FormEvent } from 'react';

const validCodes = ['SPRING20', 'WELCOME10'];

export default function InlineValidationCopy() {
  const [error, setError] = useState('');
  const [applied, setApplied] = useState('');

  const onSubmit = (event: FormEvent<HTMLFormElement>) => {
    event.preventDefault();
    const data = new FormData(event.currentTarget);
    const code = String(data.get('coupon')).trim().toUpperCase();

    if (!validCodes.includes(code)) {
      setApplied('');
      setError(`${code === '' ? 'This code' : code} is not valid or has expired.`);
      return;
    }

    setError('');
    setApplied(`${code} applied to your order.`);
  };

  return (
    <form onSubmit={onSubmit} style={{ display: 'grid', gap: 'var(--spacing-3)', width: '100%' }}>
      <Label htmlFor='coupon-code'>Coupon code</Label>
      <div style={{ display: 'flex', gap: 'var(--spacing-4)' }}>
        <TextField error={error !== ''} style={{ flex: 1, minWidth: 0 }}>
          <TextFieldInput
            id='coupon-code'
            name='coupon'
            autoComplete='off'
            aria-invalid={error !== ''}
            aria-describedby={error ? 'coupon-code-error' : undefined}
            onChange={() => setError('')}
          />
        </TextField>
        <Button type='submit' variant='secondary' appearance='ghost'>Apply</Button>
      </div>
      {error && (
        <Text id='coupon-code-error' size={2} style={{ '--t-color': 'var(--color-error)' } as CSSProperties}>
          {error}
        </Text>
      )}
      {applied && <Text size={2} role='status' style={{ '--t-color': 'var(--color-green-accent-600)' } as CSSProperties}>{applied}</Text>}
    </form>
  );
}

Отключённый

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

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

muted-explanatory-text.tsx · 26 строк
import { Button, Fieldset, FieldsetLegend, Label, Text, TextField, TextFieldInput } from '@vega-ui/react';
import type { CSSProperties } from 'react';

export default function MutedExplanatoryText() {
  return (
    <div style={{ display: 'grid', gap: 'var(--spacing-8)', width: '100%' }}>
      <Fieldset disabled aria-describedby='export-locked-note' style={{ display: 'grid', gap: 'var(--spacing-12)' }}>
        <FieldsetLegend style={{ marginBlockEnd: 'var(--spacing-8)' }}>Scheduled exports</FieldsetLegend>
        <div style={{ display: 'grid', gap: 'var(--spacing-3)' }}>
          <Label htmlFor='export-email'>Send CSV to</Label>
          <TextField>
            <TextFieldInput id='export-email' name='exportEmail' type='email' defaultValue='finance@acme.io' />
          </TextField>
        </div>
        <Button style={{ justifySelf: 'end' }}>Save schedule</Button>
      </Fieldset>
      <Text
        id='export-locked-note'
        size={2}
        style={{ '--t-color': 'var(--label-secondary)' } as CSSProperties}
      >
        Scheduled exports are available on the Business plan.
      </Text>
    </div>
  );
}

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

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

Text использует white-space: pre-line: переводы строк в строке становятся разрывами, что удобно для адреса. Текст из CMS с жёсткими переносами отрисуется так же, поэтому переопределяйте это через whiteSpace: 'normal', когда переводы строк не несут смысла.

multiline-content.tsx · 13 строк
import { Text } from '@vega-ui/react';

const address = 'Acme GmbH\nTorstraße 140\n10119 Berlin\nGermany';
const description = 'Soft merino wool sweater with a relaxed fit,\nribbed cuffs and a crew neck.\nMachine washable at 30\u00a0°C.';

export default function MultilineContent() {
  return (
    <div style={{ display: 'grid', gap: 'var(--spacing-8)', width: '100%' }}>
      <Text size={3}>{address}</Text>
      <Text size={3} style={{ whiteSpace: 'normal' }}>{description}</Text>
    </div>
  );
}

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

Для обрезки нужен блочный бокс: Text — строчный <span>, поэтому textOverflow: 'ellipsis' работает только вместе с display: 'block', overflow: 'hidden' и whiteSpace: 'nowrap'; title оставляет полное имя доступным. Длинные строки без пробелов, например адреса почты, переносятся через overflowWrap: 'anywhere'.

long-text-overflow.tsx · 27 строк
import { Text } from '@vega-ui/react';

const files = [
  'Q1-2026-board-presentation-final-v7-approved-by-legal.pdf',
  'customer-interviews-transcripts-berlin-and-lisbon.docx',
  'logo.svg',
];

export default function LongTextOverflow() {
  return (
    <div style={{ display: 'grid', gap: 'var(--spacing-4)', width: '100%' }}>
      {files.map((file) => (
        <Text
          key={file}
          size={2}
          title={file}
          style={{ display: 'block', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}
        >
          {file}
        </Text>
      ))}
      <Text size={2} style={{ overflowWrap: 'anywhere' }}>
        Invoices are sent to accounts-payable.emea-region@northwind-traders.example.com
      </Text>
    </div>
  );
}

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

Имена на иврите или арабском внутри английской фразы могут переставить соседние числа и знаки препинания. Обёртка имени в <bdi> через asChild изолирует его направление.

bidi-user-content.tsx · 21 строка
import { Text } from '@vega-ui/react';

const comments = [
  { author: 'Maya Cohen', handle: 'מאיה כהן', count: 3 },
  { author: 'Omar Haddad', handle: 'عمر حداد', count: 12 },
];

export default function BidiUserContent() {
  return (
    <div style={{ display: 'grid', gap: 'var(--spacing-4)' }}>
      {comments.map((comment) => (
        <Text key={comment.author} size={3}>
          <Text asChild fontWeight={500}>
            <bdi>{comment.handle}</bdi>
          </Text>{' '}
          left {comment.count} comments
        </Text>
      ))}
    </div>
  );
}

Анатомия

Обзор

Text — типографский примитив из одной части. Корень владеет всем публичным контрактом размера, начертания и семантической композиции основного текста.

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

Text

Обязательна. Рендерит текстовый узел со стилями основной типографики VegaUI.

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

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

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

  1. Text
  2. содержимое или целевой элемент asChild

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

Внешний Text рендерит <p> через asChild; строчные Text внутри остаются <span> или рендерят семантические элементы вроде <time>.

valid-composition-patterns.tsx · 15 строк
import { Text } from '@vega-ui/react';

export default function ValidCompositionPatterns() {
  return (
    <Text asChild size={3}>
      <p>
        Order <Text fontWeight={700}>#4821</Text> ships on{' '}
        <Text asChild>
          <time dateTime='2026-03-14'>March 14</time>
        </Text>
        .
      </p>
    </Text>
  );
}

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

Text вместо заголовка

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

Произвольный CSS вместо шкалы размеров

Это уводит от общей типографской системы.