When to apply
Use this skill when the task involves any of:
- Installing or upgrading
@sisyphos-ui/react,@sisyphos-ui/vue, or@sisyphos-ui/angular. - Theming colors, radii, spacing, or typography at runtime via
applyTheme(). - Building forms, modals, command palettes, menus, toasts, tables, or overlay UI in React, Vue, or Angular.
- Auditing accessibility for keyboard support, focus management, or ARIA correctness.
- Migrating from Material UI, Chakra UI, shadcn/ui, Mantine, Ant Design, or HeadlessUI.
- Adding dark mode, custom theme, or per-section overrides to a Sisyphos-powered app.
Package architecture
Sisyphos UI is published as a layered npm family under the @sisyphos-ui/* scope. There is no single bundle — each layer can be installed independently.
Three framework umbrellas — React 18+, Vue 3+, Angular 18+. Each ships every component plus the bundled stylesheet. Pick the one for your stack; the design contract is identical across all three.
@sisyphos-ui/react@sisyphos-ui/vue@sisyphos-ui/angularDesign tokens, applyTheme() runtime engine, dark-mode helpers. Framework-agnostic CSS variables under --sisyphos-* — every framework binding builds on this layer.
@sisyphos-ui/corePick one shape per app. Mixing the umbrella with per-component packages duplicates CSS variables and the cascade order is no longer predictable.
Umbrella
recommended- When
- Default for product apps.
- Pros
- One install, one import, fastest iteration.
- Trade
- A few KB of unused components (still tree-shaken).
$ pnpm add @sisyphos-ui/reactPer-component
bundle-sensitive- When
- Landing pages, embeds, marketing sites, email previews.
- Pros
- Minimum footprint per component.
- Trade
- More install lines and dependency management.
$ pnpm add @sisyphos-ui/reactInstallation
The umbrella is the recommended default. Pick a framework recipe below and copy it verbatim — the order of operations matters.
Next.js (app router)
pnpm add @sisyphos-ui/reactimport "@sisyphos-ui/react/styles.css";
import { Toaster } from "@sisyphos-ui/react";
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en">
<body>
{children}
<Toaster position="bottom-right" />
</body>
</html>
);
}React + Vite
import "@sisyphos-ui/react/styles.css";
import { Toaster } from "@sisyphos-ui/react";
createRoot(document.getElementById("root")!).render(
<StrictMode>
<App />
<Toaster position="bottom-right" />
</StrictMode>,
);Per-component
Always pair component packages with @sisyphos-ui/core. Each ships its own stylesheet at @sisyphos-ui/<name>/styles.css — import them all, or use the umbrella's bundled stylesheet instead.
pnpm add @sisyphos-ui/reactimport "@sisyphos-ui/core/styles.css";
import "@sisyphos-ui/react/styles.css";The Core package — @sisyphos-ui/core
The foundation that every other package depends on. Three exports you'll touch repeatedly:
import {
applyTheme, // (tokens: PartialTokens) => void
setMode, // ("light" | "dark" | "system") => void
getTokens, // () => Tokens — returns the resolved current tokens
defaultTokens // Tokens — built-in defaults
} from "@sisyphos-ui/core";Token shape (excerpt) — every key is also exposed as a CSS variable prefixed --sisyphos-*:
type Tokens = {
colors: {
primary: string; primaryLight: string; primaryDark: string;
success: string; error: string; warning: string; info: string;
surface: string; surfaceMuted: string; border: string;
text: string; textMuted: string; textSubtle: string;
};
spacing: { xs; sm; md; lg; xl; "2xl" };
radius: { xs; sm; md; lg; xl; full };
typography: { fontSans; fontDisplay; fontMono };
motion: { durationFast; durationNormal; easing };
};Theming with applyTheme()
Override any subset of tokens at runtime — the change cascades through every Sisyphos component immediately, no rebuild.
import { applyTheme } from "@sisyphos-ui/core";
applyTheme({
colors: {
primary: "#ff7022",
success: "#22c55e",
error: "#fb3748",
},
spacing: { md: 16, lg: 24 },
radius: { md: 12, full: 9999 },
});- Call
applyTheme()once at app boot, before the first render. Calling it inside a render path causes a layout flash. - For partial overrides, only specify the keys you want to change — everything else falls back to defaults.
- For per-section overrides, scope CSS variables on a parent element instead of calling
applyTheme()again:
<div style={{ "--sisyphos-color-primary": "#0ea5e9" } as React.CSSProperties}>
<Button>Section-scoped blue button</Button>
</div>Dark mode
Sisyphos UI ships a dark variant of every variable. Toggle it with the dark class on <html> — managed by setMode() from @sisyphos-ui/core.
import { setMode } from "@sisyphos-ui/core";
// Read the user's preference, fall back to system
setMode((localStorage.getItem("theme") as "light" | "dark" | null) ?? "system");For Next.js / SSR, add the anti-flash inline script in the <head> so the class is set before hydration:
<script>
(function () {
var s = localStorage.getItem("theme");
var prefersDark = matchMedia("(prefers-color-scheme: dark)").matches;
var dark = s === "dark" || (s !== "light" && prefersDark);
if (dark) document.documentElement.classList.add("dark");
})();
</script>Do not call applyTheme() separately for light and dark. The stylesheet already swaps surface, brand, and text tokens under the .dark selector.
Component reference
35 components, four families. Pick the one that matches the user intent — do not compose primitives when one already exists.
Inputs
12 packages| Package | Component | Purpose |
|---|---|---|
@sisyphos-ui/react | Button | 4 variants × 5 sizes, loading, optional split-button, polymorphic via href. |
@sisyphos-ui/react | Input | Text input with adornments, validation state, helper text. |
@sisyphos-ui/textarea | Textarea | Auto-resize multiline, char count. |
@sisyphos-ui/checkbox | Checkbox | Tri-state — checked, unchecked, indeterminate. |
@sisyphos-ui/switch | Switch | On/off toggle. |
@sisyphos-ui/radio | Radio + RadioGroup | Single-choice with roving tabindex. |
@sisyphos-ui/select | Select | Dropdown with search, multi-select, virtualization-ready. |
@sisyphos-ui/tree-select | TreeSelect | Hierarchical picker with cascading. |
@sisyphos-ui/number-input | NumberInput | Stepper, precision, locale-aware. |
@sisyphos-ui/slider | Slider | Single or dual-thumb, marks, snapping. |
@sisyphos-ui/datepicker | DatePicker | Calendar, range, disabled dates. |
@sisyphos-ui/file-upload | FileUpload | Drag-and-drop, preview, progress. |
Display
13 packages| Package | Component | Purpose |
|---|---|---|
@sisyphos-ui/chip | Chip | Tags, filters, removable badges. |
@sisyphos-ui/avatar | Avatar + AvatarGroup | Image, initials, status dot, grouping. |
@sisyphos-ui/spinner | Spinner | Reduced-motion aware loader. |
@sisyphos-ui/skeleton | Skeleton | Content placeholder with shimmer. |
@sisyphos-ui/empty-state | EmptyState | Standardized 'no results' UI. |
@sisyphos-ui/alert | Alert | Inline message with semantic colors. |
@sisyphos-ui/breadcrumb | Breadcrumb | Hierarchical navigation trail. |
@sisyphos-ui/card | Card | Compound surface — Header / Body / Footer. |
@sisyphos-ui/accordion | Accordion | Collapsible panels, single or multi-expand. |
@sisyphos-ui/tabs | Tabs | Compound, roving tabindex, h/v orientation. |
@sisyphos-ui/table | Table | Sorting, selection, sticky headers, density. |
@sisyphos-ui/carousel | Carousel | Touch, autoplay, indicators. |
@sisyphos-ui/kbd | Kbd | Platform-aware keyboard hint — shortcut="cmd+k". |
Overlay
7 packages| Package | Component | Purpose |
|---|---|---|
@sisyphos-ui/tooltip | Tooltip | 12 placements, aria-describedby. |
@sisyphos-ui/popover | Popover | role="dialog" floating panel. |
@sisyphos-ui/dropdown-menu | DropdownMenu | role="menu" with submenu, type-ahead. |
@sisyphos-ui/react | Dialog | Modal with focus trap, scroll lock. |
@sisyphos-ui/react | toast + Toaster | Imperative API, polite vs assertive distinction. |
@sisyphos-ui/context-menu | ContextMenu | Right-click menu, viewport-clamped. |
@sisyphos-ui/command | Command | Filterable command palette / combobox. |
Foundation
3 packages| Package | Component | Purpose |
|---|---|---|
@sisyphos-ui/core | applyTheme + tokens | Theme engine, mode toggle, design tokens. |
@sisyphos-ui/react | Portal | Renders children into the document body. |
@sisyphos-ui/form-control | FormControl | Label / helper / error wrapper for any input. |
Form patterns
Always wrap form inputs in FormControl. It wires up label, helper text, and error messages via aria-describedby / aria-invalid automatically. There is no <Form> provider — render FormControl per field.
<FormControl
label="Email"
helperText="We'll never share it."
errorText={errors.email}
required
>
<Input
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
/>
</FormControl>- Pass
errorText(truthy) to flip the input into the error state — do not toggle a class manually. - For checkbox / radio groups, wrap the whole group in one
FormControl, not each item.
Toast patterns
The toast API is imperative. There is no React context. Mount <Toaster /> once at the root, then call from anywhere.
import { toast, Toaster } from "@sisyphos-ui/react";
<Toaster position="bottom-right" duration={4000} />
toast.success("Saved");
toast.error("Network error");
await toast.promise(saveUser(data), {
loading: "Saving…",
success: "Saved.",
error: (err) => err.message,
});
const id = toast.success("Will dismiss in 1s");
setTimeout(() => toast.dismiss(id), 1000);toast.success / .info use role="status" (polite). toast.error uses role="alert"(assertive). Match the urgency — don't promote routine confirmations to error.
Dialog patterns
Compound API. Focus trap, scroll lock, Esc-to-close, and focus restoration are automatic.
<Dialog open={open} onOpenChange={setOpen}>
<Dialog.Trigger asChild>
<Button>Open</Button>
</Dialog.Trigger>
<Dialog.Content size="md">
<Dialog.Header>
<Dialog.Title>Delete project?</Dialog.Title>
<Dialog.Description>This cannot be undone.</Dialog.Description>
</Dialog.Header>
<Dialog.Body>
All members will lose access immediately.
</Dialog.Body>
<Dialog.Footer>
<Dialog.Close asChild>
<Button variant="outlined">Cancel</Button>
</Dialog.Close>
<Button color="error" onClick={confirm}>Delete</Button>
</Dialog.Footer>
</Dialog.Content>
</Dialog>Drive open state through open + onOpenChange — bypassing them disables the focus trap. For non-modal floating panels (calendars, dropdowns), use Popover instead.
Command palette
Keyboard-first filterable menu. Combobox + listbox semantics. Use it for ⌘K-style global search.
<Command>
<Command.Input placeholder="Search…" />
<Command.List>
<Command.Empty>No results.</Command.Empty>
<Command.Group heading="Actions">
<Command.Item value="new file" onSelect={create}>
New file
</Command.Item>
<Command.Item value="open settings" onSelect={openSettings}>
Open settings
</Command.Item>
</Command.Group>
</Command.List>
</Command>Filtering is case-insensitive substring matching against value. Render labels however you like — the filter only looks at value.
DropdownMenu for app actions, ContextMenu for right-click, Tooltip for hover hints, Popover for non-modal floating panels.
DropdownMenu — type-ahead, submenu
<DropdownMenu>
<DropdownMenu.Trigger asChild>
<Button variant="outlined">More</Button>
</DropdownMenu.Trigger>
<DropdownMenu.Content align="end">
<DropdownMenu.Item onSelect={share}>Share</DropdownMenu.Item>
<DropdownMenu.Item onSelect={archive}>Archive</DropdownMenu.Item>
<DropdownMenu.Separator />
<DropdownMenu.Sub>
<DropdownMenu.SubTrigger>Move to…</DropdownMenu.SubTrigger>
<DropdownMenu.SubContent>
<DropdownMenu.Item>Inbox</DropdownMenu.Item>
<DropdownMenu.Item>Archive</DropdownMenu.Item>
</DropdownMenu.SubContent>
</DropdownMenu.Sub>
</DropdownMenu.Content>
</DropdownMenu>ContextMenu — array-based items
<ContextMenu
items={[
{ type: "item", label: "Rename", onSelect: rename },
{ type: "item", label: "Duplicate", onSelect: duplicate },
{ type: "separator" },
{ type: "item", label: "Delete", danger: true, onSelect: del },
]}
>
<FileRow file={file} />
</ContextMenu>Tooltip & Popover
<Tooltip content="Save changes" placement="top">
<Button>Save</Button>
</Tooltip>
<Popover>
<Popover.Trigger asChild>
<Button variant="outlined">Filter</Button>
</Popover.Trigger>
<Popover.Content>
{/* arbitrary UI here */}
</Popover.Content>
</Popover>Tabs & Table
Tabs — automatic vs manual activation
<Tabs defaultValue="overview" orientation="horizontal">
<Tabs.List>
<Tabs.Trigger value="overview">Overview</Tabs.Trigger>
<Tabs.Trigger value="settings">Settings</Tabs.Trigger>
</Tabs.List>
<Tabs.Content value="overview">…</Tabs.Content>
<Tabs.Content value="settings">…</Tabs.Content>
</Tabs>Activation defaults to automatic (focus changes activate). Pass activationMode="manual" if Tab content is heavy and you want explicit Enter/Space activation.
Table — sortable headers, selection, density
<Table density="comfortable" striped>
<Table.Header sticky>
<Table.Row>
<Table.Head sortable sortDirection={sort} onSort={setSort}>
Name
</Table.Head>
<Table.Head>Email</Table.Head>
</Table.Row>
</Table.Header>
<Table.Body>
{users.map((u) => (
<Table.Row key={u.id}>
<Table.Cell>{u.name}</Table.Cell>
<Table.Cell>{u.email}</Table.Cell>
</Table.Row>
))}
</Table.Body>
</Table>TypeScript
- Every prop is typed; the codebase has zero
any. - Polymorphic primitives (
Button,Card) acceptasorhref— TypeScript narrows the rest of the props accordingly. - Discriminated unions are used heavily —
DropdownMenuItemvsDropdownMenuSeparator. Exhaustiveswitchworks. - Re-export the type when extending; don't redeclare:
import type { ButtonProps } from "@sisyphos-ui/react";
type MyButtonProps = ButtonProps & { trackingId: string };Accessibility guarantees
Every interactive component already ships with:
- Keyboard support per the WAI-ARIA Authoring Practices.
- Focus management — trap on dialogs, restore on close, roving tabindex on radio / tabs / menus.
prefers-reduced-motionhonored on Spinner, Skeleton, Carousel.- Correct semantic roles:
menuitem,option,dialog,alertvsstatus,combobox+listbox. - Color contrast: defaults pass WCAG AA at the documented theme.
You should not:
- Re-implement keyboard handling on top of these primitives.
- Add
tabIndex,role, oraria-*props that duplicate what the component already sets. - Wrap a
Buttonin another<button>— usehrefor compose directly.
Anti-patterns
- ✕
<ThemeProvider value={...}>Use applyTheme(). There is no ThemeProvider.
- ✕
import "@sisyphos-ui/react/styles.css" alongside the umbrellaImport "@sisyphos-ui/react/styles.css" once at the app root.
- ✕
Mixing umbrella + per-component packagesPick one shape per app. Mixing duplicates CSS variables.
- ✕
Driving Dialog open state outside its propsUse the component's open / onOpenChange — focus trap depends on it.
- ✕
applyTheme() inside a renderCall it once at boot. Inside render = layout flash.
- ✕
Tailwind / emotion to recolor a Sisyphos componentOverride the underlying --sisyphos-* CSS variable.
- ✕
role="button" on a <div>Use <Button>. It already has the right semantics.
- ✕
Manually toggling the dark classUse setMode() from @sisyphos-ui/core.
Migration cheatsheet
Side-by-side mappings for the most common migrations. The compound APIs translate one-to-one — you'll mostly be deleting providers and renaming a few props.
From Material UI (MUI)
| Material UI (MUI) | Sisyphos UI |
|---|---|
<ThemeProvider theme={createTheme(...)}> | applyTheme({ colors, spacing, radius }) at boot |
sx={{ p: 2 }} | CSS variables / utility class on the wrapper |
<Dialog open onClose> | <Dialog open onOpenChange> compound API |
enqueueSnackbar (notistack) | toast.success / .error / .promise |
useMediaQuery | Native matchMedia — no equivalent needed |
From shadcn/ui
| shadcn/ui | Sisyphos UI |
|---|---|
Copy components into your repo | pnpm add @sisyphos-ui/react (versioned package) |
cn util / tailwind-merge | Not needed — components self-style |
<Toaster /> from sonner | <Toaster /> from @sisyphos-ui/react (similar toast API) |
<Dialog> (Radix) | <Dialog> — same compound shape |
From Chakra UI
| Chakra UI | Sisyphos UI |
|---|---|
<ChakraProvider theme={...}> | applyTheme(...) |
useColorMode | setMode() + dark class |
<HStack> / <VStack> | Plain flex containers |
useToast() | import { toast } (imperative) |