import { Separator, Text } from '@vega-ui/react';
<div>
<Text size={3}>Profile</Text>
<Separator />
<Text size={3}>Billing</Text>
<Separator />
<Text size={3}>Security</Text>
</div>- Поддержка браузеров
- Широко доступно
- Chrome 111
- Edge 111
- Firefox 113
- Safari 15.4
- Safari на iOS 15.4
- Размер в бандле
- Протестирован в
- 8 тестов · 6 нояб.
- История версий
- Добавлен в v1.0.0
- Oklab and OkLCh — приходит из дизайн-токенов
- :where()
Кратко
Separator — примитив визуального разделения для горизонтальных и вертикальных границ между соседними областями интерфейса.
Импорты
import { Separator, type SeparatorProps } from '@vega-ui/react';Экспортируемые типы
SeparatorProps
Базовое использование
import { Heading, Separator, Text } from '@vega-ui/react';
export default function BasicUsage() {
return (
<div style={{ display: 'grid', gap: 'var(--spacing-8)', width: '100%' }}>
<div style={{ display: 'grid', gap: 'var(--spacing-2)' }}>
<Heading as='h3' size={4}>Notifications</Heading>
<Text size={2} style={{ color: 'var(--label-secondary)' }}>Email me when someone mentions me or assigns me a task.</Text>
</div>
<Separator />
<div style={{ display: 'grid', gap: 'var(--spacing-2)' }}>
<Heading as='h3' size={4}>Privacy</Heading>
<Text size={2} style={{ color: 'var(--label-secondary)' }}>Only workspace members can see my profile and activity.</Text>
</div>
</div>
);
}Варианты
- Ориентация:
horizontalилиvertical - Раскладка: горизонтальная линия на всю ширину или вертикальный разделитель на всю высоту
Пограничные случаи
- Вертикальным разделителям нужна явная высота от родительской раскладки.
- Компонент визуальный; отступы по-прежнему определяет окружающая раскладка.
- Декоративные разделители не должны заменять настоящие заголовки или группировку через fieldset.
Частые ошибки
- Вертикальный разделитель рендерится без заданной высоты родителя.
- Разделители используются там, где на самом деле нужна семантическая группировка.
- От
Separatorожидают, что он сам создаст отступы в раскладке.
Примеры
Базовый
Базовый: разделитель в сводке заказа
Горизонтальный Separator растягивается на ширину контейнера и отделяет итог от позиций заказа. Отступы задаёт gap родителя, а не сам разделитель.
import { Separator, Text } from '@vega-ui/react';
const items = [
{ name: 'Wireless keyboard', price: '$89.00' },
{ name: 'USB-C hub, 7 ports', price: '$45.00' },
{ name: 'Shipping', price: '$6.50' },
];
export default function HorizontalSectionDivider() {
return (
<div style={{ display: 'grid', gap: 'var(--spacing-6)', width: '100%' }}>
{items.map((item) => (
<div key={item.name} style={{ display: 'flex', justifyContent: 'space-between', gap: 'var(--spacing-8)' }}>
<Text size={2} style={{ color: 'var(--label-secondary)' }}>{item.name}</Text>
<Text size={2} style={{ fontVariantNumeric: 'tabular-nums' }}>{item.price}</Text>
</div>
))}
<Separator />
<div style={{ display: 'flex', justifyContent: 'space-between', gap: 'var(--spacing-8)' }}>
<Text size={3} fontWeight={500}>Total</Text>
<Text size={3} fontWeight={500} style={{ fontVariantNumeric: 'tabular-nums' }}>$140.50</Text>
</div>
</div>
);
}Контролируемый/с состоянием
Контролируемый/с состоянием: разделители только между видимыми группами
Разделители рендерятся между группами и никогда перед первой, поэтому скрытая фильтром группа не оставляет двойной или висящей линии. Каждая группа остаётся section с подписью из заголовка, так что структура не держится на линии.
import { Button, Heading, Separator, Text } from '@vega-ui/react';
import { Fragment, useState } from 'react';
const projects = [
{ name: 'Website redesign', status: 'active' },
{ name: 'Mobile onboarding', status: 'active' },
{ name: 'Q3 pricing test', status: 'paused' },
{ name: 'Legacy API migration', status: 'archived' },
{ name: 'Holiday campaign', status: 'archived' },
];
const groups = [
{ status: 'active', title: 'Active' },
{ status: 'paused', title: 'Paused' },
{ status: 'archived', title: 'Archived' },
];
export default function ConditionalDivider() {
const [showArchived, setShowArchived] = useState(false);
const visibleGroups = [];
for (const group of groups) {
if (group.status === 'archived' && !showArchived) continue;
const items = projects.filter((project) => project.status === group.status);
if (items.length === 0) continue;
visibleGroups.push({ ...group, items });
}
return (
<div style={{ display: 'grid', gap: 'var(--spacing-8)', width: '100%' }}>
<Button
size='sm'
variant='secondary'
appearance='ghost'
aria-pressed={showArchived}
onClick={() => setShowArchived(!showArchived)}
style={{ justifySelf: 'start' }}
>
Show archived
</Button>
{visibleGroups.map((group, index) => (
<Fragment key={group.status}>
{index > 0 && <Separator />}
<section aria-labelledby={`projects-${group.status}`} style={{ display: 'grid', gap: 'var(--spacing-3)' }}>
<Heading as='h3' size={2} id={`projects-${group.status}`} style={{ color: 'var(--label-secondary)' }}>{group.title}</Heading>
{group.items.map((project) => (
<Text key={project.name} size={3}>{project.name}</Text>
))}
</section>
</Fragment>
))}
</div>
);
}Форма/интеграция
Форма/интеграция: разделитель между группами полей
Две группы Fieldset со своими легендами, разделённые линией. Разделитель только визуальный: доступные имена группам дают легенды, а отправка читает обе группы через FormData.
import {
Button,
Fieldset,
FieldsetLegend,
Label,
Separator,
Text,
TextField,
TextFieldInput,
} from '@vega-ui/react';
import { useState, type FormEvent } from 'react';
export default function DividerBetweenFieldGroups() {
const [saved, setSaved] = useState('');
const onSubmit = (event: FormEvent<HTMLFormElement>) => {
event.preventDefault();
const data = new FormData(event.currentTarget);
setSaved(`Saved ${data.get('fullName')} <${data.get('email')}>, ${data.get('company')}`);
};
return (
<form
onSubmit={onSubmit}
onReset={() => setSaved('')}
style={{ display: 'grid', gap: 'var(--spacing-12)', width: '100%' }}
>
<Fieldset style={{ display: 'grid', gap: 'var(--spacing-8)' }}>
<FieldsetLegend style={{ marginBlockEnd: 'var(--spacing-8)' }}>Personal details</FieldsetLegend>
<div style={{ display: 'grid', gap: 'var(--spacing-3)' }}>
<Label htmlFor='billing-name'>Full name</Label>
<TextField>
<TextFieldInput id='billing-name' name='fullName' autoComplete='name' required />
</TextField>
</div>
<div style={{ display: 'grid', gap: 'var(--spacing-3)' }}>
<Label htmlFor='billing-email'>Email</Label>
<TextField>
<TextFieldInput id='billing-email' name='email' type='email' autoComplete='email' required />
</TextField>
</div>
</Fieldset>
<Separator />
<Fieldset style={{ display: 'grid', gap: 'var(--spacing-8)' }}>
<FieldsetLegend style={{ marginBlockEnd: 'var(--spacing-8)' }}>Company</FieldsetLegend>
<div style={{ display: 'grid', gap: 'var(--spacing-3)' }}>
<Label htmlFor='billing-company'>Company name</Label>
<TextField>
<TextFieldInput id='billing-company' name='company' autoComplete='organization' />
</TextField>
</div>
</Fieldset>
<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={{ color: 'var(--label-secondary)', overflowWrap: 'anywhere' }}>{saved}</Text>}
</div>
</form>
);
}Раскладка/оверлей
Раскладка/оверлей: вертикальные разделители на панели инструментов
Вертикальные разделители делят role='toolbar' на группы форматирования, списков и выравнивания. Вертикальный разделитель берёт height: 100%, поэтому у панели задана явная высота; у каждой кнопки-иконки свой aria-label.
import { Icon, IconButton, Separator } from '@vega-ui/react';
import { AlignCenter, AlignLeft, Bold, Italic, Link, List, ListOrdered } from '@vega-ui/icons';
export default function VerticalDividerInToolbar() {
return (
<div
role='toolbar'
aria-label='Text formatting'
style={{
display: 'flex',
alignItems: 'center',
gap: 'var(--spacing-2)',
padding: 'var(--spacing-2)',
border: '1px solid var(--separator-opaque)',
borderRadius: 'var(--radius-3)',
background: 'var(--surface-primary)',
}}
>
<IconButton aria-label='Bold' variant='secondary' appearance='transparent'>
<Icon><Bold /></Icon>
</IconButton>
<IconButton aria-label='Italic' variant='secondary' appearance='transparent'>
<Icon><Italic /></Icon>
</IconButton>
<IconButton aria-label='Insert link' variant='secondary' appearance='transparent'>
<Icon><Link /></Icon>
</IconButton>
<Separator orientation='vertical' style={{ height: 'var(--spacing-10)', marginInline: 'var(--spacing-2)' }} />
<IconButton aria-label='Bulleted list' variant='secondary' appearance='transparent'>
<Icon><List /></Icon>
</IconButton>
<IconButton aria-label='Numbered list' variant='secondary' appearance='transparent'>
<Icon><ListOrdered /></Icon>
</IconButton>
<Separator orientation='vertical' style={{ height: 'var(--spacing-10)', marginInline: 'var(--spacing-2)' }} />
<IconButton aria-label='Align left' variant='secondary' appearance='transparent'>
<Icon><AlignLeft /></Icon>
</IconButton>
<IconButton aria-label='Align center' variant='secondary' appearance='transparent'>
<Icon><AlignCenter /></Icon>
</IconButton>
</div>
);
}Ошибка
Ошибка: разделитель перед опасной зоной
Разделитель отделяет форму с разрушительным действием от обычных настроек. Подтверждение проверяется при отправке: при несовпадении поле помечается невалидным, HelperText связывается через aria-describedby, а ввод снимает ошибку.
import {
Button,
Heading,
HelperText,
Label,
Separator,
Text,
TextField,
TextFieldInput,
} from '@vega-ui/react';
import { useState, type CSSProperties, type FormEvent } from 'react';
const workspaceName = 'acme-marketing';
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 DividerBeforeDestructiveSection() {
const [error, setError] = useState('');
const [deleted, setDeleted] = useState(false);
const onSubmit = (event: FormEvent<HTMLFormElement>) => {
event.preventDefault();
const data = new FormData(event.currentTarget);
if (data.get('confirmation') !== workspaceName) {
setError(`Type ${workspaceName} exactly to confirm.`);
return;
}
setDeleted(true);
};
return (
<div style={{ display: 'grid', gap: 'var(--spacing-12)', width: '100%' }}>
<div style={{ display: 'grid', gap: 'var(--spacing-2)' }}>
<Heading as='h3' size={4}>Workspace name</Heading>
<Text size={2} style={{ color: 'var(--label-secondary)' }}>{workspaceName}</Text>
</div>
<Separator />
<form onSubmit={onSubmit} aria-labelledby='danger-zone-title' style={{ display: 'grid', gap: 'var(--spacing-8)' }}>
<div style={{ display: 'grid', gap: 'var(--spacing-2)' }}>
<Heading as='h3' size={4} id='danger-zone-title'>Delete workspace</Heading>
<Text size={2} style={{ color: 'var(--label-secondary)' }}>All projects, files and billing history are removed permanently.</Text>
</div>
<div style={{ display: 'grid', gap: 'var(--spacing-3)' }}>
<Label htmlFor='delete-confirmation'>Type {workspaceName} to confirm</Label>
<TextField error={error !== ''}>
<TextFieldInput
id='delete-confirmation'
name='confirmation'
autoComplete='off'
aria-invalid={error !== ''}
aria-describedby={error ? 'delete-confirmation-error' : undefined}
onChange={() => setError('')}
/>
</TextField>
{error && <HelperText id='delete-confirmation-error' error>{error}</HelperText>}
</div>
<div style={{ display: 'flex', alignItems: 'center', gap: 'var(--spacing-6)', flexWrap: 'wrap' }}>
<Button type='submit' style={destructive}>Delete workspace</Button>
{deleted && <Text size={2} role='status' style={{ color: 'var(--label-secondary)' }}>Workspace scheduled for deletion.</Text>}
</div>
</form>
</div>
);
}Отключённый
Отключённый: разделитель в отключённом Fieldset
У Separator нет отключённого состояния: он лежит внутри Fieldset, который отключён целиком, а пояснение связано с группой через aria-describedby. Переключатель показывает одну и ту же раскладку в обоих состояниях.
import {
Button,
Fieldset,
FieldsetLegend,
Label,
Separator,
Text,
TextField,
TextFieldInput,
} from '@vega-ui/react';
import { useState } from 'react';
export default function MutedSectionBreakInDisabledPanel() {
const [locked, setLocked] = useState(true);
return (
<div style={{ display: 'grid', gap: 'var(--spacing-8)', width: '100%' }}>
<Fieldset disabled={locked} aria-describedby='smtp-lock-note' style={{ display: 'grid', gap: 'var(--spacing-8)' }}>
<FieldsetLegend style={{ marginBlockEnd: 'var(--spacing-8)' }}>Outgoing mail</FieldsetLegend>
<div style={{ display: 'grid', gap: 'var(--spacing-3)' }}>
<Label htmlFor='smtp-host'>SMTP host</Label>
<TextField>
<TextFieldInput id='smtp-host' name='host' defaultValue='smtp.acme.io' />
</TextField>
</div>
<Separator />
<div style={{ display: 'grid', gap: 'var(--spacing-3)' }}>
<Label htmlFor='smtp-port'>Port</Label>
<TextField>
<TextFieldInput id='smtp-port' name='port' inputMode='numeric' defaultValue='587' />
</TextField>
</div>
</Fieldset>
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 'var(--spacing-8)' }}>
<Text id='smtp-lock-note' size={2} style={{ color: 'var(--label-secondary)' }}>
{locked ? 'Managed by your IT team.' : 'Changes apply to new messages only.'}
</Text>
<Button
size='sm'
variant='secondary'
appearance='ghost'
aria-pressed={!locked}
onClick={() => setLocked(!locked)}
style={{ flexShrink: 0 }}
>
{locked ? 'Unlock settings' : 'Lock settings'}
</Button>
</div>
</div>
);
}Пограничный случай
Пограничный случай: вертикальный разделитель без фиксированной высоты
height: 100% схлопывается в ноль, если у родителя нет определённой высоты. В строке, высота которой задаётся содержимым, переопределите её через height: 'auto' и alignSelf: 'stretch', чтобы линия заняла высоту строки.
import { Separator, Text } from '@vega-ui/react';
const stats = [
{ label: 'Revenue', value: '$12.4k' },
{ label: 'Orders', value: '318' },
{ label: 'Refunds', value: '4' },
];
export default function ConstrainedVerticalDivider() {
return (
<div style={{ display: 'flex', gap: 'var(--spacing-10)' }}>
{stats.map((stat, index) => (
<div key={stat.label} style={{ display: 'flex', gap: 'var(--spacing-10)' }}>
{index > 0 && <Separator orientation='vertical' style={{ height: 'auto', alignSelf: 'stretch' }} />}
<div style={{ display: 'grid', gap: 'var(--spacing-1)' }}>
<Text size={2} style={{ color: 'var(--label-secondary)' }}>{stat.label}</Text>
<Text size={5} fontWeight={500} style={{ fontVariantNumeric: 'tabular-nums' }}>{stat.value}</Text>
</div>
</div>
))}
</div>
);
}Пограничный случай: декоративный разделитель
Внутри списка каждый role='separator' озвучивается между пунктами. Если линия чисто декоративная, передайте aria-hidden, чтобы скринридер читал только пункты списка.
import { Separator, Text } from '@vega-ui/react';
const posts = [
{ title: 'Release notes 3.2', date: 'Mar 12' },
{ title: 'Dark mode for charts', date: 'Mar 5' },
{ title: 'Faster CSV exports', date: 'Feb 26' },
];
export default function DecorativeDivider() {
return (
<ul style={{ display: 'grid', gap: 'var(--spacing-6)', width: '100%', margin: 0, padding: 0, listStyle: 'none' }}>
{posts.map((post, index) => (
<li key={post.title} style={{ display: 'grid', gap: 'var(--spacing-6)' }}>
{index > 0 && <Separator aria-hidden />}
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'baseline', gap: 'var(--spacing-8)' }}>
<Text size={2}>{post.title}</Text>
<Text size={2} style={{ color: 'var(--label-secondary)', fontVariantNumeric: 'tabular-nums', flexShrink: 0 }}>{post.date}</Text>
</div>
</li>
))}
</ul>
);
}Анатомия
Обзор
Separator — примитив из одной части, который рендерит тонкую разделительную линию с role='separator'.
Обязательные части
Separator
Обязательная часть. Отвечает и за семантическую роль разделителя, и за визуальную линию.
Необязательные части
Экспортируемых дочерних частей нет.
Порядок композиции
Separator
Допустимые варианты композиции
Горизонтальный разделитель между содержимым и подвалом и вертикальный — между действиями подвала. У строки подвала явная высота, потому что вертикальный разделитель берёт height: 100% от родителя.
import { Button, Card, Heading, Separator, Text } from '@vega-ui/react';
export default function ValidCompositionPatterns() {
return (
<Card style={{ display: 'grid', gap: 'var(--spacing-8)', width: '100%' }}>
<div style={{ display: 'grid', gap: 'var(--spacing-2)' }}>
<Heading as='h3' size={4}>Quarterly report</Heading>
<Text size={2} style={{ color: 'var(--label-secondary)' }}>Revenue grew 12% compared to the previous quarter.</Text>
</div>
<Separator />
<div style={{ display: 'flex', alignItems: 'center', gap: 'var(--spacing-4)', height: 'var(--spacing-16)' }}>
<Button size='sm' variant='secondary' appearance='ghost'>Download PDF</Button>
<Separator orientation='vertical' />
<Button size='sm' variant='secondary' appearance='ghost'>Share</Button>
</div>
</Card>
);
}Недопустимые варианты композиции
Вертикальный разделитель без высоты родителя
Разделитель схлопывается, потому что у него нет контекста высоты.
Разделитель как единственная структура раздела
Это создаёт визуальное разделение без семантической подписи.