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

Separator

Separator — примитив визуального разделения для горизонтальных и вертикальных границ между соседними областями интерфейса.
ProfileBillingSecurity
separator.tsx · 9 строк
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
Размер в бандле
История версий
Добавлен в v1.0.0

Кратко

Separator — примитив визуального разделения для горизонтальных и вертикальных границ между соседними областями интерфейса.

Импорты

import { Separator, type SeparatorProps } from '@vega-ui/react';

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

  • SeparatorProps

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

basic-usage.tsx · 17 строк
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 родителя, а не сам разделитель.

horizontal-section-divider.tsx · 25 строк
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 с подписью из заголовка, так что структура не держится на линии.

conditional-divider.tsx · 57 строк
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.

divider-between-field-groups.tsx · 62 строки
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.

vertical-divider-in-toolbar.tsx · 44 строки
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, а ввод снимает ошибку.

divider-before-destructive-section.tsx · 70 строк
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. Переключатель показывает одну и ту же раскладку в обоих состояниях.

muted-section-break-in-disabled-panel.tsx · 51 строка
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', чтобы линия заняла высоту строки.

constrained-vertical-divider.tsx · 23 строки
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, чтобы скринридер читал только пункты списка.

decorative-divider.tsx · 23 строки
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

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

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

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

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

  1. Separator

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

Горизонтальный разделитель между содержимым и подвалом и вертикальный — между действиями подвала. У строки подвала явная высота, потому что вертикальный разделитель берёт height: 100% от родителя.

valid-composition-patterns.tsx · 18 строк
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>
  );
}

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

Вертикальный разделитель без высоты родителя

Разделитель схлопывается, потому что у него нет контекста высоты.

Разделитель как единственная структура раздела

Это создаёт визуальное разделение без семантической подписи.