npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@xsolla/xui-tabs

v0.190.0

Published

A cross-platform React tab list with two visual variants — `line` (default underlined) and `segmented` (button-group with sliding indicator on web). Implements the WAI-ARIA tablist pattern with full keyboard navigation. <!-- BEGIN:xui-mcp-instructions:tab

Readme

Tabs

A cross-platform React tab list with two visual variants — line (default underlined) and segmented (button-group with sliding indicator on web). Implements the WAI-ARIA tablist pattern with full keyboard navigation.

A horizontal navigation bar that divides related content into labelled sections. The user switches between sections by clicking a tab item; only one section is active at a time. Each tab can optionally carry a numeric counter badge. Tabs support 2 to 7 items, four sizes, and two alignment modes.

When to use

To organise content that belongs to the same context into distinct, peer-level views — for example, "Overview", "Activity", "Settings" on a profile page

When users need to switch between views without leaving the current page or losing their position in the layout

When the number of views is small (2–7) and all labels fit horizontally without truncation

When not to use

  • When the sections represent a sequence or progression — use a Stepper instead
  • When there are more than 7 tabs — consider a a sidebar navigation, or collapsing less-used items into an overflow menu
  • When the content under each tab is very long and users will rarely switch between them — use anchored sections with a sticky sidebar nav instead
  • As the primary page-level navigation — use a Navigation bar or Sidebar for top-level routing

Content guidelines

Tab labels — use short nouns or noun phrases: "Overview", "Activity", "Members", "Settings", "Billing". Avoid verbs ("Manage settings") and questions ("What's new?").

Label length — aim for 1–2 words per label. Three words is a maximum. Never truncate a tab label with an ellipsis — if the label is too long, shorten it.

Consistency — all labels in the same Tabs instance should follow the same grammatical form and approximate length. Mixing a single-word label ("Users") with a long phrase ("Billing & Payments") in the same bar looks unbalanced.

Counter values — show only counts that are directly actionable or informative. Avoid counters that show total records (e.g. "1,452 transactions") — that belongs in the panel content, not the tab.

Show the counter only when the number is meaningful — when it communicates something actionable (unread messages, pending approvals, new results)

  • Do not show a counter of 0 — hide the badge entirely when the count is zero
  • Cap the display at 99+. Display "99+" for values ≥ 100 rather than showing a 3-digit number
  • The counter palette should match the tab's contextual state: use Brand on the active tab, Default on inactive tabs
  • Badge rendered at the far right of the tab item. Enable with badge=true. Unlike the counter, it carries no number — it signals only that something new or unseen exists in that section.
  • Hide the counter entirely when the count is zero — do not show "0"

Behaviour guidelines

Tab switching — clicking a Default tab immediately switches the active view: the clicked tab transitions to State=Active, the previously active tab returns to State=Default, and the associated content panel below is shown/hidden accordingly. There is no transition animation on the content unless the product design calls for it.

Active state persistence — the active tab should be preserved across filter changes, searches, and other in-page interactions. Only navigate away from the active tab if the user explicitly clicks a different one.

Disabled tabs — use State=Disable only when a section is temporarily inaccessible (e.g. insufficient permissions, feature not yet available). Always provide a tooltip or nearby explanation for why the tab is disabled. Never disable all tabs simultaneously.

Counter updates — counter values should update in real time when new data arrives (e.g. a new message). Update the .counter-tab-item value without remounting the Tabs component. If the count drops to 0, remove the badge rather than showing "0".

Overflow handling — the Tabs component does not support horizontal scrolling or overflow menus natively. If the number of items exceeds 7 or the labels overflow their containers, this is a design problem — shorten the labels or reduce the item count rather than implementing a scroll container.

URL routing — when tabs represent different routes or states that should be bookmarkable or shareable, reflect the active tab in the URL (query param or path segment). On page load, restore the active tab from the URL.

Align left with dynamic content — when Align left=True and tab labels can change at runtime (e.g. localised strings of varying length), ensure the container has a minimum width and the tab bar does not collapse narrower than the shortest label.

Accessibility

The Tabs container must use role="tablist" with an aria-label describing the group (e.g. aria-label="Account sections").

Each .tab-item must use role="tab" with aria-selected="true" for the active tab and aria-selected="false" for all others. Disabled tabs must also have aria-disabled="true".

