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

@versini/ui-tabs

v1.3.2

Published

[![npm version](https://img.shields.io/npm/v/@versini/ui-tabs?style=flat-square)](https://www.npmjs.com/package/@versini/ui-tabs) ![npm package minimized gzipped size](<https://img.shields.io/bundlejs/size/%40versini%2Fui-tabs?style=flat-square&label=size

Readme

@versini/ui-tabs

npm version npm package minimized gzipped size

An accessible, zero-layout-shift React tabs component built with TypeScript and TailwindCSS.

The Tabs component provides a fully WAI-ARIA-compliant tab set: keyboard navigation, roving tabindex, automatic/manual activation, horizontal/vertical orientation, and a no-layout-shift active state (the active tab can go bold without nudging its neighbors). It is hand-rolled with no third-party runtime dependency to keep the bundle small.

Table of Contents

Features

  • ♿ Fully accessible: tablist / tab / tabpanel roles, aria-selected, aria-controls/aria-labelledby, roving tabindex, arrow / Home / End keys, automatic or manual activation.
  • 📐 No layout shift: the active tab goes bold without shifting surrounding tabs (an invisible bold twin reserves the width).
  • 🧭 Orientation: horizontal (default) or vertical.
  • 🎨 Surface-aware theming: mode (system | light | dark | alt-system) keeps text readable even on a surface whose lightness differs from the ambient theme.
  • 🎛️ Controlled & uncontrolled: value / onValueChange or defaultValue.
  • 🪶 Tiny: no Radix, only clsx + three small @versini/ui-hooks.
  • 🧪 Type safe: generic over the tab value (Tabs<Value extends string>), so your tab keys stay narrowed.

Installation

npm install @versini/ui-tabs

Note: This component requires TailwindCSS and the @versini/ui-styles plugin for proper styling. See the installation documentation for complete setup instructions.

Usage

import { Tabs, TabsContent, TabsList, TabsTrigger } from "@versini/ui-tabs";

type TabKey = "details" | "history" | "settings";

export const Example = () => (
	<Tabs<TabKey> defaultValue="details">
		<TabsList aria-label="Parcel views">
			<TabsTrigger value="details">Details</TabsTrigger>
			<TabsTrigger value="history">History</TabsTrigger>
			<TabsTrigger value="settings">Settings</TabsTrigger>
		</TabsList>
		<TabsContent value="details">Details panel</TabsContent>
		<TabsContent value="history">History panel</TabsContent>
		<TabsContent value="settings">Settings panel</TabsContent>
	</Tabs>
);

Controlled

const [value, setValue] = useState<TabKey>("details");

<Tabs<TabKey> value={value} onValueChange={setValue}>
	{/* ... */}
</Tabs>;

Note: In uncontrolled mode with no defaultValue, the first enabled tab is auto-selected after mount and onValueChange fires once to report it. Pass defaultValue (or use controlled value) to avoid the mount-time callback.

Navigation / trigger-only

TabsList + TabsTrigger work without any TabsContent (e.g. route-driven tabs). In that mode no aria-controls is emitted, so it stays valid ARIA. Keep your panel content in your own switch when it needs its own state or lazy loading; only the active TabsContent is mounted, so panels with unsaved local state should not live inside TabsContent (a forceMount opt-in is planned).

API

Tabs<Value extends string = string>

| Prop | Type | Default | |------------------|----------------------------------------|------------------| | value | Value | (controlled) | | defaultValue | Value | first enabled trigger | | onValueChange | (value: Value) => void | — | | orientation | "horizontal" \| "vertical" | "horizontal" | | activationMode | "automatic" \| "manual" | "automatic" | | size | "small" \| "medium" \| "large" | "medium" | | mode | "system" \| "light" \| "dark" \| "alt-system" | "system" | | className | string | — |

TabsList

role="tablist". Owns keyboard navigation. Pass aria-label or aria-labelledby to give the tablist an accessible name (required by APG; a dev warning fires if missing). Extra props are forwarded.

TabsTrigger<Value extends string = string>

role="tab". Props: value (required, unique within a Tabs), disabled, plus standard button attributes. Labels may be any non-interactive node (text, icon + text); the no-shift twin duplicates the label, so labels must not contain focusable or id-bearing elements.

TabsContent<Value extends string = string>

role="tabpanel". Props: value (required), plus standard div attributes. Renders only when active and a matching trigger exists.

Accessibility

Implements the WAI-ARIA APG Tabs pattern:

  • Roving tabindex: exactly one trigger is in the tab order; arrow keys move between tabs (wrapping, skipping disabled), Home/End jump to first/last.
  • automatic activation selects on focus; manual requires Enter/Space.
  • Activation keeps focus on the trigger; Tab moves to the active panel.
  • aria-controls is emitted only on the active trigger (whose panel is mounted), avoiding dangling references. APG's example wires it on every tab; this component intentionally deviates because inactive panels are unmounted.
  • The active panel is tabIndex={0}.

Theming and mode

Text color must match the surface the tabs sit on, which is not always the ambient theme. Because dark mode is driven by prefers-color-scheme, the dark: variant follows the OS, not the local surface — so a light card inside a dark-mode app would otherwise get light text on a light background (unreadable).

  • mode="system" (default) — follow the ambient theme. Use when the tabs sit on the page surface (which follows the OS).
  • mode="light" — force dark text. Use on a light surface regardless of OS.
  • mode="dark" — force light text. Use on a dark surface regardless of OS.
  • mode="alt-system" — invert the ambient theme.
// A light card that stays light even when the OS is in dark mode:
<div className="bg-surface-light">
	<Tabs defaultValue="a" mode="light">…</Tabs>
</div>