ts-compcomps
v0.8.0
Published
Type-safe compound components for React with compile-time type safety
Maintainers
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-compcompsPeer dependencies (must be installed):
npm install react react-domQuick 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-compcompsimport { 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 groupscreateTypedTabs- Tabbed interfacescreateTypedSegmentedControl- Segmented controlscreateTypedStepper- Step/stepper components
Optional Components
These require additional dependencies:
npm install @radix-ui/react-accordion @radix-ui/react-checkbox @radix-ui/react-selectThen 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/coreThen 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:
useRadioGroupContextuseTabsContextuseSegmentedControlContextuseStepperContextuseAccordionContext(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 itemsHome/End: Jump to first/last itemEnter/Space: Select item
Tabs:
Arrow Left/Right: Navigate between tabsHome/End: Jump to first/last tabEnter/Space: Activate tab
Accordion:
Enter/Space: Toggle itemTab: Move between accordion items
Select:
Arrow Up/Down: Navigate optionsEnter/Space: Open/selectEscape: CloseHome/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 itemsaria-selected: For tabs and select itemsaria-expanded: For accordion/trigger, selectaria-controls: Links trigger to contentaria-label: For labeled groupsaria-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