Each tab must have aria-controls pointing to the id of its associated content panel. The content panel must have role="tabpanel" and aria-labelledby pointing back to the tab's id. Keyboard navigation within the tab list:

  • ← / → arrows move focus between tabs (automatic activation or manual activation depending on product preference)
  • Home moves focus to the first tab; End moves focus to the last tab
  • Enter / Space activates the focused tab in manual-activation mode
  • Tab moves focus out of the tab list into the active panel's content

← / → arrows move focus between tabs (automatic activation or manual activation depending on product preference)

  • Home moves focus to the first tab; End moves focus to the last tab
  • Enter / Space activates the focused tab in manual-activation mode
  • Tab moves focus out of the tab list into the active panel's content
  • When a counter is present, include its value in the tab's aria-label — e.g. aria-label="Activity, 5 unread" — so screen readers announce the count without requiring navigation to the badge.
  • When a badge is present (dot only, no number), include a qualitative description in aria-label — e.g. aria-label="Messages, new".
  • Icon-only tabs (label=false) must have aria-label with the tab name — the icon alone is not sufficient for screen readers.
  • Decorative icons within a tab (when a label is also present) must have aria-hidden="true" so they are not announced redundantly alongside the label.
  • Disabled tabs must remain focusable (do not use tabindex="-1" on disabled tabs) so keyboard users can discover them. Announce the reason they are disabled via aria-describedby.
  • The active indicator underline must not be communicated by colour alone — the aria-selected attribute is the primary signal for assistive technology.

Installation

npm install @xsolla/xui-tabs

Imports

import {
  Tabs,
  TabPanel,
  type TabItemType,
  type TabsProps,
  type TabPanelProps,
} from '@xsolla/xui-tabs';

Quick start

import * as React from 'react';
import { Tabs, TabPanel } from '@xsolla/xui-tabs';

export default function QuickStart() {
  const [active, setActive] = React.useState('overview');
  const tabs = [
    { id: 'overview', label: 'Overview' },
    { id: 'pricing', label: 'Pricing' },
  ];
  return (
    <div>
      <Tabs id="qs" tabs={tabs} activeTabId={active} onChange={setActive} />
      <TabPanel id="overview" tabsId="qs" hidden={active !== 'overview'}>Overview content</TabPanel>
      <TabPanel id="pricing" tabsId="qs" hidden={active !== 'pricing'}>Pricing content</TabPanel>
    </div>
  );
}

API Reference

<Tabs>

| Prop | Type | Default | Description | | --- | --- | --- | --- | | tabs | TabItemType[] | — | Required. Tab definitions. | | activeTabId | string | — | Currently active tab. | | onChange | (id: string) => void | — | Called when tab selection changes. | | size | "xl" \| "lg" \| "md" \| "sm" | "md" | Size variant. | | variant | "line" \| "segmented" | "line" | Visual style. | | alignLeft | boolean | true | Line variant only — left vs centre alignment. | | stretched | boolean | false | Segmented variant only — distribute items at equal width. | | activateOnFocus | boolean | true | Activate the focused tab automatically; otherwise require Enter/Space. | | id | string | — | Root id used to derive *-tablist, *-tab-{id}, and *-tabpanel-{id} ids. | | aria-label | string | — | Accessible label for the tab list. | | aria-labelledby | string | — | ID of an element labelling the tab list. | | testID | string | — | Test identifier for the tab list container. |

Inherits ThemeOverrideProps (themeMode, themeProductContext).

<TabPanel>

| Prop | Type | Default | Description | | --- | --- | --- | --- | | id | string | — | Required. Matches the corresponding tab.id. | | tabsId | string | — | Required. Parent Tabs id. | | hidden | boolean | false | Whether the panel is hidden. | | children | ReactNode | — | Required. Panel content. | | aria-label | string | — | Accessible label for the panel. | | testID | string | — | Test identifier. |

TabItemType

interface TabItemType {
  id: string;
  label: string;
  icon?: React.ReactNode;
  counter?: string | number;
  counterPalette?: "brand" | "tertiary" | "default"; // line variant only; default "brand"
  badge?: boolean | string | number;
  disabled?: boolean;
  "aria-label"?: string;
  testID?: string; // applied as data-testid on this individual tab button
}

Exported types

| Type | Description | | --- | --- | | TabsProps | Props for <Tabs>. | | TabPanelProps | Props for <TabPanel>. | | TabItemType | Tab definition. |

Keyboard navigation

| Key | Action | | --- | --- | | Arrow Right / Down | Move to next enabled tab. | | Arrow Left / Up | Move to previous enabled tab. | | Home | Jump to first enabled tab. | | End | Jump to last enabled tab. | | Enter / Space | Activate the focused tab when activateOnFocus={false}. |

Examples

