Skip to content
v2.5.0
English
Русский
Documentation

Option

Option is the presentational selectable-row primitive reused by list-like controls.
Backlog
In progress
Done
option.tsx · 7 lines
import { Option } from '@vega-ui/react';

<div role='listbox' aria-label='Issue status'>
  <Option>Backlog</Option>
  <Option selected>In progress</Option>
  <Option>Done</Option>
</div>
Browser support
Widely available
  • Chrome 111
  • Edge 111
  • Firefox 113
  • Safari 15.4
  • Safari on iOS 15.4
Bundle size
Tested in
14 tests

Summary

Option is the presentational selectable-row primitive reused by list-like controls. It is a div with role='option' and selected, active, size, and disabled states, but it does not manage selection logic by itself. For a working list use Listbox or Select.

Imports

import { Option, type OptionProps, type OptionSize } from '@vega-ui/react';

Exported Types

  • OptionProps
  • OptionSize

Basic Usage

basic-usage.tsx · 12 lines
import { Option } from '@vega-ui/react';

export default function BasicUsage() {
  return (
    <div role='listbox' aria-label='Issue status' style={{ display: 'flex', flexDirection: 'column', gap: 'var(--spacing-3)', width: '100%', maxWidth: 260, padding: 'var(--spacing-4)', borderRadius: 'var(--spacing-8)', background: 'var(--surface-secondary)', border: '1px solid var(--separator-opaque)', boxSizing: 'border-box' }}>
      <Option>Backlog</Option>
      <Option selected>In progress</Option>
      <Option>In review</Option>
      <Option>Done</Option>
    </div>
  );
}

Variants

  • Size: xs, sm, md, lg, xl
  • State: selected, active, disabled
  • Content: plain label or richer icon/text row

Edge Cases

  • Option is a UI primitive, not a full select or menu system.
  • A disabled Option does not call onClick.
  • Parent controls still need to own active, selected, and keyboard navigation logic coherently.

Common Mistakes

  • Using Option alone and expecting selection state management.
  • Mixing Option size with a parent field size that does not match.
  • Forgetting to coordinate selected and visible active state in the parent system.

Examples

Basic

Basic: rows inside a named list

Option renders a row with role='option', so it always goes inside an element with role='listbox' and an accessible name. selected only paints and announces the state; it does not manage it.

basic-usage.tsx · 12 lines
import { Option } from '@vega-ui/react';

export default function BasicUsage() {
  return (
    <div role='listbox' aria-label='Issue status' style={{ display: 'flex', flexDirection: 'column', gap: 'var(--spacing-3)', width: '100%', maxWidth: 260, padding: 'var(--spacing-4)', borderRadius: 'var(--spacing-8)', background: 'var(--surface-secondary)', border: '1px solid var(--separator-opaque)', boxSizing: 'border-box' }}>
      <Option>Backlog</Option>
      <Option selected>In progress</Option>
      <Option>In review</Option>
      <Option>Done</Option>
    </div>
  );
}

Controlled/Stateful

Controlled/Stateful: selected state from parent

A custom list owns both states: selected follows a click or Enter, active follows the arrow keys. The focus stays on the list, which points at the active row with aria-activedescendant, so every row needs an id.

selected-state-from-parent.tsx · 70 lines
import { Option, Text } from '@vega-ui/react';
import { KeyboardEvent, useId, useState } from 'react';

const statuses = [
  { value: 'backlog', label: 'Backlog' },
  { value: 'progress', label: 'In progress' },
  { value: 'review', label: 'In review' },
  { value: 'done', label: 'Done' },
];

