Skip to content
Agent skill · v1.0.0

sisyphos-ui-best-practices

Complete reference for Sisyphos UI — a 35-component, runtime-themeable React UI kit. Use this skill when adding or refactoring UI in a React or Next.js app, theming the design system, or wiring up forms, dialogs, toasts, command palettes, menus, and tables. Covers every package in the family.

MIT35 componentsLayered packagesapplyTheme()Agent-ready
Install

Drop this skill into your project

Single Markdown file. Pick the channel that matches your agent setup — pnpm, Claude Code, or a plain curl.

terminal

Adds the skill to your repo via the skills CLI.

Summary

A self-contained skill for AI coding agents. It covers the entire Sisyphos UI package family — foundation layer, the 35 component packages, the umbrella, the theming engine, dark mode, every common pattern, and migration paths from MUI, Chakra, shadcn/ui, and HeadlessUI.

Skill

SKILL.md

The skill is a single Markdown file with YAML frontmatter. Drop it in your project's .claude/skills/ folder, or load it through any agent that consumes a markdown skill.

skills.mdmarkdown
---
name: sisyphos-ui-best-practices
description: Complete reference for Sisyphos UI — a 35-component, runtime-themeable React UI kit. Use this skill when adding or refactoring UI in a React or Next.js app, theming the design system, or wiring up forms, dialogs, toasts, command palettes, menus, and tables. Covers every package in the family.
license: MIT
homepage: https://sisyphosui.com
repository: https://github.com/sisyphos-ui/sisyphos-ui
version: 1.0.0
---
Outline

Quick reference

17 sections, prioritized by how often the model should consult them when generating UI code.

SectionPriority
When to applyCritical
Package architectureCritical
InstallationCritical
The Core packageHigh
Theming with applyTheme()Critical
Dark modeHigh
Component referenceCritical
Form patternsHigh
Toast patternsHigh
Dialog patternsHigh
Command paletteMedium
Menus & overlaysMedium
Tabs & TableMedium
TypeScriptMedium
Accessibility guaranteesMedium
Anti-patternsHigh
Migration cheatsheetMedium
01

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.
02

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.

Layer 2 · Framework bindings

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/angular
Layer 1 · Foundation

Design tokens, applyTheme() runtime engine, dark-mode helpers. Framework-agnostic CSS variables under --sisyphos-* — every framework binding builds on this layer.

@sisyphos-ui/core

Pick 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/react

Per-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/react
03

Installation

The umbrella is the recommended default. Pick a framework recipe below and copy it verbatim — the order of operations matters.

Next.js (app router)

snippet.bashbash
pnpm add @sisyphos-ui/react
app/layout.tsxtsx
import "@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

src/main.tsxtsx
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.

snippet.bashbash
pnpm add @sisyphos-ui/react
app/layout.tsxtsx
import "@sisyphos-ui/core/styles.css";
import "@sisyphos-ui/react/styles.css";
04

The Core package — @sisyphos-ui/core

The foundation that every other package depends on. Three exports you'll touch repeatedly:

snippet.tsts
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-*:

snippet.tsts
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 };
};
05

Theming with applyTheme()

Override any subset of tokens at runtime — the change cascades through every Sisyphos component immediately, no rebuild.

snippet.tsxtsx
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:
snippet.tsxtsx
<div style={{ "--sisyphos-color-primary": "#0ea5e9" } as React.CSSProperties}>
  <Button>Section-scoped blue button</Button>
</div>
06

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.

snippet.tsxtsx
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:

snippet.htmlhtml
<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.

07

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
PackagePurpose
@sisyphos-ui/react4 variants × 5 sizes, loading, optional split-button, polymorphic via href.
@sisyphos-ui/reactText input with adornments, validation state, helper text.
@sisyphos-ui/textareaAuto-resize multiline, char count.
@sisyphos-ui/checkboxTri-state — checked, unchecked, indeterminate.
@sisyphos-ui/switchOn/off toggle.
@sisyphos-ui/radioSingle-choice with roving tabindex.
@sisyphos-ui/selectDropdown with search, multi-select, virtualization-ready.
@sisyphos-ui/tree-selectHierarchical picker with cascading.
@sisyphos-ui/number-inputStepper, precision, locale-aware.
@sisyphos-ui/sliderSingle or dual-thumb, marks, snapping.
@sisyphos-ui/datepickerCalendar, range, disabled dates.
@sisyphos-ui/file-uploadDrag-and-drop, preview, progress.

Display

