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

ts-compcomps

v0.8.0

Published

Type-safe compound components for React with compile-time type safety

Readme

TS-Compcomps

A generic factory library for creating type-safe compound components in React. Provides compile-time type safety for RadioGroup, Tabs, SegmentedControl, Stepper, Accordion, CheckboxGroup, and Select components.

Features

  • Compile-time Type Safety: TypeScript catches type errors at compile time, not runtime
  • Multiple UI Library Support: Works with Radix UI, Ark UI, and Kobalte
  • Optional Components: Accordion, CheckboxGroup, and Select available as separate imports
  • Memoized Components: All components are memoized for optimal performance
  • Error Boundary: Built-in error boundary for graceful error handling
  • Hooks for Extensibility: Use hooks to create custom child components
  • Generic Factory: Use createCompoundComponent() for custom compound components
  • React Server Components: Compatible with Next.js App Router and RSC
  • Zod Integration: Generate types from Zod schemas (optional import)

React Server Components

This library is compatible with React Server Components. The 'use client' directive is automatically included for Next.js App Router support.

// Works in both client and server components
import { createTypedRadioGroup } from 'ts-compcomps';

const StatusRadio = createTypedRadioGroup<'draft' | 'published'>();

Installation

npm install ts-compcomps
# or
pnpm add ts-compcomps
# or
yarn add ts-compcomps

Peer dependencies (must be installed):

npm install react react-dom

Quick Start

import { createTypedRadioGroup } from 'ts-compcomps';

// Define your value type once
type Status = 'draft' | 'published' | 'archived';

// Create typed factory
const StatusRadio = createTypedRadioGroup<Status>();

// Use with full type safety
function MyComponent() {
  const [status, setStatus] = useState<Status>('draft');

  return (
    <StatusRadio.RadioGroup value={status} onValueChange={setStatus}>
      <StatusRadio.RadioItem value="draft">Draft</StatusRadio.RadioItem>
      <StatusRadio.RadioItem value="published">Published</StatusRadio.RadioItem>
      <StatusRadio.RadioItem value="archived">Archived</StatusRadio.RadioItem>
    </StatusRadio.RadioGroup>
  );
}

Generic Compound Component Factory

Use createCompoundComponent() to build your own type-safe compound components:

import { createCompoundComponent } from 'ts-compcomps';

// Define your context type
interface MenuContextValue {
  activeIndex: number;
  onItemSelect: (index: number) => void;
}

// Create the factory
const Menu = createCompoundComponent<MenuContextValue>({
  Root: 'div',
  Child: 'button',
});

// Use with full type safety
function MyMenu() {
  const [activeIndex, setActiveIndex] = useState(0);

  return (
    <Menu.Root>
      <Menu.ContextProvider value={{ activeIndex, onItemSelect: setActiveIndex }}>
        <Menu.Child>Item 1</Menu.Child>
        <Menu.Child>Item 2</Menu.Child>
      </Menu.ContextProvider>
    </Menu.Root>
  );
}

Zod Integration (Optional)

Generate type-safe compound components from Zod schemas:

npm install zod ts-compcomps
# or
pnpm add zod ts-compcomps
import { createTypedFromZod } from 'ts-compcomps/zod';
import { z } from 'zod';

// Create from Zod enum schema
const StatusSchema = z.enum(['draft', 'published', 'archived']);
const StatusRadio = createTypedFromZod(StatusSchema, 'radio');

// Types are automatically inferred from the Zod schema
<StatusRadio.RadioGroup value={status} onValueChange={setStatus}>
  <StatusRadio.RadioItem value="draft">Draft</StatusRadio.RadioItem>
</StatusRadio.RadioGroup>

API

Core Components (Built-in)

These components are included in the main package:

  • createTypedRadioGroup - Radio button groups
  • createTypedTabs - Tabbed interfaces
  • createTypedSegmentedControl - Segmented controls
  • createTypedStepper - Step/stepper components

Optional Components

These require additional dependencies:

npm install @radix-ui/react-accordion @radix-ui/react-checkbox @radix-ui/react-select

Then import from the subpath:

import { createTypedAccordion } from 'ts-compcomps/accordion';
import { createTypedCheckboxGroup } from 'ts-compcomps/checkbox';
import { createTypedSelect } from 'ts-compcomps/select';

Alternative UI Libraries

Support for Ark UI and Kobalte backends for components where you prefer those libraries:

npm install @ark-ui/react-tabs @ark-ui/react-radio-group
# or
npm install @kobalte/core

Then import from the subpath:

// Ark UI backends
import { createTypedRadioGroupArk } from 'ts-compcomps/ark';
import { createTypedTabsArk } from 'ts-compcomps/ark';

// Kobalte backends
import { createTypedRadioGroupKobalte } from 'ts-compcomps/kobalte';
import { createTypedTabsKobalte } from 'ts-compcomps/kobalte';

Components

createTypedRadioGroup

const Radio = createTypedRadioGroup<Status>();

<Radio.RadioGroup value={status} onValueChange={setStatus}>
  <Radio.RadioItem value="draft">Draft</Radio.RadioItem>
  <Radio.RadioItem value="published">Published</Radio.RadioItem>
</Radio.RadioGroup>

createTypedTabs

const Tabs = createTypedTabs<TabId>();

<Tabs.Tabs defaultValue="home">
  <Tabs.TabList aria-label="Navigation">
    <Tabs.Tab value="home">Home</Tabs.Tab>
    <Tabs.Tab value="about">About</Tabs.Tab>
  </Tabs.TabList>
  <Tabs.TabPanel value="home">Home content</Tabs.TabPanel>
  <Tabs.TabPanel value="about">About content</Tabs.TabPanel>