export default function SelectedStateFromParent() {
  const id = useId();

Form/Integration

Form/Integration: suggestions for a text field

The text field is the combobox and keeps the focus: it points at the active suggestion with aria-activedescendant, and Enter or a click copies the name into the field, which the form submits as assignee. onMouseDown prevents the row from taking the focus away from the field.

option-in-custom-selector-list.tsx · 99 lines
import { Button, Label, Option, Text, TextField, TextFieldInput } from '@vega-ui/react';
import { FormEvent, KeyboardEvent, useId, useState } from 'react';

const people = ['Amara Okafor', 'Boris Petrov', 'Chloé Lefèvre', 'Dmitri Volkov', 'Elena Rossi', 'Farah Haddad'];

export default function OptionInCustomSelectorList() {
  const id = useId();
  const listId = `${id}-list`;
  const [query, setQuery] = useState('');
  const [active, setActive] = useState(0);
  const [submitted, setSubmitted] = useState('');

Layout/Overlay

Layout/Overlay: rich option content

Rows with an icon and a hint inside a raised panel. The row is still one selection surface; two-line content needs height: 'auto', because the row height is fixed by size.

rich-option-content.tsx · 47 lines
import { Icon, Option, Text } from '@vega-ui/react';
import { CopyIcon, LinkIcon, MailIcon, Share2Icon } from '@vega-ui/icons';

const actions = [
  { value: 'link', label: 'Copy link', hint: 'Anyone with the link can view', icon: LinkIcon },
  { value: 'email', label: 'Send by email', hint: 'Up to 20 recipients', icon: MailIcon },
  { value: 'duplicate', label: 'Duplicate', hint: 'Creates a private copy', icon: CopyIcon },
  { value: 'export', label: 'Export to Drive', hint: 'PDF or DOCX', icon: Share2Icon },
];

export default function RichOptionContent() {
  return (

Disabled

Disabled: unavailable options with a reason

Disabled rows stay visible and ignore clicks. The reason is shown in the row and linked with aria-describedby, so a screen reader announces why the plan cannot be chosen.

unavailable-plan-hint.tsx · 36 lines
import { Option } from '@vega-ui/react';
import { useId } from 'react';

const plans = [
  { value: 'starter', label: 'Starter', reason: '' },
  { value: 'team', label: 'Team', reason: '' },
  { value: 'legacy', label: 'Legacy Pro', reason: 'No longer available for new workspaces' },
  { value: 'enterprise', label: 'Enterprise', reason: 'Requires a sales contract' },
];

export default function UnavailablePlanHint() {
  const id = useId();

Edge

Edge: active but not selected row

active and selected are separate: the active row is where the keyboard is, the selected row is the value. Here "Newest first" is the value while the list points at "Oldest first".

active-but-not-selected-row.tsx · 21 lines
import { Option } from '@vega-ui/react';
import { useId } from 'react';

export default function ActiveButNotSelectedRow() {
  const id = useId();

  return (
    <div
      role='listbox'
      tabIndex={0}
      aria-label='Sort by'
      aria-activedescendant={`${id}-oldest`}

Edge: long labels in a narrow list

Rows have a fixed height, so long text has to be truncated inside the row. The full path stays available in title.

long-label.tsx · 20 lines
import { Option } from '@vega-ui/react';

const folders = [
  'Inbox',
  'Projects / 2026 / Northwind logistics / Contracts and signed amendments',
  'Receipts',
  'Archive / Customer support / Escalations that were resolved after the SLA deadline',
];

export default function LongLabel() {
  return (
    <div role='listbox' aria-label='Move to folder' style={{ display: 'flex', flexDirection: 'column', gap: 'var(--spacing-3)', width: '100%', maxWidth: 260, padding: 'var(--spacing-4)', borderRadius: 'var(--spacing-8)', background: 'var(--surface-secondary)', border: '1px solid var(--separator-opaque)', boxSizing: 'border-box' }}>

Anatomy

Overview

Option is a single-part primitive that renders a div with role='option'.

Required Parts

Option

Required. Owns the full option-row surface, sizing, and presentational states.

Optional Parts

There are no exported child parts.

Composition Order

  1. Option
  2. label and optional rich content

Valid Composition Patterns

rich-option-content.tsx · 47 lines
import { Icon, Option, Text } from '@vega-ui/react';
import { CopyIcon, LinkIcon, MailIcon, Share2Icon } from '@vega-ui/icons';

const actions = [
  { value: 'link', label: 'Copy link', hint: 'Anyone with the link can view', icon: LinkIcon },
  { value: 'email', label: 'Send by email', hint: 'Up to 20 recipients', icon: MailIcon },
  { value: 'duplicate', label: 'Duplicate', hint: 'Creates a private copy', icon: CopyIcon },
  { value: 'export', label: 'Export to Drive', hint: 'PDF or DOCX', icon: Share2Icon },
];

export default function RichOptionContent() {
  return (

Every Option sits inside an element with role='listbox' that has an accessible name. The content of a row can be an icon and two lines of text, but the row stays one selection surface; rows with more than one line need height: 'auto'.

Invalid Composition Patterns

Option used as a standalone selector without parent state

The row renders, but selection semantics are incomplete.

Complex nested interactive controls inside one option

The option row should remain one coherent selection surface.