13 packages
PackagePurpose
@sisyphos-ui/chipTags, filters, removable badges.
@sisyphos-ui/avatarImage, initials, status dot, grouping.
@sisyphos-ui/spinnerReduced-motion aware loader.
@sisyphos-ui/skeletonContent placeholder with shimmer.
@sisyphos-ui/empty-stateStandardized 'no results' UI.
@sisyphos-ui/alertInline message with semantic colors.
@sisyphos-ui/breadcrumbHierarchical navigation trail.
@sisyphos-ui/cardCompound surface — Header / Body / Footer.
@sisyphos-ui/accordionCollapsible panels, single or multi-expand.
@sisyphos-ui/tabsCompound, roving tabindex, h/v orientation.
@sisyphos-ui/tableSorting, selection, sticky headers, density.
@sisyphos-ui/carouselTouch, autoplay, indicators.
@sisyphos-ui/kbdPlatform-aware keyboard hint — shortcut="cmd+k".

Overlay

7 packages
PackagePurpose
@sisyphos-ui/tooltip12 placements, aria-describedby.
@sisyphos-ui/popoverrole="dialog" floating panel.
@sisyphos-ui/dropdown-menurole="menu" with submenu, type-ahead.
@sisyphos-ui/reactModal with focus trap, scroll lock.
@sisyphos-ui/reactImperative API, polite vs assertive distinction.
@sisyphos-ui/context-menuRight-click menu, viewport-clamped.
@sisyphos-ui/commandFilterable command palette / combobox.

Foundation

3 packages
PackagePurpose
@sisyphos-ui/coreTheme engine, mode toggle, design tokens.
@sisyphos-ui/reactRenders children into the document body.
@sisyphos-ui/form-controlLabel / helper / error wrapper for any input.
08

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.

snippet.tsxtsx
<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.
09

Toast patterns

The toast API is imperative. There is no React context. Mount <Toaster /> once at the root, then call from anywhere.

snippet.tsxtsx
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.

10

Dialog patterns

Compound API. Focus trap, scroll lock, Esc-to-close, and focus restoration are automatic.

snippet.tsxtsx
<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.

11

Command palette

Keyboard-first filterable menu. Combobox + listbox semantics. Use it for ⌘K-style global search.

snippet.tsxtsx
<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

snippet.tsxtsx
<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

snippet.tsxtsx
<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

snippet.tsxtsx
<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>
13

Tabs & Table

Tabs — automatic vs manual activation

snippet.tsxtsx
<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

snippet.tsxtsx
<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>
14

TypeScript

  • Every prop is typed; the codebase has zero any.
  • Polymorphic primitives (Button, Card) accept as or href — TypeScript narrows the rest of the props accordingly.
  • Discriminated unions are used heavily — DropdownMenuItem vs DropdownMenuSeparator. Exhaustive switch works.
  • Re-export the type when extending; don't redeclare:
snippet.tsts
import type { ButtonProps } from "@sisyphos-ui/react";

type MyButtonProps = ButtonProps & { trackingId: string };
15

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-motion honored on Spinner, Skeleton, Carousel.
  • Correct semantic roles: menuitem, option, dialog, alert vs status, 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, or aria-* props that duplicate what the component already sets.
  • Wrap a Button in another <button> — use href or compose directly.
16

Anti-patterns

  • <ThemeProvider value={...}>

    Use applyTheme(). There is no ThemeProvider.

  • import "@sisyphos-ui/react/styles.css" alongside the umbrella

    Import "@sisyphos-ui/react/styles.css" once at the app root.

  • Mixing umbrella + per-component packages

    Pick one shape per app. Mixing duplicates CSS variables.

  • Driving Dialog open state outside its props

    Use the component's open / onOpenChange — focus trap depends on it.

  • applyTheme() inside a render

    Call it once at boot. Inside render = layout flash.

  • Tailwind / emotion to recolor a Sisyphos component

    Override the underlying --sisyphos-* CSS variable.

  • role="button" on a <div>

    Use <Button>. It already has the right semantics.

  • Manually toggling the dark class

    Use setMode() from @sisyphos-ui/core.

17

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
useMediaQueryNative matchMedia — no equivalent needed

From shadcn/ui

shadcn/uiSisyphos UI
Copy components into your repopnpm add @sisyphos-ui/react (versioned package)
cn util / tailwind-mergeNot 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 UISisyphos UI
<ChakraProvider theme={...}>applyTheme(...)
useColorModesetMode() + dark class
<HStack> / <VStack>Plain flex containers
useToast()import { toast } (imperative)

Source on GitHub

Skill file, library source, and component packages all live in the same monorepo.