Foundation of the SkandaDX digital ecosystem: a headless design system (React + TypeScript) plus a theme contract that a future theme builder will target to produce branded component libraries.
- 54 unstyled, accessible primitives — behavior, keyboard interaction, and ARIA, zero visual opinion.
- 1,848 themeable SVG icon components (
@sdxui/icons) — 32×32 by default, colored viacurrentColorso--sdx-*tokens apply. - A theme is a plain JSON object.
themeToCss()/applyTheme()turn it into--sdx-*CSS custom properties. - A Storybook workspace ships every primitive with playgrounds, props tables, and live Brand + Mode toolbars (19 bundled brands × light/dark — 38 themes) proving that brands re-skin without touching component code.
┌─────────────────────────────────────────────────────────┐
│ Brand component libraries (future) │
│ e.g. @sdxui/brand-acme = primitives + acme theme │
├──────────────────┬───────────────────┬──────────────────┤
│ @sdxui/react │ @sdxui/icons │ @sdxui/tokens │
│ headless │ 1,848 themeable │ Theme contract, │
│ components: │ SVG icons — │ default theme, │
│ behavior + a11y +│ 32×32 default, │ createTheme(), │
│ data-* hooks, │ currentColor → │ themeToCss() / │
│ zero visual │ --sdx-* tokens │ applyTheme() → │
│ opinion │ apply │ --sdx-* CSS vars │
├──────────────────┴───────────────────┴──────────────────┤
│ Theme builder (future) — edits a `Theme` JSON object, │
│ previews live via applyTheme(), exports CSS/JSON │
└─────────────────────────────────────────────────────────┘
packages/react— unstyled, accessible components in the spirit of Radix UI / Base UI. Each component ships behavior, keyboard interaction, ARIA semantics, anddata-state/data-disabled/data-orientationattributes as CSS hooks. Every part supportsasChildto render onto your own element.packages/icons— 1,848 SVG icon components generated from Lucide (ISC, free for commercial use). Icons render 32×32 by default and draw withcurrentColor, so settingcolor: var(--sdx-color-primary-600)(or any theme token) themes them;size,color, andstrokeWidthprops override per-use. Each icon is published as its own module (@sdxui/icons/<name>) so client apps only ship the icons they import.packages/tokens— the theming seam. AThemeis a plain JSON object (color scales, semantic colors, typography, space, radii, shadows, motion).themeToCss()renders it to--sdx-*CSS variables;applyTheme()applies it at runtime for live preview;createTheme()derives brand themes from the default.apps/storybook— Storybook showcasing every primitive with interactive prop controls, auto-generated props tables, usage samples, and a toolbar brand switcher (all 22 bundled themes) proving that brands re-skin without touching component code.apps/docs— the SDX UI documentation portal: reference docs, live previews, props/methods/token tables, and consumer-integration guides for all three packages, built with React + TypeScript + Vite + plain CSS. Run withpnpm --filter @sdxui/docs dev.
sdxui/
├── apps/
│ ├── docs/ Documentation portal — see apps/docs/README.md
│ └── storybook/ Storybook workspace — playgrounds, docs, theme switcher
│ └── src/
│ ├── stories/ one *.stories.tsx per primitive
│ └── brand.css example brand stylesheet consuming --sdx-* tokens
├── packages/
│ ├── icons/ @sdxui/icons — 1,848 themeable SVG icon components
│ │ ├── scripts/ generate-icons.mjs — regenerates the set from lucide-static
│ │ └── src/ createIcon() factory + generated icons/ + iconNames manifest
│ ├── react/ @sdxui/react — headless components + shared utilities
│ │ └── src/
│ │ ├── utils/ Slot, Primitive, Portal, popper, focus-trap, controllable state, …
│ │ └── <name>/ one folder per component, colocated with its *.test.tsx
│ └── tokens/ @sdxui/tokens — theme contract, themes, CSS generation
│ └── src/
│ ├── themes/ defaultTheme, createTheme + one file per brand theme
│ └── … Theme types, themeToCss, applyTheme
├── pnpm-workspace.yaml
└── tsconfig.base.json
| Tier | Components |
|---|---|
| Basic | Button, Label, Separator, Avatar, Progress, Badge, Kbd, Card, Split Button |
| Feedback | Alert, Skeleton, Spinner |
| Layout | Aspect Ratio, Scroll Area, Resizable (panel group), Carousel |
| Form controls | Form, Text Field, Textarea, Number Field, Pin Input, Tags Input, File Upload, Checkbox, Radio Group, Switch, Toggle, Toggle Group, Rating, Slider (multi-thumb) |
| Disclosure | Collapsible, Accordion (single/multiple), Tabs |
| Navigation | Toolbar, Navigation Menu, Breadcrumb, Pagination |
| Data display | Table (sortable/selectable), Tree, Timeline |
| Date | Calendar, Date Picker |
| Overlays / advanced | Tooltip, Popover, Hover Card, Dialog, Alert Dialog, Dropdown Menu, Context Menu, Menubar, Select, Combobox, Command, Toast |
Every component is exported from @sdxui/react — see packages/react/src/index.ts for the full export list (parts + prop types).
@sdxui/icons ships the full Lucide set as theme-aware React components. Every icon is its own published module with a subpath export, so client apps bundle only the icons they import (works even without tree-shaking; the root barrel import also exists for bundler users):
import { SearchIcon } from '@sdxui/icons/search';
import { BellIcon } from '@sdxui/icons/bell';
import { CircleCheckIcon } from '@sdxui/icons/circle-check';
<SearchIcon /> // 32×32, inherits CSS color
<CircleCheckIcon style={{ color: 'var(--sdx-color-success-600)' }} /> // themed via tokens
<BellIcon size={20} strokeWidth={1.5} title="Notifications" /> // sized + accessibleBrowse and search the whole set in Storybook under Basics/Icons → Gallery. See packages/icons/README.md for props, custom icons via createIcon(), and regeneration instructions.
Requires pnpm (see packageManager in package.json) and Node 18+.
pnpm install # installs all workspaces
pnpm build # builds @sdxui/tokens + @sdxui/react
pnpm dev # starts Storybook at http://localhost:6006Run from the repo root (pnpm workspace filters fan these out to the right package):
| Command | Description |
|---|---|
pnpm install |
Install dependencies for every workspace |
pnpm dev |
Start Storybook dev server (localhost:6006) |
pnpm build |
Build @sdxui/tokens, @sdxui/react, and @sdxui/icons (tsup → ESM + CJS + .d.ts) |
pnpm build:storybook |
Build the static Storybook site |
pnpm typecheck |
tsc --noEmit across every workspace |
pnpm test |
Run the Vitest suite for every workspace |
pnpm test:coverage |
Run tests with V8 coverage (text + html + lcov) |
Per-package scripts (run inside packages/react or packages/tokens, or via pnpm --filter <name> run <script>):
dev(primitives only) —tsup --watchtest:watch— Vitest in watch mode
Primitives render no styles of their own — they render structure, behavior, and data-* attributes for you to hook into with your own CSS or brand layer.
import {
Dialog,
DialogTrigger,
DialogPortal,
DialogOverlay,
DialogContent,
DialogTitle,
DialogClose,
} from '@sdxui/react';
<Dialog>
<DialogTrigger className="button">Open</DialogTrigger>
<DialogPortal>
<DialogOverlay className="overlay" />
<DialogContent className="dialog">
<DialogTitle>Title</DialogTitle>
<DialogClose className="button">Close</DialogClose>
</DialogContent>
</DialogPortal>
</Dialog>;Style with CSS against class names and the exposed data attributes:
.dialog {
background: var(--sdx-color-semantic-background);
border-radius: var(--sdx-radius-xl);
}
.button[data-state='open'] {
background: var(--sdx-color-primary-700);
}Any part can delegate rendering to its child while keeping behavior — useful when composing with your own styled button, a router <Link>, etc.
<DialogTrigger asChild>
<MyBrandButton>Open</MyBrandButton>
</DialogTrigger>Select — composed of trigger, portal, viewport, and items; ships a hidden native <select> when given name so it participates in form submission:
import {
Select,
SelectTrigger,
SelectValue,
SelectIcon,
SelectPortal,
SelectContent,
SelectViewport,
SelectItem,
SelectItemText,
SelectItemIndicator,
} from '@sdxui/react';
<Select defaultValue="react" name="framework">
<SelectTrigger className="select-trigger">
<SelectValue placeholder="Pick a framework" />
<SelectIcon />
</SelectTrigger>
<SelectPortal>
<SelectContent className="menu">
<SelectViewport>
{['react', 'vue', 'svelte', 'solid'].map((fw) => (
<SelectItem key={fw} value={fw} className="menu-item">
<SelectItemIndicator>✓</SelectItemIndicator>
<SelectItemText>{fw}</SelectItemText>
</SelectItem>
))}
</SelectViewport>
</SelectContent>
</SelectPortal>
</Select>;Toast — self-closing, uncontrolled by default; mount one Toast per notification and drop it from state on onOpenChange(false):
import { Toast, ToastTitle, ToastDescription, ToastClose } from '@sdxui/react';
<Toast duration={4000} type="background" onOpenChange={(open) => !open && dismiss(id)}>
<ToastTitle>Saved</ToastTitle>
<ToastDescription>Your changes have been saved.</ToastDescription>
<ToastClose aria-label="Dismiss">✕</ToastClose>
</Toast>;Accordion — single or multiple expansion via a discriminated type prop, controlled or uncontrolled:
import { Accordion, AccordionItem, AccordionHeader, AccordionTrigger, AccordionContent } from '@sdxui/react';
<Accordion type="single" collapsible defaultValue="item-1">
<AccordionItem value="item-1">
<AccordionHeader>
<AccordionTrigger>Section one</AccordionTrigger>
</AccordionHeader>
<AccordionContent>Content for section one.</AccordionContent>
</AccordionItem>
</Accordion>;More runnable examples for every component live in apps/storybook/src/stories — run pnpm dev and open the matching story for props, controls, and edge cases.
A brand theme is data, not code:
import { createTheme, themeToCss, applyTheme } from '@sdxui/tokens';
const acme = createTheme('acme', {
colors: { primary: {/* 50–950 scale */}, semantic: { focusRing: '#e11d48' } },
radii: { md: '0.25rem' },
});
applyTheme(acme); // runtime (live preview in the theme builder)
const css = themeToCss(acme); // build time → ship as a .css fileThe generated variables are namespaced (--sdx-color-primary-600, --sdx-space-4,
--sdx-radius-md, --sdx-shadow-overlay, --sdx-motion-duration-fast, …) and scoped to
[data-sdx-theme="<name>"], so multiple brands can coexist on one page. applyTheme also
stamps data-sdx-mode="light|dark" (from the theme's mode field), so mode-conditional
CSS can target the root independently of the brand.
The package also ships ready-made brand themes (skandaLight, emeraldBrand, nordDark,
draculaDark, …, plus a themes array) — each a fully-resolved Theme object in its own
file under packages/tokens/src/themes/.
Brand and color mode are independent theme dimensions. Every bundled brand pairs a complete
light theme and dark theme in the brands registry:
import { applyTheme, getBrand, brands, defaultBrand } from '@sdxui/tokens';
const brand = getBrand('nord') ?? defaultBrand; // preference fallback
const mode = matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
applyTheme(brand.themes[mode]);Persist the brand preference and the color-mode preference ('light' | 'dark' | 'system')
separately — resolve 'system' at runtime and never store the resolved value, so the app
keeps following the OS while that preference is active. The docs app
(apps/docs/src/utils/theme.ts) is the reference
implementation, including a build-time static stylesheet of every brand-and-mode theme and
an inline no-flash script (see the sdx-theme-css plugin in
apps/docs/vite.config.ts).
Defined in packages/tokens/src/utils/theme.ts:
| Section | Contents |
|---|---|
colors |
primary / neutral / success / warning / danger — each a 50–950 ColorScale — plus semantic (background, surface, text, border, focusRing, …) |
typography |
fontFamily, fontFamilyMono, fontSize (xs–3xl), fontWeight, lineHeight |
space |
Scale from 1 to 16 |
radii |
sm / md / lg / xl / full |
shadows |
sm / md / lg / overlay |
motion |
durationFast / durationNormal / durationSlow / easing |
createTheme(name, override) deep-merges a ThemeOverride (any subset of the shape above) onto defaultTheme, so brand themes only need to specify what differs.
@sdxui/tokens ships 19 brands — 38 bundled themes (see packages/tokens/README.md for the full list) as stand-ins for what a future theme builder will produce. apps/storybook/.storybook/preview.tsx registers every one of them and switches live via the Storybook toolbar's Brand and Mode controls:
skanda (default) · emerald · midnight · graphite (shadcn/zinc-inspired) · material (Material 3) · ant (Ant Design) · fluent (Microsoft Fluent 2) · chakra (Chakra UI) · nord · dracula · and 9 more (Catppuccin, Solarized, Gruvbox, Rosé Pine, Everforest, One, Primer, Cupcake, Synthwave) — each in light and dark.
- Headless first: primitives render structure, keyboard behavior, and ARIA — never CSS or visual defaults. All styling hooks are
className(orasChild) plus data attributes; the same component looks identical to the browser whether it's unstyled or fully rebranded. - Controlled + uncontrolled: every stateful component accepts
value/onValueChange(controlled) ordefaultValue(uncontrolled) — same pattern foropen,checked,pressed, driven by the shareduseControllableStatehook. - Data attributes over props for styling:
data-state="open|closed|checked|active|…",data-disabled,data-side/data-alignon positioned content — style against these instead of reaching into internals. - Forms: Checkbox, Switch, Radio Group, Select, and Slider render a hidden native input when given a
name, so they participate in plain<form>submission and native validation without extra glue. - Positioning: Tooltip, Popover, Hover Card, Dropdown Menu, Context Menu, Menubar, and Select share one popper layer (
@floating-ui/react-dom) withside,align,sideOffset, and collision handling. - Composition over configuration: complex components (Dialog, Select, Accordion, Toast, …) are assembled from small parts (
Trigger,Portal,Content,Item, …) rather than one prop-heavy component — compose only what you need. asChildeverywhere: any part can delegate its DOM element to a child via the sharedSlotprimitive, so primitives compose cleanly with your own styled components or a router's<Link>.forceMountis available on conditional parts (content that mounts/unmounts) for animation libraries that need the node kept in the DOM during exit transitions.- Theme as data: a
Themeis a plain, JSON-serializable object — never TS/JS logic — because it must round-trip through a future visual theme builder, be diffed, and be exported as CSS or JSON. - Every component is tested: each
component.tsxis colocated with acomponent.test.tsx(Vitest + Testing Library); tokens and core utilities carry the same coverage discipline.
See CONTRIBUTING.md.
- Theme builder app (edits
ThemeJSON, previews viaapplyTheme, exports CSS + JSON). - Branded component packages composed from primitives + a theme (e.g.
@sdxui/brand-acme). - Exit-animation presence handling (currently only mount animations are first-class).
- Dropdown Menu / Context Menu / Menubar submenus (nested menu items) — typeahead is already implemented (
useRovingFocus'stypeaheadoption). - Date range selection for Date Picker (currently single-date only).
- Visual regression testing in CI; automated per-story
axe-corechecks are already wired via Storybook's@storybook/addon-a11y. @sdxui/react,@sdxui/tokens, and@sdxui/iconsalready auto-publish to npm on version bumps tomain(.github/workflows/publish.yml) — next step is Changesets-driven independent versioning + changelogs instead of manual lockstep version bumps.