Tabs with icons and counters

import * as React from 'react';
import { Tabs } from '@xsolla/xui-tabs';
import { Home, User, Settings } from '@xsolla/xui-icons-base';

export default function IconTabs() {
  const [active, setActive] = React.useState('home');
  const tabs = [
    { id: 'home', label: 'Home', icon: <Home size={16} /> },
    { id: 'profile', label: 'Profile', icon: <User size={16} />, counter: 5 },
    { id: 'settings', label: 'Settings', icon: <Settings size={16} /> },
  ];
  return <Tabs tabs={tabs} activeTabId={active} onChange={setActive} />;
}

Per-tab test IDs

import * as React from 'react';
import { Tabs } from '@xsolla/xui-tabs';

export default function TestableTabs() {
  const [active, setActive] = React.useState('general');
  const tabs = [
    { id: 'general', label: 'General', testID: 'tab-general' },
    { id: 'billing', label: 'Billing', testID: 'tab-billing' },
  ];
  // Each tab button renders its own data-testid (e.g. data-testid="tab-general").
  return <Tabs testID="settings-tabs" tabs={tabs} activeTabId={active} onChange={setActive} />;
}

Segmented variant

import * as React from 'react';
import { Tabs } from '@xsolla/xui-tabs';

export default function SegmentedTabs() {
  const [active, setActive] = React.useState('daily');
  const tabs = [
    { id: 'daily', label: 'Daily' },
    { id: 'weekly', label: 'Weekly' },
    { id: 'monthly', label: 'Monthly' },
  ];
  return <Tabs tabs={tabs} activeTabId={active} onChange={setActive} variant="segmented" />;
}

Stretched segmented

import * as React from 'react';
import { Tabs } from '@xsolla/xui-tabs';

export default function StretchedTabs() {
  const [active, setActive] = React.useState('login');
  const tabs = [
    { id: 'login', label: 'Log in' },
    { id: 'signup', label: 'Sign up' },
  ];
  return <Tabs tabs={tabs} activeTabId={active} onChange={setActive} variant="segmented" stretched />;
}

Disabled tab

import * as React from 'react';
import { Tabs } from '@xsolla/xui-tabs';

export default function DisabledTab() {
  const [active, setActive] = React.useState('a');
  const tabs = [
    { id: 'a', label: 'Active' },
    { id: 'b', label: 'Disabled', disabled: true },
    { id: 'c', label: 'Another' },
  ];
  return <Tabs tabs={tabs} activeTabId={active} onChange={setActive} />;
}

Manual activation

import * as React from 'react';
import { Tabs } from '@xsolla/xui-tabs';

export default function ManualActivation() {
  const [active, setActive] = React.useState('one');
  const tabs = [
    { id: 'one', label: 'One' },
    { id: 'two', label: 'Two' },
    { id: 'three', label: 'Three' },
  ];
  return (
    <Tabs
      tabs={tabs}
      activeTabId={active}
      onChange={setActive}
      activateOnFocus={false}
    />
  );
}

Full panels

import * as React from 'react';
import { Tabs, TabPanel } from '@xsolla/xui-tabs';

export default function FullTabs() {
  const [active, setActive] = React.useState('dashboard');
  const tabs = [
    { id: 'dashboard', label: 'Dashboard' },
    { id: 'analytics', label: 'Analytics', counter: 5 },
    { id: 'reports', label: 'Reports' },
  ];
  return (
    <div>
      <Tabs id="main" tabs={tabs} activeTabId={active} onChange={setActive} />
      <div style={{ padding: 24, border: '1px solid #eee', borderTop: 'none' }}>
        <TabPanel id="dashboard" tabsId="main" hidden={active !== 'dashboard'}>Dashboard content.</TabPanel>
        <TabPanel id="analytics" tabsId="main" hidden={active !== 'analytics'}>Analytics content.</TabPanel>
        <TabPanel id="reports" tabsId="main" hidden={active !== 'reports'}>Reports content.</TabPanel>
      </div>
    </div>
  );
}

Accessibility

  • Implements the WAI-ARIA tablist pattern with role="tablist", role="tab", and role="tabpanel".
  • aria-selected, aria-controls, and aria-labelledby link tabs and panels.
  • Roving tabIndex moves focus to the active tab; arrow keys cycle through enabled tabs.
  • The segmented variant's sliding indicator is marked aria-hidden.

Platform Support

  • Web — segmented variant uses a smooth sliding active indicator backed by ResizeObserver.
  • React Native — segmented variant falls back to per-tab background highlighting (no animation).