TabItem
Rounded pill that fills in when it is selected. It is a single item, not a tab bar: laying several out and deciding which is active is yours to do.
Use this when / not when
- Use for a short filter or preset the user switches between — the quick-add chips of
QuantityPickerare these. - Not for a tab bar with content behind it:
Tabsdraws the bar, scrolls it, and renders the selected tab's body. - Not for a multiple choice in a form —
Checkboxis the real control and is reachable from the keyboard. - Not for a plain action;
Buttonis that.
Import
import { TabItem } from "@onlyoffice/apps-ui-kit/components/tab-item";
Also exported from the root barrel @onlyoffice/apps-ui-kit.
Needs ThemeProvider from @onlyoffice/apps-ui-kit/providers/theme; the selected fill is the
accent colour it supplies.
Stories
Default
An unselected pill: click it to see it fill in, and change any other prop live in the Controls panel below.
Active State
The filled look of a selected pill, for a filter that is already applied when the screen opens (isActive).
Disabled State
A dimmed pill that ignores clicks, for an option that does not apply right now (isDisabled).
With React Node Label
Tab with a React node as label, allowing custom content like icons alongside text. The label renders inside a <p>, so the node has to be phrasing content -- a <span>, not a <div>.
Tab Group
Interactive tab group demonstrating single-selection behavior. Clicking a tab selects it and deselects others.

Multi Select
Three pills that toggle independently, for a filter where several values can apply at once: a click on a selected pill deselects it (withMultiSelect).

Css Customization
Every overridable variable set on one wrapper -- the variables are listed under CSS variables on this page. Documents is selected, for the two active variables; Images is unselected, for the border; Videos is disabled, for the opacity. Radius and padding show on all three.

Minimal example
The pill tracks its own selected look, so keep your state in step by passing isActive.
import { useState } from "react";
import { TabItem } from "@onlyoffice/apps-ui-kit/components/tab-item";
const FILTERS = ["All", "Documents", "Spreadsheets"];
export function FileFilters() {
const [active, setActive] = useState("All");
return (
<div style={{ display: "flex", gap: 8 }}>
{FILTERS.map((filter) => (
<TabItem
key={filter}
label={filter}
isActive={filter === active}
onSelect={() => setActive(filter)}
/>
))}
</div>
);
}
Props
Recipes
Disabled / read-only
isDisabled fades the pill to half opacity and the CSS drops its pointer events — unless it is
also active, in which case it keeps its fill and the click is stopped in the handler instead.
import { TabItem } from "@onlyoffice/apps-ui-kit/components/tab-item";
export function LockedFilter() {
return <TabItem label="Trash" isDisabled onSelect={() => {}} />;
}
Several at once
withMultiSelect is what lets a selected pill be clicked off again; without it a pill that is
selected stays selected however often it is clicked.
import { useState } from "react";
import { TabItem } from "@onlyoffice/apps-ui-kit/components/tab-item";
const TYPES = ["Docs", "Sheets", "Slides"];
export function TypeFilters() {
const [chosen, setChosen] = useState<string[]>([]);
const toggle = (type: string) =>
setChosen((current) =>
current.includes(type)
? current.filter((x) => x !== type)
: [...current, type],
);
return (
<div style={{ display: "flex", gap: 8 }}>
{TYPES.map((type) => (
<TabItem
key={type}
label={type}
withMultiSelect
isActive={chosen.includes(type)}
onSelect={() => toggle(type)}
/>
))}
</div>
);
}
Behaviour the types don't state
- The pill owns its selected look and
isActiveonly seeds it. An effect re-applies the prop whenever it changes, so a controlled group works — but between the click and your state update the pill has already changed colour on its own. - A selected pill does not deselect. Clicking it calls
onSelectand leaves the fill in place unlesswithMultiSelectis set. lockLastSelectionswallows the click entirely. On an already selected pill nothing happens at all —onSelectdoes not fire either — which is how a group keeps at least one item chosen.allowNoSelectionfreezes the look at whatever it was on mount. Despite the name it does not enable deselection: it turns off every internal update, including the effect that copiesisActivein, so the pill's colour stops following the prop. Use it only for a pill whose appearance you drive entirely withclassName.- Pressing an enabled pill shows the selected look — the accent fill and label colour are
applied under
:activetoo, so an unselected pill flashes selected while the pointer is down, and a disabled one does not. - A disabled pill that is also active stays fully opaque and keeps its fill, because the fade rule is skipped for the active state; only the click guard still applies.
- The pill has no width of its own beyond
padding: 4px 16pxandmax-width: 100%, so a long label stretches it until it is capped by the parent, then truncates with an ellipsis. - The label is rendered through
Textat 13px/600, with selection disabled — passing a React node aslabelputs it inside that<p>.
CSS variables
Set them on any ancestor.
Accessibility
- The pill is a
<div>with a click handler: no role, notabIndex, no key handler. It cannot be reached or activated from the keyboard. aria-selectedis set on that same<div>, which has no role to carry it — the attribute is ignored, and assistive technology is told nothing about the selected state.- There is no group semantics: a row of pills is a row of
<div>s, not atablistor aradiogroup, so nothing announces how many options there are or which is chosen. - Where a choice has to be operable by everyone, use
Checkboxor a row ofButtons and style them.
Test ids
Related
Tabs— the real tab bar, with content and keyboard support.Checkbox— the accessible control for the same choice.QuantityPicker— builds its quick-add chips from these.