@jiwambe/components
v0.6.6
Published
A production-ready, accessible React component library built with Tailwind CSS and a semantic token design system. Ships as ESM with full TypeScript support.
Readme
@jiwambe/components
A production-ready, accessible React component library built with Tailwind CSS and a semantic token design system. Ships as ESM with full TypeScript support.
Current release: 0.6.6. Release history and migration notes live in CHANGELOG.md.
Publishing (maintainers): pnpm publish runs prepublishOnly, which executes pnpm run test, pnpm run build, and pnpm audit. Resolve or accept any audit findings before the tarball is created. Releases via Changesets (pnpm run release) use the same hook when the package is published.
Installation
pnpm add @jiwambe/componentsPeer Dependencies
Make sure you have the following installed in your project:
pnpm add react react-dom tailwindcssFont Loading
The design system uses Instrument Sans and Inter fonts. Add them to your project (for example via Google Fonts):
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link
href="https://fonts.googleapis.com/css2?family=Instrument+Sans:wght@400;500;600;700&family=Inter:wght@400;500;600&display=swap"
rel="stylesheet"
/>Setup
1. Install
pnpm add @jiwambe/componentsPeer dependencies: react, react-dom, tailwindcss
2. Configure Tailwind
In your project's tailwind.config.ts, use the Jiwambe preset. The preset includes the plugin (which injects all design token CSS variables) and the safelist (which ensures all spacing utility classes survive purging).
import uiPreset from '@jiwambe/components/tailwind.preset';
export default {
presets: [uiPreset],
content: [
'./src/**/*.{js,ts,jsx,tsx,mdx}',
// Recommended: prevents spacing classes from being purged
'./node_modules/@jiwambe/components/dist/**/*.js',
],
};If you need to add your own plugins, extend the preset:
export default {
presets: [uiPreset],
plugins: [yourOtherPlugin],
content: [...],
};3. Usage
import { Box, Stack, Container, Grid } from '@jiwambe/components';
// Spacing tokens match the Jiwambe design token vocabulary:
<Container>
<Stack spacing="space-4" mt="space-9" direction="column">
<Box p="space-4">Fixed padding</Box>
<Box px="fluid-4-9" py="space-6">Fluid horizontal padding</Box>
<Box m="auto" px="fluid-4-9">Centered box</Box>
</Stack>
</Container>
<Grid gap="space-4" p="space-2">
<Box>Cell 1</Box>
<Box>Cell 2</Box>
</Grid>
// Container gutters default to fluid-4-9 and can be overridden:
<Container /> // fluid-4-9 gutters (default)
<Container px="space-9" /> // custom gutters
<Container disableGutters /> // no guttersSpacing token reference
| Token | CSS variable | Resolved value | |---------------|---------------------------|-----------------------------------| | space-0 | var(--space-0) | 0rem | | space-0-25 | var(--space-0-25) | 0.0625rem | | space-1 | var(--space-1) | 0.25rem | | space-2 | var(--space-2) | 0.5rem | | space-3 | var(--space-3) | 0.75rem | | space-4 | var(--space-4) | 1rem | | space-5 | var(--space-5) | 1.25rem | | space-6 | var(--space-6) | 1.5rem | | space-8 | var(--space-8) | 2rem | | space-9 | var(--space-9) | 2.25rem | | space-12 | var(--space-12) | 3rem | | space-16 | var(--space-16) | 4rem | | space-18 | var(--space-18) | 4.5rem | | space-24 | var(--space-24) | 6rem | | space-30 | var(--space-30) | 7.5rem | | space-36 | var(--space-36) | 9rem | | space-48 | var(--space-48) | 12rem | | space-72 | var(--space-72) | 18rem | | space-list-indent | var(--space-list-indent) | 1.6875rem (27px) | | fluid-1-2 | var(--space-fluid-1-2) | clamp(0.25rem, 0.1706rem + 0.3968vw, 0.5rem) | | fluid-2-4 | var(--space-fluid-2-4) | clamp(0.5rem, 0.3413rem + 0.7937vw, 1rem) | | fluid-4-5 | var(--space-fluid-4-5) | clamp(1rem, 0.9206rem + 0.3968vw, 1.25rem) | | fluid-4-6 | var(--space-fluid-4-6) | clamp(1rem, 0.8413rem + 0.7937vw, 1.5rem) | | fluid-4-8 | var(--space-fluid-4-8) | clamp(1rem, 0.6825rem + 1.5873vw, 2rem) | | fluid-4-9 | var(--space-fluid-4-9) | clamp(1rem, 0.6032rem + 1.9841vw, 2.25rem) | | fluid-5-6 | var(--space-fluid-5-6) | clamp(1.25rem, 1.1706rem + 0.3968vw, 1.5rem) | | fluid-6-9 | var(--space-fluid-6-9) | clamp(1.5rem, 1.2619rem + 1.1905vw, 2.25rem) | | fluid-8-16 | var(--space-fluid-8-16) | clamp(2rem, 1.3651rem + 3.1746vw, 4rem) | | fluid-16-18 | var(--space-fluid-16-18) | clamp(4rem, 3.8413rem + 0.7937vw, 4.5rem) | | fluid-30-36 | var(--space-fluid-30-36) | clamp(7.5rem, 7.0238rem + 2.381vw, 9rem) | | fluid-48-72 | var(--space-fluid-48-72) | clamp(12rem, 10.0952rem + 9.5238vw, 18rem) |
Components
Typography
Renders text with design-system typographic styles.
<Typography variant="title-xl" as="h1">Big Headline</Typography>
<Typography variant="text-md">Body copy here.</Typography>
<Typography variant="form-label" as="label">Email</Typography>Props:
| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| variant | TypographyVariant | — | Required. One of title-sm, title-md, title-lg, title-xl, text-xs, text-xs-bold, text-sm, text-sm-bold, text-md, text-md-bold, text-xl, form-text, form-label, btn-small, btn-reg, link-md. |
| as | string | "p" | HTML element to render (h1–h6, p, span, label). |
| className | string | — | Additional CSS classes. |
Button
Interactive button with four variants and three sizes.
<Button variant="primary">Submit</Button>
<Button variant="secondary" size="small">Cancel</Button>
<Button variant="ghost" disabled>Disabled</Button>Props:
| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| variant | "primary" \| "secondary" \| "inverse" \| "ghost" \| "destructive" | — | Required. Visual style. |
| size | "small" \| "medium" \| "large" | "small" | Height, padding, and typography scale. |
| disabled | boolean | false | Disables the button. |
Link
Anchor element with variant-based styling.
<Link variant="primary" href="/about">About Us</Link>
<Link variant="inverse" href="/contact">Contact</Link>Props:
| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| variant | "primary" \| "secondary" \| "tertiary" \| "inverse" | "primary" | Visual style. |
| disabled | boolean | false | Prevents navigation and applies disabled styles. |
Accordion
Single-expansion accordion with keyboard navigation.
<Accordion
items={[
{ title: 'FAQ 1', content: <p>Answer to FAQ 1</p> },
{ title: 'FAQ 2', content: <p>Answer to FAQ 2</p> },
{ title: 'FAQ 3', content: <p>Answer to FAQ 3</p>, disabled: true },
]}
defaultOpenIndex={0}
/>Props:
| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| items | AccordionItem[] | — | Required. Array of { title, content, disabled? }. |
| defaultOpenIndex | number | — | Initially open item index (uncontrolled). |
| value | number | — | Open item index (controlled). |
| onChange | (index: number) => void | — | Called when the open item changes (controlled). |
FAQ
FAQ section with heading, accordion of question/answer pairs, and optional footer link. Multiple items may be open at the same time (per-item toggle; controlled or uncontrolled) with full keyboard navigation. Layout and typography use design tokens aligned with Figma web · library FAQ (fluid section padding, --font-line-snug + FAQ letter-spacing on the heading, text-title-md / text-text-md / text-link-md, and space-* gaps at lg 940px per preset).
<FAQ
heading="Questions"
items={[
{ question: 'What is X?', answer: 'X is...' },
{ question: 'How do I Y?', answer: 'You can Y by...', disabled: true },
]}
defaultOpenIndex={0}
link={{ label: 'View all FAQs', href: '/faq' }}
linkAs={NextLink}
/>Props:
| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| heading | string | "FAQ" | Section heading above the list. |
| items | FAQItem[] | — | Required. Array of { question, answer, disabled? }. |
| defaultOpenIndex | number \| number[] | [] | Index(es) open by default (uncontrolled); several may be set. |
| value | number \| number[] | — | Open index(es) (controlled); several may be open. |
| onChange | (indices: number[]) => void | — | Called when open indices change (controlled). |
| link | { label: string; href: string } | — | Optional footer link (e.g. "View all FAQs"). |
| linkAs | ElementType | "a" | Component for the footer link (e.g. next/link). |
Tab
Horizontal tabs with full ARIA support.
<Tab
tabs={[
{ label: 'Overview', content: <p>Overview content</p> },
{ label: 'Details', content: <p>Detail content</p> },
{ label: 'Settings', content: <p>Settings content</p>, disabled: true },
]}
defaultActiveIndex={0}
/>Props:
| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| tabs | TabItem[] | — | Required. Array of { label, content, disabled? }. |
| defaultActiveIndex | number | 0 | Initially active tab (uncontrolled). |
| activeIndex | number | — | Active tab index (controlled). |
| onTabChange | (index: number) => void | — | Called when the active tab changes (controlled). |
List
Semantic list with correct ul/ol structure. For marker="disc" and marker="decimal" uses block layout and item margin (not flex/gap) so markers render reliably. Use indent and itemPadding="none" for legal/body-copy lists.
<List spacing="space-2">
<List.Item size="sm">Item one</List.Item>
<List.Item size="sm">Item two</List.Item>
</List>
<List marker="decimal" spacing="space-3">
<List.Item size="md">Step one</List.Item>
<List.Item size="md">Step two</List.Item>
</List>
{/* Legal/body-copy: 27px indent, no extra item padding */}
<List indent="space-list-indent" itemPadding="none" spacing="space-2">
<List.Item size="md">First term.</List.Item>
<List.Item size="md">Second term.</List.Item>
</List>
<List marker="none" spacing="space-1">
<List.Item><Link href="/about">About</Link></List.Item>
</List>Props:
| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| marker | "disc" \| "decimal" \| "none" | "disc" | Marker style; decimal renders <ol>. |
| spacing | SpacingToken | "space-2" | Vertical spacing between items (margin for disc/decimal, gap for none). |
| indent | SpacingToken \| string | — | Left indent (e.g. "space-6", "space-list-indent" for 27px, or "27px"). |
| itemPadding | "default" \| "none" | "default" | Item left padding; "none" for body-copy lists. |
Card
Card with optional image, title, description, and action. Aligned with Figma (web · library): media cards use secondary background, fluid padding/gaps, title-md/text-md typography, and 48px action button with rounded-rad-md.
<Card type="text-only" title="Title" description="Description." label="Action" onLabelClick={() => {}} />
<Card
type="media-horizontal"
imageSrc="/img.jpg"
imageAlt="Product"
title="Product"
description="Description."
label="View"
labelHref="/product"
/>
<Card
type="media-horizontal"
imageSrc="/img.jpg"
imageAlt="Product"
message="Special Offer"
title="Product"
description="Description."
label="Get started"
/>Props: type (text-only | media-horizontal | media-vertical), imageSrc, imageAlt, message (overlay on image), title, description, label, onLabelClick, labelHref, linkAs.
Breadcrumb
Breadcrumb navigation with slash or chevron separator. Optional truncation for long paths (first … second-to-last / current).
<Breadcrumb
items={[
{ label: 'Home', href: '/' },
{ label: 'Products', href: '/products' },
{ label: 'Detail' },
]}
/>
<Breadcrumb items={manyItems} truncate separator="slash" />Props: items (array of { label, href? }), truncate, linkVariant, separator ("slash" | "chevron").
Grid
CSS Grid layout with responsive column counts.
<Grid>
<div className="col-span-6">Left</div>
<div className="col-span-6">Right</div>
</Grid>Props:
| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| columns | number | 9 (XS), 12 (SM+) | Override responsive column count. |
| gap | string | Fluid token | Override gap (e.g. "space-4" or any CSS value). |
Container
Width-constrained content wrapper with responsive padding.
<Container>
<Grid>
<div className="col-span-12">Full-width inside container</div>
</Grid>
</Container>Props:
| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| className | string | — | Additional CSS classes. |
Theming
The Jiwambe plugin registers all design tokens as Tailwind utilities. You can override semantic color mappings by passing options to the plugin:
import jiwambePlugin from '@jiwambe/components/plugin';
export default {
content: [
'./src/**/*.{js,ts,jsx,tsx}',
'./node_modules/@jiwambe/components/dist/**/*.js',
],
plugins: [
jiwambePlugin({
colors: {
'text-primary': '#005748',
'link-primary': '#19AE8A',
'fill-action-primary': '#19AE8A',
'fill-action-primary-hover': '#109274',
},
}),
],
};Available Token Categories
| Category | Example Tailwind Class | Description |
| --- | --- | --- |
| Text colors | text-text-primary | Semantic text color |
| Link colors | text-link-primary | Semantic link color |
| Fill colors | bg-fill-action-primary | Background fills |
| Border colors | border-border-light | Border colors |
| Icon colors | text-icon-primary | Icon colors |
| Typography | text-title-lg | Composite typography styles |
| Spacing | p-space-4, gap-space-fluid-2-4 | Fixed and fluid spacing |
| Border radius | rounded-rad-md | Border radius |
| Box shadow | shadow-elevation-low | Elevation shadows |
CSS Custom Properties
All tokens are also available as CSS custom properties on :root:
.custom-element {
color: var(--color-text-primary);
padding: var(--space-4);
border-radius: var(--rad-md);
box-shadow: var(--elevation-low);
}Design Tokens Reference
Breakpoints
| Name | Value |
| --- | --- |
| xs | 0px |
| sm | 600px |
| md | 800px |
| lg | 940px |
| xl | 1440px |
Grid
| Breakpoint | Columns | Gap |
| --- | --- | --- |
| XS (0+) | 9 | space-2 (8px) |
| SM+ (600+) | 12 | space-fluid-2-4 (8–16px) |
Container
| Breakpoint | Behaviour |
| --- | --- |
| XS–LG | Horizontal padding space-fluid-4-9 (16–36px) |
| XL+ | Max width 83rem (1328px), centred, no padding |
Development
# Install dependencies
pnpm install
# Type-check
pnpm typecheck
# Build
pnpm buildThe build output lands in dist/ as ESM with TypeScript declarations and source maps.