</Tabs.Tabs>

createTypedSegmentedControl

const Segmented = createTypedSegmentedControl<'day' | 'week' | 'month'>();

<Segmented.SegmentedControl defaultValue="week">
  <Segmented.SegmentedControlItem value="day">Day</Segmented.SegmentedControlItem>
  <Segmented.SegmentedControlItem value="week">Week</Segmented.SegmentedControlItem>
</Segmented.SegmentedControl>

createTypedStepper

const Stepper = createTypedStepper<number>();

<Stepper.Stepper value={1} min={0} max={10} step={1}>
  <Stepper.StepperItem value={0}>0</Stepper.StepperItem>
  <Stepper.StepperPrevious>Previous</Stepper.StepperPrevious>
  <Stepper.StepperNext>Next</Stepper.StepperNext>
</Stepper.Stepper>

createTypedAccordion (optional)

const Accordion = createTypedAccordion();

<Accordion.Accordion>
  <Accordion.AccordionItem value="item-1">
    <Accordion.AccordionTrigger>What is this?</Accordion.AccordionTrigger>
    <Accordion.AccordionContent>It's type-safe!</Accordion.AccordionContent>
  </Accordion.AccordionItem>
</Accordion.Accordion>

createTypedCheckboxGroup (optional)

const Checkboxes = createTypedCheckboxGroup<Color>();

<Checkboxes.CheckboxGroup onValueChange={handleChange}>
  <Checkboxes.CheckboxItem value="red">Red</Checkboxes.CheckboxItem>
  <Checkboxes.CheckboxItem value="green">Green</Checkboxes.CheckboxItem>
</Checkboxes.CheckboxGroup>

createTypedSelect (optional)

const Select = createTypedSelect<Color>();

<Select.Select value={color} onValueChange={setColor}>
  <Select.SelectTrigger placeholder="Select..." />
  <Select.SelectContent>
    <Select.SelectItem value="red">Red</Select.SelectItem>
  </Select.SelectContent>
</Select.Select>

Hooks for Extensibility

Each factory exports a use*Context hook for creating custom child components:

const { useRadioGroupContext } = createTypedRadioGroup<Status>();

// Use in your custom component
function MyCustomRadioItem({ value, children }) {
  const { value: currentValue, onValueChange } = useRadioGroupContext();

  return (
    <button onClick={() => onValueChange?.(value)}>
      {children}
    </button>
  );
}

Available hooks:

  • useRadioGroupContext
  • useTabsContext
  • useSegmentedControlContext
  • useStepperContext
  • useAccordionContext (optional)
  • useCheckboxGroupContext (optional)
  • useSelectContext (optional)

Error Handling

ErrorBoundary

import { ErrorBoundary } from 'ts-compcomps';

<ErrorBoundary
  fallback={(error, reset) => (
    <div>
      <p>Error: {error.message}</p>
      <button onClick={reset}>Try again</button>
    </div>
  )}
>
  <YourComponent />
</ErrorBoundary>

Context Errors

// This throws a helpful error:
Error: RadioItem must be used within RadioGroup.
Did you forget to wrap your RadioItems in RadioGroup?

Type Safety Examples

String Literal Unions (Recommended)

type Status = 'draft' | 'published' | 'archived';
const Radio = createTypedRadioGroup<Status>();

// ✅ Valid
<Radio.RadioItem value="draft" />

// ❌ TypeScript Error: Type '"invalid"' is not assignable to type 'Status'
<Radio.RadioItem value="invalid" />

Number Types

const NumberRadio = createTypedRadioGroup<number>({
  parseValue: (value: string) => Number(value),
});

<NumberRadio.RadioGroup value={1}>
  <NumberRadio.RadioItem value={1}>One</NumberRadio.RadioItem>
</NumberRadio.RadioGroup>

Bundle Size

  • ES Module: ~48KB (12.9KB gzipped)
  • CommonJS: ~33KB (11KB gzipped)

Requirements

  • React 18+
  • TypeScript 5.0+

Accessibility

This library leverages Radix UI primitives, which provide comprehensive accessibility out of the box.

Keyboard Navigation

All components support full keyboard navigation:

RadioGroup / SegmentedControl:

  • Arrow Left/Right / Arrow Up/Down: Navigate between items
  • Home / End: Jump to first/last item
  • Enter / Space: Select item

Tabs:

  • Arrow Left/Right: Navigate between tabs
  • Home / End: Jump to first/last tab
  • Enter / Space: Activate tab

Accordion:

  • Enter / Space: Toggle item
  • Tab: Move between accordion items

Select:

  • Arrow Up/Down: Navigate options
  • Enter / Space: Open/select
  • Escape: Close
  • Home / End: Jump to first/last

ARIA Attributes

Radix automatically applies appropriate ARIA attributes:

  • role: The appropriate ARIA role (radio, tab, listbox, etc.)
  • aria-checked: For radio/checkbox items
  • aria-selected: For tabs and select items
  • aria-expanded: For accordion/trigger, select
  • aria-controls: Links trigger to content
  • aria-label: For labeled groups
  • aria-labelledby: For labeled content

Focus Management

  • Automatic focus management on open/close
  • Return focus to trigger on close (for popups/selects)
  • Visible focus indicators

Interested in Consolidating?

If you're a maintainer considering rolling similar functionality into your core package, I'm happy to point users your direction instead. Open an issue to discuss.

License

MIT