Skip to content
Agent skill · v2.0.0

sisyphos-ui-best-practices

Complete reference for Sisyphos UI v1 — a 33-component, runtime-themeable design system shipped as three framework umbrella packages (React, Vue 3, Angular 18) that share one visual, ARIA, and CSS-token contract. Use this skill when adding or refactoring UI in a React/Next.js, Vue, or Angular app, theming the design system, or wiring up forms, dialogs, toasts, command palettes, menus, and tables.

MIT33 componentsReact · Vue · AngularapplyTheme()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 — the core foundation, the 33 components, the three framework umbrellas, the theming engine, dark mode, and every common UI pattern across React, Vue, and Angular.

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 v1 — a 33-component, runtime-themeable design system shipped as three framework umbrella packages (React, Vue 3, Angular 18) that share one visual, ARIA, and CSS-token contract. Use this skill when adding or refactoring UI in a React/Next.js, Vue, or Angular app, theming the design system, or wiring up forms, dialogs, toasts, command palettes, menus, and tables.
license: MIT
homepage: https://sisyphosui.com
repository: https://github.com/sisyphos-ui/sisyphos-ui
version: 2.0.0
---
Outline

Quick reference

16 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
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.
  • Adding dark mode, custom theme, or per-section overrides to a Sisyphos-powered app.
02

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.

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

v0.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 # migrate
03

Installation

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)

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>,
);

Vue 3

snippet.bashbash
pnpm add @sisyphos-ui/vue
App.vuevue
<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.

snippet.bashbash
pnpm add @sisyphos-ui/angular
app.component.tsts
// 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"); }
}
04

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:

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

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

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.

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

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("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().

07

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
PackagePurpose
@sisyphos-ui/react4 variants × 5 sizes, loading state, optional dropdownItems split-button, renders <a> via href.
@sisyphos-ui/reactText field with label, error/errorMessage, icons, password toggle, masks, char count.
@sisyphos-ui/reactMultiline with optional auto-resize, character counter, error state.
@sisyphos-ui/reactTri-state via indeterminate — activating promotes to checked.
@sisyphos-ui/reactrole="switch" toggle; always controlled (checked + onChange).
@sisyphos-ui/reactSingle choice; compose <Radio> children or pass an options array.
@sisyphos-ui/reactSingle/multi combobox with search, clearable, creatable, async load-more.
@sisyphos-ui/reactHierarchical multi-select with search, cascade selection, mixed parent state.
@sisyphos-ui/reactLocale-aware formatting, stepper buttons, prefix/suffix, min/max/step.
@sisyphos-ui/reactSingle or range dual-thumb; Arrow/Page/Home/End keyboard support.
@sisyphos-ui/reactSingle date or isRange, optional time, min/max, localized names.
@sisyphos-ui/reactDrag-and-drop, accept/maxSize/maxFiles validation, per-file progress.

Display

13 packages
PackagePurpose
@sisyphos-ui/reactTags and filters with optional icon/avatar and delete button.
@sisyphos-ui/reactImage with fallback to initials or placeholder; grouping.
@sisyphos-ui/reactrole="status" loading indicator with screen-reader label.
@sisyphos-ui/reactPlaceholder — rectangular/circular/text, shimmer/pulse/none.
@sisyphos-ui/reactStandardized zero-data view: icon, title, description, actions.
@sisyphos-ui/reactPersistent inline callout with semantic colors.
@sisyphos-ui/reactNav trail with maxItems middle-collapse, aria-current.
@sisyphos-ui/reactCompound surface — Header / Body / Footer, interactive mode.
@sisyphos-ui/reactCollapsible panels, single or multi-expand.
@sisyphos-ui/reactCompound List / Trigger / Panel, roving tabindex, h/v orientation.
@sisyphos-ui/reactData-driven (data + columns): sorting, selection, expansion, pagination, sticky header.
@sisyphos-ui/reactAutoplay (paused on hover/focus), arrows, dot indicators.
@sisyphos-ui/reactKeyboard hint — shortcut="cmd+k" or keys={["cmd","k"]}.

Overlay

7 packages
PackagePurpose
@sisyphos-ui/react8 placements with auto-flip, wired via aria-describedby.
@sisyphos-ui/reactcontent prop + trigger child; role="dialog" panel; click/hover/manual.
@sisyphos-ui/reactrole="menu" action menu driven by an items array.
@sisyphos-ui/reactModal with focus trap, scroll lock; compound Header/Body/Footer/Close.
@sisyphos-ui/reactImperative API, polite vs assertive distinction.
@sisyphos-ui/reactRight-click menu at the cursor, viewport-clamped; same items shape as DropdownMenu.
@sisyphos-ui/reactFilterable command palette — combobox + listbox semantics.

Foundation

3 packages
PackagePurpose
@sisyphos-ui/coreTheme engine (applyTheme, setThemeMode, toggleThemeMode), design tokens.
@sisyphos-ui/reactRenders children into the document body; plus useFocusTrap / useScrollLock.
@sisyphos-ui/reactCompound field wrapper — FormLabel / FormHelperText / FormErrorText children.
08

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.

snippet.tsxtsx
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>
  • FormControl props: id?, disabled, required, readOnly, error (boolean), fullWidth — there is no label / helperText / errorText prop.
  • FormErrorText renders only while error is true on the FormControl.
  • For quick single fields, Input, Select, and Textarea also accept their own label, error, and errorMessage props — pick one mechanism per field, not both.
  • 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"; // 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 all

toast() / .success / .warning / .info / .loading 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, 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.

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

11

Command palette

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

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

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

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

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

13

Tabs & Table

Tabs — compound List / Trigger / Panel

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

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

TypeScript

  • Every prop is typed; the codebase has zero any.
  • Button renders an <a> when href is set — there is no generic as prop.
  • Discriminated unions for menu items — DropdownMenuAction | DropdownMenuSeparator | DropdownMenuLabel. Exhaustive switch on type works.
  • Theme types (ThemeConfig) come from @sisyphos-ui/core. Extend component prop types instead of redeclaring:
snippet.tsts
import type { ButtonProps, TableColumn, SortState } from "@sisyphos-ui/react";
import type { ThemeConfig } from "@sisyphos-ui/core";

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 + restore on Dialog, roving tabindex on Tabs and RadioGroup, focus return from menus.
  • Correct semantic roles: menu/menuitem, combobox + listbox + option, dialog, switch, tooltip, alert vs status on toasts.
  • Form wiring — FormControl connects label/helper/error through id / aria-describedby; inputs set aria-invalid in 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, 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() 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 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.

  • ✕setThemeMode("system") or toggling the sisyphos-theme-dark class by hand

    Only "light" | "dark" are valid; resolve the OS preference yourself and use setThemeMode() / toggleThemeMode().

Source on GitHub

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