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.
- Adding dark mode, custom theme, or per-section overrides to a Sisyphos-powered app.
Package architecture
Sisyphos UI v1 ships four packages under the @sisyphos-ui/* scope: three framework umbrellas that each export all 33 components from one barrel, on top of a shared core foundation. Theme functions are exported only from @sisyphos-ui/core.
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 binding per app. Never mix the legacy v0.5 per-component packages with a v1 umbrella — duplicate CSS variables collide and the cascade order is no longer predictable.
v1 framework umbrella
recommended- When
- All new projects — React, Vue 3, or Angular 18.
- Pros
- One install, one import, one stylesheet; ESM tree-shaking keeps bundles lean.
- Trade
- None — this is the only supported v1 layout.
$ pnpm add @sisyphos-ui/reactv0.5 per-component
deprecated- When
- Legacy React apps not yet migrated.
- Pros
- Frozen at 0.5.x on npm for back-compat.
- Trade
- React-only, no updates; migrate by swapping @sisyphos-ui/ui → @sisyphos-ui/react.
$ pnpm add @sisyphos-ui/react # migrateInstallation
Pick the framework recipe below and copy it verbatim — every binding ships its own bundled stylesheet, imported once at the app root.
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>,
);Vue 3
pnpm add @sisyphos-ui/vue<script setup lang="ts">
import "@sisyphos-ui/vue/styles.css";
import { Button, Toaster, toast } from "@sisyphos-ui/vue";
</script>
<template>
<Toaster position="bottom-right" />
<Button @click="toast.success('Ready to ship')">Click me</Button>
</template>Angular 18 (standalone)
Components are standalone (no NgModule) with sui-* selectors. Add the stylesheet to styles.css or the angular.json "styles" array.
pnpm add @sisyphos-ui/angular// styles.css: @import "@sisyphos-ui/angular/styles.css";
import { Component } from "@angular/core";
import { Button, Toaster, toast } from "@sisyphos-ui/angular";
@Component({
selector: "app-root",
standalone: true,
imports: [Button, Toaster],
template: `
<sui-toaster position="bottom-right" />
<sui-button (buttonClick)="hi()">Click me</sui-button>
`,
})
export class AppComponent {
hi() { toast.success("Ready to ship"); }
}The Core package — @sisyphos-ui/core
The foundation every binding depends on. Theme functions are exported only from @sisyphos-ui/core — not re-exported by the framework umbrellas:
import {
applyTheme, // (config: ThemeConfig) => void — writes --sisyphos-* vars to :root
setThemeMode, // ("light" | "dark") => void — toggles the html class
getThemeMode, // () => "light" | "dark"
toggleThemeMode, // () => void — flips the current mode
themes, // presets: themes.default | blue | purple | green
createTheme, // identity helper that preserves literal types
mergeThemes, // (...configs) => ThemeConfig — later wins
generateThemeCSS, // (config) => ":root { … }" string for SSR injection
} from "@sisyphos-ui/core";ThemeConfig shape (all keys optional) — every value is written as a CSS variable prefixed --sisyphos-*:
interface ThemeConfig {
colors?: {
// string or shade object per semantic color
primary?: string | { main?; light?; lighter?; dark?; darker?; contained? };
success?; error?; warning?; info?;
};
neutral?: { main?; lighter?; light?; dark?; darker?; border? };
spacing?: { xxs?; xs?; s?; md?; lg?; xl?; "2xl"?; "3xl"? };
typography?: { fontFamily?; sizes?; weights?; lineHeights? };
borderRadius?: { xxs?; xs?; s?; md?; lg?; xl?; full? };
opacity?: { xs?; s?; md?; lg? };
duration?: { s?; m? };
zIndex?: { tooltip?; pickers?; overlay? };
}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: { main: "#0284c7", light: "#38bdf8", dark: "#0369a1" },
success: "#16a34a", // plain string sets the main shade
},
spacing: { md: 16, lg: 24 }, // numbers become px
borderRadius: { md: 12, full: 9999 },
typography: { fontFamily: "Inter, sans-serif" },
});- 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
Dark mode is class-based: setThemeMode("dark") puts sisyphos-theme-dark on <html>. Only neutral/surface tokens change — semantic brand colors stay. There is no "system" mode value; resolve the OS preference yourself.
import { setThemeMode, toggleThemeMode, getThemeMode } from "@sisyphos-ui/core";
// Restore the stored preference, fall back to the OS setting
const stored = localStorage.getItem("theme") as "light" | "dark" | null;
const prefersDark = matchMedia("(prefers-color-scheme: dark)").matches;
setThemeMode(stored ?? (prefersDark ? "dark" : "light"));
// Wire your toggle button
toggleThemeMode();
localStorage.setItem("theme", getThemeMode());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("sisyphos-theme-dark");
})();
</script>Do not call applyTheme() separately for light and dark. The stylesheet already swaps the neutral tokens under the .sisyphos-theme-dark selector — and do not toggle the class by hand at runtime; use setThemeMode() / toggleThemeMode().
Component reference
33 components with identical names and behavior across React, Vue, and Angular (Angular templates use sui-* selectors). 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 state, optional dropdownItems split-button, renders <a> via href. |
@sisyphos-ui/react | Input | Text field with label, error/errorMessage, icons, password toggle, masks, char count. |
@sisyphos-ui/react | Textarea | Multiline with optional auto-resize, character counter, error state. |
@sisyphos-ui/react | Checkbox | Tri-state via indeterminate — activating promotes to checked. |
@sisyphos-ui/react | Switch | role="switch" toggle; always controlled (checked + onChange). |
@sisyphos-ui/react | Radio + RadioGroup | Single choice; compose <Radio> children or pass an options array. |
@sisyphos-ui/react | Select | Single/multi combobox with search, clearable, creatable, async load-more. |
@sisyphos-ui/react | TreeSelect | Hierarchical multi-select with search, cascade selection, mixed parent state. |
@sisyphos-ui/react | NumberInput | Locale-aware formatting, stepper buttons, prefix/suffix, min/max/step. |
@sisyphos-ui/react | Slider | Single or range dual-thumb; Arrow/Page/Home/End keyboard support. |
@sisyphos-ui/react | DatePicker | Single date or isRange, optional time, min/max, localized names. |
@sisyphos-ui/react | FileUpload | Drag-and-drop, accept/maxSize/maxFiles validation, per-file progress. |
Display
13 packages| Package | Component | Purpose |
|---|---|---|
@sisyphos-ui/react | Chip | Tags and filters with optional icon/avatar and delete button. |
@sisyphos-ui/react | Avatar + AvatarGroup | Image with fallback to initials or placeholder; grouping. |
@sisyphos-ui/react | Spinner | role="status" loading indicator with screen-reader label. |
@sisyphos-ui/react | Skeleton | Placeholder — rectangular/circular/text, shimmer/pulse/none. |
@sisyphos-ui/react | EmptyState | Standardized zero-data view: icon, title, description, actions. |
@sisyphos-ui/react | Alert | Persistent inline callout with semantic colors. |
@sisyphos-ui/react | Breadcrumb | Nav trail with maxItems middle-collapse, aria-current. |
@sisyphos-ui/react | Card | Compound surface — Header / Body / Footer, interactive mode. |
@sisyphos-ui/react | Accordion | Collapsible panels, single or multi-expand. |
@sisyphos-ui/react | Tabs | Compound List / Trigger / Panel, roving tabindex, h/v orientation. |
@sisyphos-ui/react | Table | Data-driven (data + columns): sorting, selection, expansion, pagination, sticky header. |
@sisyphos-ui/react | Carousel | Autoplay (paused on hover/focus), arrows, dot indicators. |
@sisyphos-ui/react | Kbd | Keyboard hint — shortcut="cmd+k" or keys={["cmd","k"]}. |
Overlay
7 packages| Package | Component | Purpose |
|---|---|---|
@sisyphos-ui/react | Tooltip | 8 placements with auto-flip, wired via aria-describedby. |
@sisyphos-ui/react | Popover | content prop + trigger child; role="dialog" panel; click/hover/manual. |
@sisyphos-ui/react | DropdownMenu | role="menu" action menu driven by an items array. |
@sisyphos-ui/react | Dialog | Modal with focus trap, scroll lock; compound Header/Body/Footer/Close. |
@sisyphos-ui/react | toast + Toaster | Imperative API, polite vs assertive distinction. |
@sisyphos-ui/react | ContextMenu | Right-click menu at the cursor, viewport-clamped; same items shape as DropdownMenu. |
@sisyphos-ui/react | Command | Filterable command palette — combobox + listbox semantics. |
Foundation
3 packages| Package | Component | Purpose |
|---|---|---|
@sisyphos-ui/core | applyTheme + tokens | Theme engine (applyTheme, setThemeMode, toggleThemeMode), design tokens. |
@sisyphos-ui/react | Portal | Renders children into the document body; plus useFocusTrap / useScrollLock. |
@sisyphos-ui/react | FormControl | Compound field wrapper — FormLabel / FormHelperText / FormErrorText children. |
Form patterns
FormControl is a compound wrapper — labels, helper text, and error text are children, not props. It auto-generates the field id and wires aria-describedby (helper vs error) for you. There is no <Form> provider — render FormControl per field.
import { FormControl, FormLabel, FormHelperText, FormErrorText, Input } from "@sisyphos-ui/react";
<FormControl required error={!!errors.email} fullWidth>
<FormLabel>Email</FormLabel>
<Input type="email" value={email} onChange={(e) => setEmail(e.target.value)} />
<FormHelperText>We'll never share it.</FormHelperText>
<FormErrorText>{errors.email}</FormErrorText>
</FormControl>FormControlprops:id?,disabled,required,readOnly,error(boolean),fullWidth— there is nolabel/helperText/errorTextprop.FormErrorTextrenders only whileerroris true on theFormControl.- For quick single fields,
Input,Select, andTextareaalso accept their ownlabel,error, anderrorMessageprops — pick one mechanism per field, not both. - 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"; // or /vue, /angular
// Toaster props: position, max, gap — duration is per-toast
<Toaster position="bottom-right" max={5} />
toast.success("Saved");
toast.error("Network error", { description: "Retrying in 5s…" });
toast.warning("Storage almost full");
toast.loading("Uploading…"); // persists until updated
toast.success("Done", { duration: Infinity }); // per-toast option, default 4000
await toast.promise(saveUser(data), {
loading: "Saving…",
success: "Saved.",
error: (err) => (err as Error).message,
});
const id = toast.success("Will dismiss in 1s");
setTimeout(() => toast.dismiss(id), 1000);
toast.clear(); // remove alltoast() / .success / .warning / .info / .loading 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, backdrop-close, and focus restoration are automatic. There is no Dialog.Trigger and no Dialog.Content — the Dialog root is the panel; open it from your own button and put size on the root.
<Button onClick={() => setOpen(true)}>Delete…</Button>
<Dialog open={open} onOpenChange={setOpen} 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>Cancel</Dialog.Close>
<Button color="error" onClick={confirm}>Delete</Button>
</Dialog.Footer>
</Dialog>Root props: open, onOpenChange, size (sm | md | lg | xl | full), closeOnBackdropClick, closeOnEscape, backdrop, showCloseButton, initialFocus. Vue uses v-model:open with DialogHeader/DialogBody/… imports; Angular uses <sui-dialog [(open)]>. For non-modal floating panels (calendars, filters), use Popover instead.
Command palette
Keyboard-first filterable menu. Combobox + listbox semantics. Use it for ⌘K-style global search.
<Command onSelect={(value) => run(value)}>
<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.Separator />
</Command.List>
</Command>Filtering is case-insensitive substring matching against each item's value (falling back to its text). The root also supports controlled search via value / onValueChange.
DropdownMenu and ContextMenu are data-driven: an items array wrapping a single trigger child — there are no Trigger/Content/Sub subcomponents and no nested submenus. Tooltip for hover hints, Popover for non-modal floating panels.
DropdownMenu — items array, WAI-ARIA menu-button pattern
<DropdownMenu
placement="bottom-start"
items={[
{ label: "Share", icon: <ShareIcon />, onSelect: share },
{ label: "Archive", shortcut: "⌘E", onSelect: archive },
{ type: "separator" },
{ type: "label", label: "Danger zone" },
{ label: "Delete", destructive: true, onSelect: del },
]}
>
<Button variant="outlined">More</Button>
</DropdownMenu>ContextMenu — same items shape, anchored at the cursor
<ContextMenu
items={[
{ label: "Rename", onSelect: rename },
{ label: "Duplicate", onSelect: duplicate },
{ type: "separator" },
{ label: "Delete", destructive: true, onSelect: del },
]}
>
<FileRow file={file} />
</ContextMenu>Item shape: { label, onSelect, icon?, shortcut?, disabled?, destructive?, closeOnSelect? }, plus { type: "separator" } and { type: "label", label }. Use destructive: true for delete actions.
Tooltip & Popover — content prop + single child
<Tooltip content="Save changes" placement="top">
<Button>Save</Button>
</Tooltip>
<Popover
trigger="click" // "click" | "hover" | "manual"
placement="bottom"
content={<FilterPanel />} // no Popover.Trigger / Popover.Content
>
<Button variant="outlined">Filter</Button>
</Popover>placement accepts 8 values — top, bottom, left, right, and the -start/-endvariants of top/bottom — with auto-flip when there's no room.
Tabs & Table
Tabs — compound List / Trigger / Panel
<Tabs defaultValue="overview" orientation="horizontal" variant="underline">
<Tabs.List>
<Tabs.Trigger value="overview">Overview</Tabs.Trigger>
<Tabs.Trigger value="settings">Settings</Tabs.Trigger>
</Tabs.List>
<Tabs.Panel value="overview">…</Tabs.Panel>
<Tabs.Panel value="settings">…</Tabs.Panel>
</Tabs>The panel component is Tabs.Panel (not Tabs.Content). Controlled via value + onValueChange, or uncontrolled via defaultValue. Extras: variant (underline | pill | soft), size, fullWidth, orientation. Arrow keys and roving tabindex are built in.
Table — data-driven (data + columns)
Pass data and columns, not JSX rows — there are no Table.Header / Table.Row / Table.Cell subcomponents.
import type { TableColumn } from "@sisyphos-ui/react";
const columns: TableColumn<User>[] = [
{ id: "name", header: "Name", accessor: "name", sortable: true },
{ id: "email", header: "Email", accessor: "email", truncate: true },
{ id: "status", header: "Status", accessor: "status", render: (u) => <Chip>{u.status}</Chip> },
];
<Table<User>
data={users}
columns={columns}
rowKey={(u) => u.id} // required for selection/expansion
sort={sort} // { key, direction: "asc" | "desc" }
onSortChange={setSort}
selectable
selectedIds={selected}
onSelectionChange={setSelected}
loading={isLoading} // renders skeleton rows
stickyHeader
striped
size="md" // "sm" | "md" | "lg" — no density prop
pagination={{ page, pageCount, onPageChange: setPage }}
/>TypeScript
- Every prop is typed; the codebase has zero
any. Buttonrenders an<a>whenhrefis set — there is no genericasprop.- Discriminated unions for menu items —
DropdownMenuAction|DropdownMenuSeparator|DropdownMenuLabel. Exhaustiveswitchontypeworks. - Theme types (
ThemeConfig) come from@sisyphos-ui/core. Extend component prop types instead of redeclaring:
import type { ButtonProps, TableColumn, SortState } from "@sisyphos-ui/react";
import type { ThemeConfig } from "@sisyphos-ui/core";
type MyButtonProps = ButtonProps & { trackingId: string };Accessibility guarantees
Every interactive component already ships with:
- Keyboard support per the WAI-ARIA Authoring Practices.
- Focus management — trap + restore on
Dialog, roving tabindex onTabsandRadioGroup, focus return from menus. - Correct semantic roles:
menu/menuitem,combobox+listbox+option,dialog,switch,tooltip,alertvsstatuson toasts. - Form wiring —
FormControlconnects label/helper/error throughid/aria-describedby; inputs setaria-invalidin the error state. - Icon-only buttons require an
aria-label(enforced by the types).
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() from @sisyphos-ui/core. There is no ThemeProvider.
- ✕
import { applyTheme } from "@sisyphos-ui/react"Theme utilities live only in @sisyphos-ui/core — the framework umbrellas do not re-export them.
- ✕
v0.5 imports (@sisyphos-ui/ui, @sisyphos-ui/button, …)Use the framework umbrella (@sisyphos-ui/react / vue / angular). Never mix v0.5 packages with a v1 umbrella.
- ✕
<FormControl label="…" errorText="…">v1 FormControl is compound — use FormLabel / FormHelperText / FormErrorText children.
- ✕
<Dialog.Trigger> / <Dialog.Content>They don't exist. Open the dialog from your own button and put size on the Dialog root.
- ✕
JSX table rows (Table.Row, Table.Cell) or <Tabs.Content>The v1 Table takes data + columns; the Tabs panel is Tabs.Panel.
- ✕
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.
- ✕
setThemeMode("system") or toggling the sisyphos-theme-dark class by handOnly "light" | "dark" are valid; resolve the OS preference yourself and use setThemeMode() / toggleThemeMode().