Updated proposal attached — ready for review.
Speyer UI System
Built around four constraints: accessibility is mandatory, no build tools required, components work with or without JS, and status is never communicated by color alone. Lightweight and zero runtime dependencies. Scores 100 on Lighthouse accessibility.
Design Philosophy
Five principles that guide every decision in SUI.
Architecture
Four files, zero runtime dependencies, instant deployment.
/dist — served via jsDelivr. No signup required.
Component Library
Every SUI component, live and interactive. All built from design tokens, all accessible by default.
Accordion
Collapsible content sections. Keyboard accessible (Enter/Space). Multiple sections can be open.
SUI is a lightweight, accessible design system built on CSS custom properties. It provides design tokens, reusable component classes, and an optional JavaScript toolkit for interactive components — no heavy frameworks required.
No. The CSS files handle all visual styling. sui.js is optional and adds behavior for modals, toasts, dropdowns, tooltips, and accordion interactivity. Everything degrades gracefully without it.
Absolutely. SUI tokens are plain CSS custom properties that work in any environment. The component classes are standard CSS and can be applied to any HTML element. Use the tokens as your design foundation and build framework-specific components on top.
Alerts
Every alert uses icon + text. Color is never the sole indicator.
Avatars
Initials-based, deterministic colors. Privacy-friendly, no image dependencies.
Badges
Always paired with icon or text. Covers every SaaS status and labeling need.
Bottom Sheet
Mobile-first slide-up drawer. Handles safe areas, scroll trapping, and Escape key.
Buttons
44px touch-friendly. Visible focus states. Tab through to test keyboard navigation.
Card Variants
Default (border), elevated (shadow), muted, and flush.
Border-based separation. The standard card.
Opt-in elevation with sui-card-shadow.
Elevated background for nested cards.
Header, body, and the action footer — sui-card-footer right-aligns buttons with a top border, matching the modal footer anatomy.
Subtle hover lift. Lower visual weight than regular interactive.
.sui-card-header / .sui-card-body / .sui-card-footer, pair the wrapper with .sui-card-flush to zero out the card’s own padding. Use .sui-card-compact for dense layouts like KPI grids.Chips / Tags
Visual pill component. Behaviour (add, remove, keyboard) is bring-your-own JS — same as icons.
Dropzone
File upload area. CSS only — drag-and-drop behaviour is bring-your-own JS.
Drop files here or click to browse
PNG, JPG, PDF up to 10MB
Embed & Keyboard
Responsive video embeds and keyboard shortcut styling.
Responsive video embed (16:9)
Keyboard shortcuts
Toggle theme: Ctrl + Shift + T
Search: Ctrl + K
Save: Ctrl + S
Empty State
The "no data yet" pattern every table, list, and dashboard needs.
Form Inputs
Clear borders, visible focus rings, and accessible error messages.
Input Action v2.5.0
Embed a button inside an input — for search, copy, clear, or show/hide password.
Interactive Components
Powered by sui.js. Modals, toasts, dropdowns, and tooltips.
Layout Utilities
Grid auto-fit, horizontal scroll, flex-nowrap, interactive cards, and container queries.
sui-scroll-x)sui-container opt-in)Mark / Highlight
Search result highlighting. Automatic dark mode and print handling.
Search results: The quick brown fox jumped over the lazy dog while the quick cat watched.
sui-mark = all matches (soft yellow) · sui-mark-current = current match (strong amber)
Metadata Line
Compact metadata with automatic dot separators. No separator markup needed — CSS handles it.
Panel
Side panel / slide-over — the third overlay type. Dialog blocks, sheet slides up, panel slides from the right. Desktop: coexists with main content. Mobile: full-screen.
Click to open a panel with contact details. Desktop: panel appears alongside this content. Mobile: full-screen overlay.
Desktop (≥769px): Focus moves to the panel but is not trapped. Both the panel and main content remain interactive — panels are parallel content, not blocking content. Trapping focus would prevent keyboard users from referencing the main view.
Mobile (<769px): Panel goes full-screen. The user can no longer see main content, so the interaction model changes to a blocking overlay with focus trap enabled.
If the viewport resizes while the panel is open, the trap toggles automatically.
<!-- Trigger -->
<button data-sui-panel="#my-panel" aria-expanded="false">Open</button>
<!-- Panel (outside main for stacking context) -->
<div class="sui-panel" id="my-panel" aria-label="Detail" aria-hidden="true">
<div class="sui-panel-header">
<span class="sui-panel-title">Title</span>
<button class="sui-panel-close sui-btn sui-btn-ghost sui-btn-sm" type="button"
aria-label="Close panel">×</button>
</div>
<div class="sui-panel-body">Content</div>
<div class="sui-panel-footer">
<button class="sui-btn sui-btn-ghost">Cancel</button>
<button class="sui-btn sui-btn-primary">Save</button>
</div>
</div>
<!-- Custom width -->
<div class="sui-panel" style="--sui-panel-width: 50vw">...</div>
<!-- No backdrop (desktop) -->
<div class="sui-panel sui-panel-no-backdrop">...</div>
SUI.panel.open('#my-panel');
SUI.panel.close('#my-panel');
SUI.panel.toggle('#my-panel');
Progress Bars
Accessible with aria attributes. Status-colored variants and indeterminate mode.
Prose
Long-form typography for articles, docs, and rendered Markdown. One class, it works. Size variants and width constraints available.
The Human Advantage
In a world where AI automates everything else, human connection becomes the last competitive advantage. This is not a metaphor — it's a measurable business outcome.
Why Connection Wins
The companies that thrive in the next decade will be the ones that invest in meaningful relationships — with customers, partners, and communities.
The best technology disappears. What remains is the relationship it enabled.
Key Metrics
- Customer retention increases 23% with community investment
- NPS scores correlate with depth of connection, not frequency
- Enterprise deals close 40% faster when champions exist
const roi = connections.map(c => c.lifetime_value)
.reduce((sum, v) => sum + v, 0);
| Strategy | Investment | Return |
|---|---|---|
| Community | $50K/yr | 3.2x |
| Events | $120K/yr | 4.1x |
Compact prose at 15px. Tighter line height for dense content like sidebars, footnotes, or auxiliary text panels.
- Smaller base size
- Tighter leading
Generous prose at 20px. Wider line height for immersive reading — book chapters, long articles.
- Larger base size
- Relaxed leading
Screen Layout
Full-viewport app shell for mobile-first apps. Sticky header, scrollable body, pinned footer with safe area insets.
Classes
sui-screen — full-viewport flex column (100dvh)
sui-screen-header — sticky top with safe-area-inset-top
sui-screen-body — scrollable area (flex:1)
sui-screen-footer — pinned bottom with safe-area-inset-bottom
Multi-view
Wrap screens in data-sui-screens and toggle with .is-active. Inactive screens are hidden. Use sui-screen-solo for single-screen apps.
Segmented Control
Value picker with radio semantics. Arrow keys navigate. Not tabs — semantically distinct.
Skeleton Loaders
Placeholder shimmer for content that's still loading.
Stepper
Multi-step workflows. CSS-only. Horizontal on desktop, vertical on mobile. States communicated by shape + colour — never colour alone.
- ✓ Account
- 2 Details
- 3 Review
- ✓ Cart
- ✓ Shipping
- 3 Payment
- 4 Review
- 5 Confirm
- ✓Register
- ✓Verify
- ✓Profile
- ✓Settings
- ✓Import
- ✓Review
- ✓Launch
Tables
Responsive tables with mobile stacking, striped and hover variants.
Team Members
| Name | Role | Status | Joined |
|---|---|---|---|
ASAdrian Speyer |
Admin | Active | Jan 2024 |
JKJane Kim |
Developer | Active | Mar 2024 |
MLMike Lee |
Designer | Away | Jun 2024 |
RWRiya Wang |
PM | Offline | Sep 2024 |
Sortable + Interactive
| Company | Revenue | Stage | Owner |
|---|---|---|---|
| Acme Corp | $120,000 | Closed Won | Adrian S. |
| Globex Inc | $85,000 | Negotiation | Jane K. |
| Initech | $45,000 | Discovery | Mike L. |
Table Modifiers v2.5.0
Combine with any sui-table. Sticky header, dense rows, and tablet card stacking.
Sticky + Dense
| ID | Metric | Value | Change |
|---|---|---|---|
| 001 | Page views | 12,482 | +8% |
| 002 | Sessions | 4,291 | +3% |
| 003 | Bounce rate | 34.2% | +2% |
| 004 | Avg. duration | 2m 41s | -5% |
| 005 | Conversions | 847 | +12% |
| 006 | Revenue | $24,100 | +6% |
Stack (tablet card mode)
sui-table-stack stacks at 768px. Resize your browser below tablet width to see the card layout.
| Name | Department | Status |
|---|---|---|
| Dana Reeves | Engineering | Active |
| Alex Chen | Design | Away |
Timeline
Activity feed layout. Avatar left, content right, vertical connector. Content is flexible — badges, text, timestamps all compose inside.
30 min discovery call. Interested in Enterprise tier.
Quarterly review. Action items assigned.
Contract renewal due next month. Flag for follow-up.
Toolbar
Horizontal scrolling action bar for formatting, filters, or controls. Hidden scrollbar, touch-friendly.
Visibility Utilities
Two-tier visibility: completely hidden vs. screen-reader only.
Removes from layout and accessibility tree. Use for toggling panels, tabs, and conditional content.
display: none !important
Hidden visually but remains accessible to screen readers. Use for skip links and descriptive labels.
clip-path: inset(50%)
Screen readers can see this.
For Designers
Token reference, geometry specs, and workflow guidance for Figma or any design tool.
538 Purpose-Built Icons New in v3
Original SVG icons for SaaS interfaces — 28 categories, accessibility built in, works in both themes.
Token-first workflow
Design in layers: background → surface → border → text → accents.
--sui-text-primary on --sui-bg-card.--sui-space-2 (8px), --sui-space-3 (16px), --sui-space-4 (24px).Quick reference
The most common tokens for day-to-day layouts.
--sui-bg-primary--sui-bg-card--sui-bg-elevated--sui-border--sui-blue-primary--sui-shadow-mdComponent Geometry
Consistent border radius, sizing, and elevation from the token system.
AI Prompt
Paste into any AI tool to generate SUI-compliant code. Works with ChatGPT, Claude, Copilot, Cursor, and more.
Quick Prompt
Recommended for most tasks.
I want you to use the Speyer UI System (SUI) for this project.
## Step 1: Fetch the design system
Fetch the official SUI CSS and JS from CDN:
- Tokens: https://cdn.jsdelivr.net/gh/adrianspeyer/speyer-ui@latest/dist/sui-tokens.min.css
- Components: https://cdn.jsdelivr.net/gh/adrianspeyer/speyer-ui@latest/dist/sui-components.min.css
- JS (optional, defer): https://cdn.jsdelivr.net/gh/adrianspeyer/speyer-ui@latest/dist/sui.min.js
Place CSS <link> tags in <head>. Place <script> tags before </body> with the defer attribute.
Icons — SUI ships 538 purpose-built icons. Add the icon CSS and sprite loader:
<link rel="stylesheet" href="https://cdn.jsdelivr.net/gh/adrianspeyer/speyer-ui@latest/dist/sui-icons.min.css">
<script defer>
fetch('https://cdn.jsdelivr.net/gh/adrianspeyer/speyer-ui@latest/dist/sui-icons.min.svg')
.then(r => r.text())
.then(svg => { var d=document.createElement('div'); d.style.display='none'; d.innerHTML=svg; document.body.prepend(d); });
</script>
Or bring your own icons (Lucide, Heroicons, Phosphor, Font Awesome, or plain SVG).
Or fetch the readable source files from:
- https://raw.githubusercontent.com/adrianspeyer/speyer-ui/main/sui-tokens.css
- https://raw.githubusercontent.com/adrianspeyer/speyer-ui/main/sui-components.css
- https://raw.githubusercontent.com/adrianspeyer/speyer-ui/main/sui.js
Use the CSS custom properties (--sui- prefix) and component classes as your foundation.
## Step 2: Confirm display mode
Before writing any code, ask me:
"Would you like light mode only, dark mode only, or both (with a toggle)?"
Then implement accordingly:
- Light mode = default :root tokens
- Dark mode = [data-theme="dark"] tokens
- Both = include both + a toggle using the data-theme attribute
## Step 3: Apply SUI design rules
Core Principles: readability, accessibility, mobile-friendly, color-blind friendly, consistency.
Typography: Inter font (Google Fonts). Fallback: system-ui.
Scale: H1 32px/700, H2 24px/600, H3 20px/600, Body 16px/400, Small 14px/400, Meta 12px/400.
Layout: 8px spacing grid (--sui-space-1 through --sui-space-6). Mobile-first. Max width: 1280px. Min touch target: 44px.
Components (use SUI classes):
- Buttons: sui-btn, sui-btn-primary, sui-btn-secondary, sui-btn-outline, sui-btn-ghost, sui-btn-dashed, sui-btn-danger, sui-btn-success, sui-btn-sm, sui-btn-lg, sui-btn-full
- Cards: sui-card, sui-card-lg, sui-card-muted, sui-card-shadow, sui-card-flush, sui-card-header, sui-card-body, sui-card-footer
- Inputs: sui-input, sui-input-group, sui-input-label, sui-input-error, sui-input-success, sui-select, sui-checkbox-label
- Toggle: sui-toggle-label, sui-toggle, sui-toggle-track
- Badges: sui-badge + sui-badge-success/warning/error/info/neutral/new/beta/pro
- Badge modifiers: sui-badge-outline, sui-badge-sm, sui-badge-dot, sui-badge-count
- Alerts: sui-alert + sui-alert-success/warning/error/info (always icon + text)
- Avatars: sui-avatar + sui-avatar-sm/md/lg (initials-first, optional photo with onerror fallback)
- Tables: sui-table-wrap > sui-table (use data-label on td for mobile stacking)
- Table modifiers: sui-table-stack (card stacking at tablet), sui-table-sticky (sticky thead), sui-table-dense (compact padding)
- Input action: sui-input-action (embedded button inside input — search, copy, clear)
- Progress: sui-progress + sui-progress-bar + status variants + sui-progress-indeterminate
- Navigation: sui-breadcrumb, sui-pagination, sui-page-btn
- Accordion: sui-accordion (requires sui.js for interactivity)
- Dropdown: sui-dropdown (requires sui.js)
- Modal: dialog.sui-dialog (recommended, native) — or legacy sui-modal-overlay + sui-modal. Both work with SUI.modal.open/close.
- Toast: SUI.toast.success/error/warning/info (requires sui.js)
- Tooltip: sui-tooltip + sui-tooltip-content
- Empty state: sui-empty + sui-empty-icon + sui-empty-title + sui-empty-text
- Skeleton: sui-skeleton + sui-skeleton-text/heading/avatar/card
- Bottom sheet: sui-sheet + sui-sheet-panel/handle/header/body/footer (requires sui.js). SUI.sheet.open/close.
- Sidenav: sui-sidenav + sui-sidenav-panel/link/heading/toggle/close (requires sui.js). Sticky sidebar desktop, slide-in mobile. SUI.sidenav.open/close/toggle.
- Collapsible groups: use sui-sidenav-group-toggle (aria-expanded + aria-controls) + sui-sidenav-group-links (+ optional sui-sidenav-group-count). SUI.sidenav.collapseAll/expandAll.
- Panel: sui-panel + sui-panel-header/title/close/body/footer (requires sui.js). Slides from right. Desktop: no focus trap (parallel content). Mobile: full-screen with trap. Width: --sui-panel-width. No backdrop: sui-panel-no-backdrop. SUI.panel.open/close/toggle.
- Segmented control: sui-segmented + sui-segment (role="radiogroup" + role="radio", requires sui.js)
- Chip/Tag: sui-chip + sui-chip-remove (CSS only, behaviour is bring-your-own JS)
- Interactive card: sui-card-interactive (clickable card with hover shadow elevation)
- Tables: sui-table-interactive (clickable rows), sui-table-sortable (th[data-sort] visual indicators)
- Progress labeled: sui-progress-labeled + sui-progress-text (text inside bar, contrast-safe in both themes)
- Prose: sui-prose (long-form typography container), sui-prose-sm/lg (size), sui-prose-narrow/wide (width)
- Mark: sui-mark (search highlight), sui-mark-current (active match)
- Meta: sui-meta (dot-separated metadata line)
- Toolbar: sui-toolbar + sui-toolbar-btn + sui-toolbar-sep, sui-toolbar-bordered, sui-toolbar-compact
- Screen: sui-screen (100dvh app shell), sui-screen-header, sui-screen-body, sui-screen-footer, sui-screen-solo
- Dropzone: sui-dropzone (CSS only, .is-dragover state, behaviour is bring-your-own JS)
- Timeline: sui-timeline + sui-timeline-item + sui-timeline-content (activity feed, role="feed", optimised for sui-avatar-sm)
- Stepper: sui-stepper + sui-step + sui-step-indicator (states: is-complete, is-active, is-pending; aria-current="step" on active)
- Topbar aligned: sui-topbar-aligned modifier (flex alternative to grid, brand left, nav+actions right)
- Avatars: sui-avatar + sui-avatar-sm/md/lg/xl (initials-first, optional photo with onerror fallback)
- Scoped tabs: wrap in data-sui-tabs to isolate multiple tab sets on one page
- Layout: sui-grid, sui-grid-2/3/4, sui-grid-auto, sui-section, sui-flex, sui-flex-col, sui-flex-nowrap, sui-scroll-x
- Container queries: sui-container (opt-in), sui-cq-stack, sui-cq-full, sui-cq-hide
CRITICAL: Check Recipes Before Building Custom CSS
SUI uses composition — complex SaaS patterns combine existing classes.
Before writing custom CSS, check if a recipe exists:
| Pattern | Composes From | Type |
|---------|--------------|------|
| Kanban / Board | sui-flex-nowrap + sui-scroll-x + sui-card | Recipe |
| Master-Detail / Inbox | sui-table-interactive + sui-panel | Recipe |
| Notification Panel | sui-panel + stacked sui-card + sui-badge | Recipe |
| Floating Action Bar | sui-card-shadow + sui-btn + fixed position | Recipe |
| Panel Push Mode | CSS grid toggle resizing main content | Recipe |
| Inline Edit | sui-input + sui-btn-sm + click-to-edit | Recipe |
| Settings / Preferences | sui-toggle + sui-input + sui-segmented | Recipe |
| Action Sheet | sui-sheet + sui-btn-ghost + sui-divider | Recipe |
| Button Group | sui-grid + sui-btn + role="radiogroup" | Recipe |
| App Shell | sui-screen + sui-segmented | Recipe |
| Blog Post | sui-prose + sui-meta + sui-avatar | Recipe |
| Document Library | sui-table + sui-badge + sui-avatar | Recipe |
| Profile Page | sui-avatar-xl + sui-badge + sui-card | Recipe |
| Split Pane | CSS grid + sui-card + sui-avatar | Recipe |
| Sidenav Search | sui-input + filter JS hiding sui-sidenav-link | Recipe |
| Sidenav Context | SUI.sidenav.collapseAll() + expand active | Recipe |
Common names → SUI solutions:
- Sidebar / Side menu → sui-sidenav (Component). Collapsible groups: sui-sidenav-group-toggle + sui-sidenav-group-links.
- Modal / Popup / Dialog → sui-dialog with native <dialog> (Component)
- Drawer / Slide-over → sui-panel (right) or sui-sheet (bottom) (Component)
- Toast / Snackbar → SUI.toast.success/error/warning/info() (JS API)
- Wizard / Steps → sui-stepper + sui-step + sui-step-indicator (Component)
- Activity feed / Log → sui-timeline + sui-timeline-item (Component)
- Search highlight → sui-mark + sui-mark-current (Component)
- Action bar → sui-toolbar + sui-toolbar-btn + sui-toolbar-sep (Component)
- Tag / Chip → sui-chip + sui-chip-remove (Component)
- Segmented toggle → sui-segmented + sui-segment (Component)
- Empty state → sui-empty + sui-empty-icon/title/text (Component)
- File upload → sui-dropzone (Component)
- App shell / SPA → sui-screen + sui-screen-header/body/footer (Component)
For the comprehensive reference (all class names, ARIA requirements, recipes with code), fetch:
https://raw.githubusercontent.com/adrianspeyer/speyer-ui/main/.claude/instructions.md
Accessibility (mandatory):
- WCAG 2.1 AA contrast ratios
- Colour must NEVER be the only indicator — always pair with icon + text
- Colour-blind friendly: no red/green-only patterns
- Keyboard navigation, visible focus states, ARIA labels
- Tabs MUST use: role="tablist" on container, role="tab" on buttons, role="tabpanel" on panels, aria-label on each
- Any text hidden via display:none at any breakpoint needs aria-label backup on its parent
- Never use aria-selected without role="tab"
- Respect prefers-reduced-motion
- Skip link pattern: add a skip link as the first child of <body>: <a href="#main" class="sui-visually-hidden">Skip to main content</a> and add id="main" to your <main> element
Icons: SUI works with any icon library. Always add aria-hidden="true" on decorative icons. Always pair icons with visible text labels — never use an icon alone to communicate meaning.
Status patterns (always icon + text):
- Success: ✓ + text | Warning: ⚠ + text | Error: ✕ + text | Info: ℹ + text
Elevation: Borders are the default card separation. Shadows are opt-in (sui-card-shadow).
Motion: 150-250ms ease-out. Feedback only. No decorative animations.
Brand colours: SUI semantic tokens (--sui-blue-primary, etc.) shift between themes. Do NOT use them for logos or brand marks — hardcode brand colours or use a custom property instead.
If you notice a component or pattern SUI doesn't cover yet, mention it — we welcome contributions at the GitHub repo.
Full docs: https://github.com/adrianspeyer/speyer-ui
Minimal Prompt
For quick tasks.
Use the Speyer UI System (SUI). CDN: https://cdn.jsdelivr.net/gh/adrianspeyer/speyer-ui@latest/dist/sui-tokens.min.css and sui-components.min.css Optional JS: https://cdn.jsdelivr.net/gh/adrianspeyer/speyer-ui@latest/dist/sui.min.js CSS links go in <head>. Scripts go before </body> with defer. Icons: SUI ships 538 purpose-built icons. Add sui-icons.min.css + sprite loader (see docs). Or bring your own (Heroicons, Phosphor, Font Awesome, plain SVG). Always pair icons with text labels. Ask me if I want light mode, dark mode, or both before coding. Key rules: Inter font, 8px spacing grid, 44px min touch targets, 12px button radius, 16px card radius, borders default (shadows opt-in), WCAG 2.1 AA, all status indicators must use icon + text (never colour alone), mobile-first, colour-blind friendly, tabs must use role="tablist"/role="tab"/role="tabpanel". Avatars are initials-first (optional photo with onerror fallback). Modals use native <dialog class="sui-dialog">. Bottom sheet: sui-sheet. Sidenav: sui-sidenav with collapsible groups (sui-sidenav-group-toggle + aria-expanded + aria-controls). Panel: sui-panel (slides from right, --sui-panel-width, sui-panel-no-backdrop). SUI.panel.open/close/toggle. Desktop: no focus trap. Mobile: focus trap. Segmented control: sui-segmented. Chips: sui-chip. Tables: sui-table-interactive (clickable rows), sui-table-sortable (sort indicators), sui-table-stack (card stacking at tablet), sui-table-sticky (sticky thead), sui-table-dense (compact). Input with button: sui-input-action. Timeline: sui-timeline. Dropzone: sui-dropzone. Stepper: sui-stepper + sui-step + sui-step-indicator (is-complete/is-active/is-pending). Scoped tabs: wrap in data-sui-tabs. SUI.tabs.activate(tabElement) for programmatic switching. Accordion: SUI.accordion.toggle(trigger), .expandAll(container), .collapseAll(container). Prose: sui-prose for long-form content. Mark: sui-mark for search highlights. Meta: sui-meta for dot-separated metadata. Toolbar: sui-toolbar for action bars. Screen layout: sui-screen (100dvh), sui-screen-header, sui-screen-body, sui-screen-footer. Topbar: sui-topbar (grid default) or sui-topbar-aligned (flex, brand left, nav+actions right). Multi-view: toggle .is-active. Single-screen: sui-screen-solo. Buttons: sui-btn + sui-btn-primary/secondary/ghost/dashed/danger/success. Use sui-* component classes for everything. CRITICAL: Before building custom CSS, check if a recipe exists. SUI composes patterns like Kanban (flex-nowrap + scroll-x + card), Master-Detail (table-interactive + panel), Notification Panel (panel + card + badge), Floating Action Bar (card-shadow + btn + fixed position), and 5 integration recipes (Flatpickr, Tom Select, FilePond, Quill, Chart.js) with SUI token overrides. 29 recipes total. Full recipe list and comprehensive AI reference: https://raw.githubusercontent.com/adrianspeyer/speyer-ui/main/.claude/instructions.md Skip link: add <a href="#main" class="sui-visually-hidden">Skip to main content</a> as first child of <body>. If a component is missing from SUI, mention it — contributions welcome. Full docs: https://github.com/adrianspeyer/speyer-ui
System Prompt
For custom instructions or Cursor rules.
When the user says "use SUI", "use the Speyer UI System", or "Speyer UI":
1. Fetch the SUI design files from CDN:
https://cdn.jsdelivr.net/gh/adrianspeyer/speyer-ui@latest/dist/sui-tokens.min.css
https://cdn.jsdelivr.net/gh/adrianspeyer/speyer-ui@latest/dist/sui-components.min.css
https://cdn.jsdelivr.net/gh/adrianspeyer/speyer-ui@latest/dist/sui.min.js (optional, for modals/toasts/dropdowns)
Or readable source from: https://raw.githubusercontent.com/adrianspeyer/speyer-ui/main/
CSS <link> tags go in <head>. <script> tags go before </body> with defer.
2. Icons: SUI ships 538 purpose-built icons via sui-icons.min.css + sprite loader. Add the CSS and use <svg class="sui-icon"><use href="#sui-icon-{category}-{name}"/></svg>. Or bring your own icons (Heroicons, Phosphor, Font Awesome, or plain SVG). Ask if they have a preference.
3. Ask the user: "Would you like light mode, dark mode, or both?"
4. Apply all CSS custom properties (--sui- prefix) and component classes.
5. Follow these design rules:
- Font: Inter (Google Fonts), fallback system-ui
- 8px spacing grid (--sui-space-1 through --sui-space-6)
- Min touch target: 44px, max content width: 1280px
- Buttons: 44px height, 12px radius | Cards: 16px radius, border separation
- Shadows: opt-in only (sui-card-shadow) — borders are default
- WCAG 2.1 AA contrast | Keyboard nav | prefers-reduced-motion support
- CRITICAL: Never use colour alone for status. Always pair with icon + text.
- Colour-blind safe: no red/green-only patterns
- Tabs: role="tablist" on container, role="tab" on buttons, role="tabpanel" on panels
- Never use aria-selected without role="tab"
- Avatars: initials-first (optional photo with onerror fallback)
- Icons: use aria-hidden="true" on decorative icons. Never use an icon alone without a text label.
6. Use SUI component classes: sui-btn (+ sui-btn-primary/secondary/ghost/dashed/danger/success),
sui-card, sui-badge, sui-alert, sui-avatar, sui-toggle, sui-progress, sui-accordion,
sui-dropdown, sui-dialog (native modal), sui-modal (legacy), sui-tooltip, sui-breadcrumb,
sui-pagination, sui-empty, sui-skeleton, sui-table, sui-sheet (bottom sheet),
sui-segmented (segmented control), sui-chip (tag/chip), sui-card-interactive,
sui-table-interactive, sui-table-sortable, sui-table-stack (card stacking at tablet),
sui-table-sticky (sticky thead), sui-table-dense (compact padding),
sui-input-action (embedded button inside input),
sui-progress-labeled, sui-dropzone,
sui-timeline, sui-stepper (multi-step wizard, CSS-only),
sui-prose (long-form typography), sui-mark (search highlight), sui-meta (metadata line),
sui-toolbar (action bar), sui-grid-auto, sui-scroll-x, sui-flex-nowrap,
sui-container (container queries, opt-in), sui-avatar-xl,
sui-topbar (grid default) or sui-topbar-aligned (flex, brand left, nav+actions right),
sui-screen/sui-screen-header/sui-screen-body/sui-screen-footer (app shells),
sui-sidenav + sui-sidenav-group-toggle/links (collapsible groups, SUI.sidenav.collapseAll/expandAll),
sui-panel (side panel, slides from right, --sui-panel-width, sui-panel-no-backdrop, SUI.panel.open/close/toggle. Desktop: no focus trap. Mobile: focus trap.), etc.
Themes: light, dark, auto.
7. CRITICAL: Before building custom CSS, check if a recipe exists. SUI uses composition:
Kanban (flex-nowrap + scroll-x + card), Master-Detail (table-interactive + panel),
Notification Panel (panel + card + badge), Floating Action Bar (card-shadow + btn + fixed),
Inline Edit (input + btn-sm), Settings (toggle + input + segmented), Action Sheet (sheet + btn-ghost),
Push Mode (CSS grid toggle), Split Pane (CSS grid + card), and more.
Common names: Sidebar = sui-sidenav, Modal = sui-dialog, Drawer = sui-panel/sui-sheet,
Toast = SUI.toast, Wizard = sui-stepper, Activity feed = sui-timeline,
Search highlight = sui-mark, Tag = sui-chip, Empty state = sui-empty, File upload = sui-dropzone.
Full recipe list: https://raw.githubusercontent.com/adrianspeyer/speyer-ui/main/.claude/instructions.md
8. Skip link: add <a href="#main" class="sui-visually-hidden">Skip to main content</a> as first child of <body>. Add id="main" to <main>.
9. If a component isn't covered by SUI, mention it — contributions welcome.
10. Reference: https://github.com/adrianspeyer/speyer-ui
Recipes
SUI ships components. You build patterns. These recipes show common application layouts composed from SUI primitives — with minimal custom CSS clearly documented. Copy the code, adjust to your needs.
Action Sheet
Mobile action menu. Composes from: sui-sheet, sui-btn, sui-btn-ghost, sui-divider. Custom CSS: ~15 lines. Inherits sheet's focus trap, Escape, and backdrop.
<!-- Trigger -->
<button class="sui-btn" data-sui-sheet="#actionSheet">More Actions</button>
<!-- Sheet -->
<div id="actionSheet" class="sui-sheet" aria-modal="true" role="dialog" aria-label="Actions">
<div class="sui-sheet-panel">
<div class="sui-sheet-handle"></div>
<div class="recipe-action-sheet">
<button class="sui-btn sui-btn-ghost sui-btn-full recipe-action-sheet-btn">Edit</button>
<button class="sui-btn sui-btn-ghost sui-btn-full recipe-action-sheet-btn">Duplicate</button>
<button class="sui-btn sui-btn-ghost sui-btn-full recipe-action-sheet-btn">Share</button>
<hr class="sui-divider">
<button class="sui-btn sui-btn-ghost sui-btn-full recipe-action-sheet-btn" style="color: var(--sui-error);">Delete</button>
</div>
</div>
</div>
<!-- Custom CSS (~15 lines) -->
<style>
.recipe-action-sheet { display: flex; flex-direction: column; }
.recipe-action-sheet-btn { justify-content: flex-start; border-radius: var(--sui-radius-md); }
.recipe-action-sheet .sui-divider { margin: var(--sui-space-1) 0; }
</style>
Mobile App Shell
Complete multi-screen mobile app skeleton with tab navigation. Composes from: sui-screen, sui-screen-header, sui-screen-body, sui-screen-footer, sui-segmented. Custom CSS: 0 lines.
<!-- Each screen is a full-viewport flex column -->
<div class="sui-screen is-active" data-screen="matches">
<div class="sui-screen-header">
<!-- Safe area inset handled automatically -->
<strong>MatchPoint</strong>
</div>
<div class="sui-screen-body">
<!-- Scrollable content -->
<div class="sui-card sui-card-interactive">...</div>
</div>
<div class="sui-screen-footer">
<!-- Safe area inset handled automatically -->
<div class="sui-segmented">...</div>
</div>
</div>
<!-- Switch screens by toggling .is-active -->
<div class="sui-screen" data-screen="schedule">...</div>
<div class="sui-screen" data-screen="messages">...</div>
<!-- For single-screen apps, use sui-screen-solo -->
<div class="sui-screen sui-screen-solo">...</div>
<!-- Custom CSS: 0 lines. All SUI classes. -->
App Shell Scaffold
Canonical responsive layout combining sui-topbar + sui-sidenav + scrollable main content. Uses app-* classes (not sui-). Composes from existing SUI components. Custom CSS: ~15 lines.
Note: sui-layout, sui-main, and sui-layout-body do not exist in SUI. This recipe provides the documented alternative.
Dashboard
<!-- App Shell Scaffold (~15 lines custom CSS) -->
<style>
.app-shell { display: flex; flex-direction: column; height: 100vh; }
.app-shell .sui-topbar { flex-shrink: 0; }
.app-shell-body { display: flex; flex: 1; min-height: 0; }
.app-shell-body .sui-sidenav {
position: static; height: auto; border-radius: 0;
border: none; border-right: 1px solid var(--sui-border);
}
.app-shell-body .sui-sidenav-panel {
position: static; width: 240px; height: 100%; box-shadow: none;
}
.app-shell-main { flex: 1; overflow-y: auto; padding: var(--sui-space-4); }
@media (max-width: 768px) {
.app-shell-body .sui-sidenav { display: none; }
}
</style>
<div class="app-shell">
<header class="sui-topbar">...</header>
<div class="app-shell-body">
<nav class="sui-sidenav" aria-label="App navigation">
<div class="sui-sidenav-panel">
<a href="/dashboard" class="sui-sidenav-link is-active">Dashboard</a>
<a href="/contacts" class="sui-sidenav-link">Contacts</a>
</div>
</nav>
<main class="app-shell-main">
<!-- Your page content -->
</main>
</div>
</div>
Blog Post / Article
Full article layout with metadata, prose content, and find-in-page highlighting. Composes from: sui-prose, sui-meta, sui-mark, sui-avatar. Custom CSS: 0 lines.
Why Connection Wins When AI Automates Everything Else
In a world where AI can write your emails, generate your reports, and schedule your meetings, what's left that only humans can do? The answer is deceptively simple: build relationships.
The companies that thrive in the next decade won't be the ones with the best algorithms — they'll be the ones with the deepest relationships.
Consider the data: customer retention increases 23% with community investment. NPS scores correlate with depth of connection, not frequency. Enterprise deals close 40% faster when internal champions exist.
<!-- Author + metadata -->
<div class="sui-flex sui-items-center sui-gap-3">
<span class="sui-avatar sui-avatar-md">AS</span>
<div>
<strong>Adrian Speyer</strong>
<div class="sui-meta">
<span>Feb 16, 2026</span>
<span>842 words</span>
<span>4 min read</span>
</div>
</div>
</div>
<!-- Article body — zero custom CSS -->
<div class="sui-prose">
<h2>Title</h2>
<p>Paragraph with <mark class="sui-mark">search matches</mark>
and <mark class="sui-mark sui-mark-current">current match</mark>.</p>
<blockquote><p>A quote.</p></blockquote>
</div>
<!-- Custom CSS: 0 lines. All SUI classes. -->
Button Group
Single-select option picker. Composes from: sui-grid, sui-btn. Custom CSS: ~10 lines. ARIA radiogroup pattern is the key value here.
<div class="sui-grid-2 sui-gap-2" role="radiogroup" aria-label="Select plan">
<button class="sui-btn sui-btn-ghost recipe-btn-group-option" role="radio" aria-checked="false">
<svg class="sui-icon" viewBox="0 0 24 24" aria-hidden="true"><use href="#sui-icon-act-zap"/></svg>Starter
</button>
<button class="sui-btn sui-btn-ghost recipe-btn-group-option is-selected" role="radio" aria-checked="true">
<svg class="sui-icon" viewBox="0 0 24 24" aria-hidden="true"><use href="#sui-icon-act-rocket"/></svg>Pro
</button>
<!-- More options... -->
</div>
<!-- Custom CSS (~10 lines) -->
<style>
.recipe-btn-group-option.is-selected {
border-color: var(--sui-blue-primary);
background: var(--sui-blue-soft);
color: var(--sui-blue-primary);
font-weight: var(--sui-weight-semibold);
}
</style>
<!-- JS: toggle is-selected + aria-checked on click -->
Chart.js Integration
SUI-native charts using Chart.js. Token-based colours for light/dark mode, SUI font family, responsive by default. This is an integration recipe — Chart.js is not part of SUI. You bring the library; SUI makes it match.
Live demo — toggle the theme to see dark mode. Charts re-render with SUI token colours.
What this recipe provides: a helper function that reads SUI tokens at runtime and applies them to Chart.js defaults (colours, font, grid lines). Theme-reactive via MutationObserver. What you bring: Chart.js from CDN, your chart configurations.
<!-- Chart.js Integration Recipe — copy into your project -->
<!-- 1. Load Chart.js (CDN) -->
<script src="https://cdn.jsdelivr.net/npm/chart.js" defer></script>
<!-- 2. SUI token helper (paste in your JS) -->
<script>
function getSUIToken(name) {
return getComputedStyle(document.documentElement)
.getPropertyValue(name).trim();
}
function suiChartDefaults() {
Chart.defaults.color = getSUIToken('--sui-text-muted');
Chart.defaults.borderColor = getSUIToken('--sui-border');
Chart.defaults.font.family =
getSUIToken('--sui-font-primary') || 'Inter, system-ui, sans-serif';
return {
blue: getSUIToken('--sui-blue-primary'),
green: getSUIToken('--sui-success-strong'),
red: getSUIToken('--sui-error-strong'),
amber: getSUIToken('--sui-warning-strong'),
purple: getSUIToken('--sui-pro-strong'),
blueSoft: getSUIToken('--sui-blue-soft'),
greenSoft: getSUIToken('--sui-success-soft'),
};
}
var t = suiChartDefaults();
</script>
<!-- 3. Use token colours in your charts -->
<canvas id="myChart"></canvas>
<script>
new Chart(document.getElementById('myChart'), {
type: 'bar',
data: {
labels: ['Jan', 'Feb', 'Mar'],
datasets: [{
label: 'Revenue',
data: [12, 19, 8],
backgroundColor: t.blue,
borderRadius: 4
}]
}
});
</script>
<!-- 4. Optional: re-render on theme toggle -->
<script>
new MutationObserver(function() {
var t = suiChartDefaults();
// Update your chart datasets with new colours, then chart.update()
}).observe(document.documentElement,
{ attributes: true, attributeFilter: ['data-theme'] });
</script>
Command Palette
Ctrl+K dialog with search input and results. Composes from: sui-dialog, sui-input, sui-kbd. Custom CSS: ~10 lines. BYOJS: you provide search logic.
<!-- Open with Ctrl/Cmd+K -->
<dialog class="sui-dialog" id="cmdk" aria-label="Command palette">
<div class="sui-modal-body" style="max-width:480px;">
<input class="sui-input" type="text" placeholder="Type a command..."
role="combobox" aria-expanded="true">
<ul role="listbox">
<li role="option">
<svg class="sui-icon" viewBox="0 0 24 24" aria-hidden="true"><use href="#sui-icon-act-plus"/></svg> New Contact
<kbd class="sui-kbd">N</kbd>
</li>
</ul>
</div>
</dialog>
<script>
document.addEventListener('keydown', e => {
if ((e.metaKey || e.ctrlKey) && e.key === 'k') {
e.preventDefault();
SUI.modal.open('#cmdk');
}
});
</script>
Confirmation Dialog
Accessible destructive-action confirmation using role="alertdialog". Composes from: sui-dialog, sui-btn-danger. Custom CSS: 0 lines.
<button class="sui-btn sui-btn-danger" data-sui-modal="#confirm">
Delete account
</button>
<dialog class="sui-dialog" id="confirm"
role="alertdialog" aria-label="Confirm deletion">
<div class="sui-modal-body">
<h3>Delete your account?</h3>
<p>This action is permanent.</p>
<div class="sui-flex sui-gap-2 sui-mt-4">
<button class="sui-btn sui-btn-danger"
onclick="SUI.modal.close('#confirm')">Delete</button>
<button class="sui-btn sui-btn-ghost"
onclick="SUI.modal.close('#confirm')">Cancel</button>
</div>
</div>
</dialog>
<!-- Custom CSS: 0 lines. All SUI classes. -->
<!-- Note: there is no SUI.modal.confirm() API — build your
own confirmation logic. This recipe shows the pattern. -->
Document Library
Searchable file list with toolbar, metadata, and status indicators. Composes from: sui-toolbar, sui-meta, sui-badge, sui-card, sui-input. Custom CSS: ~3 lines.
<div class="sui-card sui-card-flush">
<div class="sui-toolbar sui-toolbar-bordered">
<button class="sui-toolbar-btn" aria-pressed="true">...</button>
<div class="sui-toolbar-sep"></div>
<input class="sui-input" type="search" placeholder="Search...">
</div>
<div class="recipe-doc-list-item">
<div class="sui-flex sui-items-center sui-gap-3">
<svg class="sui-icon" viewBox="0 0 24 24" aria-hidden="true"><use href="#sui-icon-file-text"/></svg>
<div>
<strong>Document Name.md</strong>
<div class="sui-meta">
<span>12,340 words</span>
<span>Updated 2h ago</span>
</div>
</div>
<span class="sui-badge sui-badge-success sui-badge-sm">Published</span>
</div>
</div>
</div>
<!-- Custom CSS (~3 lines) -->
<style>
.recipe-doc-list-item { padding: var(--sui-space-3); border-bottom: 1px solid var(--sui-border); cursor: pointer; }
.recipe-doc-list-item:hover { background: var(--sui-bg-elevated); }
</style>
FilePond Integration
SUI-native file uploads using FilePond. Token-based overrides for light/dark mode, complements sui-dropzone with real upload behaviour. This is an integration recipe — FilePond is not part of SUI. You bring the library; SUI makes it match.
Live demo — drop files or click to browse. Files are not actually uploaded (demo only).
What this recipe provides: token-based colour overrides (light + dark), SUI border-radius, font family, focus ring, and status colours using SUI semantic tokens. Complements sui-dropzone — use dropzone for CSS-only styling, FilePond when you need real upload behaviour. What you bring: FilePond CSS + JS from CDN, your upload endpoint configuration.
<!-- FilePond Integration Recipe — copy into your project -->
<!-- 1. Load FilePond (CDN) -->
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/filepond/dist/filepond.min.css" />
<script src="https://cdn.jsdelivr.net/npm/filepond/dist/filepond.min.js" defer></script>
<!-- 2. SUI token overrides -->
<style>
.filepond--root { font-family: var(--sui-font-primary); }
.filepond--panel-root {
background-color: var(--sui-bg-elevated);
border-radius: var(--sui-radius-md);
}
.filepond--drop-label {
color: var(--sui-text-secondary);
font-size: var(--sui-text-small);
}
.filepond--label-action {
color: var(--sui-blue-primary);
text-decoration-color: var(--sui-blue-primary);
}
.filepond--item-panel {
border-radius: var(--sui-radius-sm);
}
.filepond--root:focus-within .filepond--panel-root {
outline: 3px solid var(--sui-blue-soft);
outline-offset: 2px;
}
[data-filepond-item-state="processing-complete"]
.filepond--item-panel {
background-color: var(--sui-success-strong);
}
[data-filepond-item-state*="error"]
.filepond--item-panel {
background-color: var(--sui-error-strong);
}
</style>
<!-- 3. Use with a file input -->
<input type="file" class="filepond" id="myUpload" multiple />
<script>
FilePond.create(document.getElementById('myUpload'), {
allowMultiple: true,
server: '/api/upload', // your endpoint
credits: false
});
</script>
Flatpickr Integration v2.5.1
SUI-native datepicker using Flatpickr. Token-based overrides for light/dark mode, accessible keyboard nav, zero custom CSS needed. This is an integration recipe — Flatpickr is not part of SUI. You bring the library; SUI makes it match.
Live demo — toggle the theme to see dark mode. Try keyboard navigation (arrow keys, Escape).
What this recipe provides: token-based colour overrides (light + dark), consistent border-radius, SUI font family, focus-visible ring (keyboard nav), shadow depth, z-index stacking. What you bring: Flatpickr CSS + JS from CDN, your init configuration. Use appendTo: document.body if the calendar is inside an overflow: hidden container.
<!-- Flatpickr Integration Recipe — copy this into your project -->
<!-- 1. Load Flatpickr (CDN) -->
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/flatpickr/dist/flatpickr.min.css" />
<script src="https://cdn.jsdelivr.net/npm/flatpickr" defer></script>
<!-- 2. SUI token overrides (paste in your <style> or CSS file) -->
<style>
.flatpickr-calendar {
background: var(--sui-bg-card);
border: 1px solid var(--sui-border);
border-radius: var(--sui-radius-md);
box-shadow: var(--sui-shadow-lg);
font-family: var(--sui-font-primary);
color: var(--sui-text-primary);
}
.flatpickr-calendar::before,
.flatpickr-calendar::after { display: none; }
/* Hide browser calendar picker icons */
.flatpickr-input::-webkit-calendar-picker-indicator { display: none; }
.flatpickr-months .flatpickr-month,
.flatpickr-current-month .flatpickr-monthDropdown-months {
background: transparent;
color: var(--sui-text-primary);
fill: var(--sui-text-primary);
}
.flatpickr-months .flatpickr-prev-month,
.flatpickr-months .flatpickr-next-month {
color: var(--sui-text-secondary);
fill: var(--sui-text-secondary);
}
.flatpickr-months .flatpickr-prev-month:hover svg,
.flatpickr-months .flatpickr-next-month:hover svg {
fill: var(--sui-blue-primary);
}
.flatpickr-current-month input.cur-year {
color: var(--sui-text-primary);
}
.flatpickr-weekdays { background: transparent; }
.flatpickr-weekday {
color: var(--sui-text-muted);
font-weight: 600;
}
.flatpickr-day {
color: var(--sui-text-primary);
border-radius: var(--sui-radius-sm);
}
.flatpickr-day:hover {
background: var(--sui-bg-elevated);
border-color: var(--sui-bg-elevated);
}
.flatpickr-day.today {
border-color: var(--sui-blue-primary);
color: var(--sui-blue-primary);
}
.flatpickr-day.today:hover {
background: var(--sui-blue-primary);
border-color: var(--sui-blue-primary);
color: #fff;
}
.flatpickr-day.selected,
.flatpickr-day.selected:hover {
background: var(--sui-btn-primary-bg);
border-color: var(--sui-btn-primary-bg);
color: #fff;
}
.flatpickr-day.flatpickr-disabled,
.flatpickr-day.prevMonthDay,
.flatpickr-day.nextMonthDay {
color: var(--sui-text-muted);
}
.flatpickr-day.inRange {
background: var(--sui-blue-soft);
border-color: var(--sui-blue-soft);
color: var(--sui-text-primary);
}
.flatpickr-time { border-top: 1px solid var(--sui-border); }
.flatpickr-time input,
.flatpickr-time .flatpickr-am-pm {
color: var(--sui-text-primary);
}
.flatpickr-time input:hover,
.flatpickr-time .flatpickr-am-pm:hover,
.numInputWrapper:hover {
background: var(--sui-bg-elevated);
}
/* Focus ring — matches SUI's canonical pattern */
.flatpickr-day:focus {
outline: 3px solid var(--sui-blue-soft);
outline-offset: 2px;
}
.flatpickr-months .flatpickr-prev-month:focus-visible,
.flatpickr-months .flatpickr-next-month:focus-visible {
outline: 3px solid var(--sui-blue-soft);
outline-offset: 2px;
}
/* Stacking — prevents clipping inside overflow:hidden containers */
.flatpickr-calendar { z-index: 9999; }
</style>
<!-- 3. Use with SUI inputs -->
<label class="sui-input-label">Select date</label>
<input type="text" class="sui-input" id="myDate" placeholder="Pick a date…" />
<script>
// appendTo: document.body prevents clipping inside overflow:hidden containers
flatpickr('#myDate', { dateFormat: 'Y-m-d', appendTo: document.body });
</script>
Floating Action Bar
A fixed-position bar that appears when items are selected, showing bulk actions. Composes from: sui-card-shadow, sui-btn, sui-badge, position: fixed. Custom CSS: ~8 lines.
<!-- Floating Action Bar — appears when items are selected -->
<!-- Custom CSS (not part of sui-components.css): -->
<style>
.fab-bar {
position: fixed;
bottom: var(--sui-space-4);
left: 50%;
transform: translateX(-50%);
z-index: 50;
padding: var(--sui-space-2) var(--sui-space-3);
white-space: nowrap;
transition: opacity var(--sui-duration-fast) var(--sui-easing),
transform var(--sui-duration-fast) var(--sui-easing);
}
.fab-bar[hidden] {
display: block;
opacity: 0;
transform: translateX(-50%) translateY(1rem);
pointer-events: none;
}
</style>
<div class="sui-card sui-card-shadow fab-bar" id="bulkBar"
role="toolbar" aria-label="Bulk actions" hidden>
<div class="sui-flex sui-gap-2" style="align-items:center">
<span class="sui-badge sui-badge-info" id="bulkCount">0 selected</span>
<button class="sui-btn sui-btn-sm sui-btn-primary">
<svg class="sui-icon" viewBox="0 0 24 24" aria-hidden="true"><use href="#sui-icon-act-download"/></svg> Export
</button>
<button class="sui-btn sui-btn-sm sui-btn-danger">
<svg class="sui-icon" viewBox="0 0 24 24" aria-hidden="true"><use href="#sui-icon-act-trash-2"/></svg> Delete
</button>
</div>
</div>
<script>
// Wire up your selection logic:
function updateFab(selectedCount) {
const bar = document.getElementById('bulkBar');
const count = document.getElementById('bulkCount');
bar.hidden = selectedCount === 0;
count.textContent = selectedCount + ' selected';
}
</script>
Inline Edit
Click-to-edit fields with save/cancel actions. Composes from: sui-input, sui-btn-sm, sui-flex. Custom CSS: ~4 lines.
<!-- Display mode -->
<span class="recipe-inline-display" role="button" tabindex="0"
aria-label="Edit company name">Acme Corporation</span>
<!-- Edit mode (toggled via JS) -->
<div class="sui-flex sui-gap-2" aria-live="polite">
<input class="sui-input" type="text" value="Acme Corporation">
<button class="sui-btn sui-btn-primary sui-btn-sm">Save</button>
<button class="sui-btn sui-btn-ghost sui-btn-sm">Cancel</button>
</div>
<!-- Custom CSS (~4 lines) -->
<style>
.recipe-inline-display { cursor: pointer; border-bottom: 1px dashed var(--sui-border); padding: 2px 0; }
.recipe-inline-display:hover { border-color: var(--sui-blue-primary); }
</style>
Inline Selection Bar
Selection count + action buttons above content. Not to be confused with the Floating Action Bar (FAB) which is a fixed-position toolbar. Composes from: sui-btn, sui-flex. Custom CSS: ~6 lines.
| Select | Name | Status |
|---|---|---|
| Acme Corp | Active | |
| Globex | Active | |
| Initech | Pending | |
| Umbrella | Inactive |
<style>
.my-selection-bar {
display: flex; align-items: center; gap: var(--sui-space-3);
padding: var(--sui-space-2) var(--sui-space-3);
background: var(--sui-blue-soft); border-radius: var(--sui-radius-sm);
flex-wrap: wrap;
}
</style>
<div class="my-selection-bar">
<span aria-live="polite"><strong>3 selected</strong></span>
<button class="sui-btn sui-btn-primary sui-btn-sm">Email</button>
<button class="sui-btn sui-btn-ghost sui-btn-sm">Tag</button>
<button class="sui-btn sui-btn-danger sui-btn-sm">Delete</button>
<button class="sui-btn sui-btn-ghost sui-btn-sm">Clear</button>
</div>
<!-- Table rows use .is-selected (v2.6.0) for visual state -->
<tr class="is-selected">...</tr>
Kanban Board
Horizontal scrolling columns with stacked cards. Composes from: sui-flex-nowrap, sui-scroll-x, sui-card-interactive, sui-badge. Custom CSS: ~6 lines.
<div class="sui-flex sui-flex-nowrap sui-scroll-x sui-gap-3">
<div class="recipe-kanban-col">
<div class="sui-kpi-label">To Do</div>
<div class="sui-card sui-card-muted sui-card-interactive" tabindex="0">
<strong>Task title</strong>
<div class="sui-flex sui-items-center sui-gap-2">
<span class="sui-avatar sui-avatar-sm">AS</span>
<span class="sui-chip sui-chip-info">Label</span>
</div>
</div>
</div>
<!-- More columns... -->
</div>
<!-- Custom CSS (~6 lines) -->
<style>
.recipe-kanban-col { min-width: 260px; flex-shrink: 0; }
</style>
Master-Detail (Table + Panel)
CRM / help desk pattern — clickable table row opens sui-panel with detail content. Composes sui-table-interactive + sui-panel.
| Name | Company | Deal |
|---|---|---|
| Elena Vasquez | Acme Corp | $120K |
| Marcus Johnson | Globex | $85K |
| Lin Wei | Initech | $45K |
<table class="sui-table sui-table-interactive">
<thead><tr><th>Name</th><th>Email</th><th>Stage</th></tr></thead>
<tbody>
<tr onclick="showDetail('jane')" style="cursor: pointer">
<td>Jane Cooper</td>
<td>jane@acme.com</td>
<td><span class="sui-badge sui-badge-info">
<span aria-hidden="true">●</span>Qualified
</span></td>
</tr>
</tbody>
</table>
<div class="sui-panel" id="detailPanel"
aria-label="Contact detail" aria-hidden="true">
<div>
<div class="sui-panel-header">
<span class="sui-panel-title">Jane Cooper</span>
<button class="sui-panel-close sui-btn sui-btn-ghost sui-btn-sm" type="button"
aria-label="Close panel">×</button>
</div>
<div class="sui-panel-body" id="detailBody">...</div>
</div>
</div>
<script>
function showDetail(id) {
document.getElementById('detailBody').innerHTML = '...';
SUI.panel.open('#detailPanel');
}
</script>
Notification Centre (Panel + Cards)
Bell icon triggers a panel with a scrollable list of notification cards. Composes sui-panel + sui-card + sui-badge.
Main application content. Click the bell icon to open the notification panel.
<div class="sui-badge-overlay">
<button data-sui-panel="#notifPanel"
aria-label="Notifications, 3 unread"
aria-expanded="false">
<svg class="sui-icon" viewBox="0 0 24 24" aria-hidden="true"><use href="#sui-icon-nav-bell"/></svg>
</button>
<span class="sui-badge-count" aria-hidden="true">3</span>
</div>
<div class="sui-panel sui-panel-no-backdrop" id="notifPanel"
aria-label="Notifications" aria-hidden="true"
style="--sui-panel-width: 360px">
<div>
<div class="sui-panel-header">
<span class="sui-panel-title">Notifications</span>
<button class="sui-panel-close sui-btn sui-btn-ghost sui-btn-sm" type="button"
aria-label="Close">×</button>
</div>
<div class="sui-panel-body" style="padding: 0">
<div class="sui-card" style="border-radius:0; border-left:0;
border-right:0; border-top:0">
<div style="display:flex; gap:var(--sui-space-2)">
<div class="sui-avatar" data-initials="AB"
style="width:32px; height:32px; font-size:12px"></div>
<div>
<strong>Alice B.</strong> commented on your task
<div class="sui-meta sui-mt-1">2 min ago</div>
</div>
</div>
</div>
<!-- Repeat for each notification -->
</div>
</div>
</div>
Panel Polish
Canonical patterns for sui-panel internals: header with title + close, sticky footer, sizing, and a CRM-style detail drawer variant. Custom CSS: ~10 lines.
Edit Contact
jane@acme.com
+1 (555) 123-4567
| Owner | JC Jane Cooper |
| Stage | Negotiation |
| Deal Value | $240,000 |
| Close Date | Mar 15, 2026 |
| Probability | |
| Next Step | Send SOW |
| Tags | Enterprise Renewal |
Client wants multi-year pricing. Will send SOW with 3-year option and 15% discount. CFO approval needed on their side before signature.
Discovery call went well. They’re migrating from Competitor X. Main concerns: data migration timeline and API compatibility.
<style>
.record-detail { display: flex; flex-direction: column; height: 480px;
background: var(--sui-bg-card); border: 1px solid var(--sui-border);
border-radius: var(--sui-radius-md); overflow: hidden; }
.record-tabs { padding: 0 var(--sui-space-4);
border-bottom: 1px solid var(--sui-border); }
.record-panel { padding: var(--sui-space-4); flex: 1; overflow-y: auto; }
.record-footer { display: flex; gap: var(--sui-space-2);
justify-content: flex-end;
padding: var(--sui-space-3) var(--sui-space-4);
border-top: 1px solid var(--sui-border);
position: sticky; bottom: 0; background: var(--sui-bg-card); }
.record-activity { display: flex; gap: var(--sui-space-3);
padding: var(--sui-space-3) 0; border-bottom: 1px solid var(--sui-border); }
.record-activity:last-child { border-bottom: none; }
.record-activity-dot { width: 32px; height: 32px; border-radius: 50%;
display: flex; align-items: center; justify-content: center;
flex-shrink: 0; }
.record-note { padding: var(--sui-space-3);
background: var(--sui-bg-elevated);
border-radius: var(--sui-radius-md); margin-bottom: var(--sui-space-3); }
</style>
<!-- Wrap in data-sui-tabs to scope tab switching -->
<div class="record-detail" data-sui-tabs>
<div class="panel-header">
<span class="sui-avatar sui-avatar-sm">AC</span>
<div><strong>Acme Corp</strong>
<div class="sui-text-muted" style="font-size:var(--sui-text-meta);">
Enterprise</div></div>
<div style="flex:1;"></div>
<span class="sui-badge sui-badge-success">Active</span>
<button class="sui-btn sui-btn-ghost sui-btn-sm"
aria-label="Close">×</button>
</div>
<div class="record-tabs">
<nav class="sui-nav" role="tablist" aria-label="Record detail"
style="margin-bottom:-1px;">
<button class="sui-tab is-active" role="tab"
aria-selected="true" data-tab="overview">Overview</button>
<button class="sui-tab" role="tab"
aria-selected="false" data-tab="activity">Activity</button>
<button class="sui-tab" role="tab"
aria-selected="false" data-tab="notes">Notes</button>
</nav>
</div>
<!-- Overview: property table with badges, progress, chips -->
<div data-view="overview" class="record-panel is-active"
role="tabpanel" aria-label="Overview">
<table class="sui-table sui-table-dense"><tbody>
<tr><td class="sui-text-muted" style="width:35%;">Owner</td>
<td>Jane Cooper</td></tr>
<tr><td class="sui-text-muted">Stage</td>
<td><span class="sui-badge sui-badge-info">Negotiation</span></td></tr>
<tr><td class="sui-text-muted">Tags</td>
<td><span class="sui-chip">Enterprise</span>
<span class="sui-chip">Renewal</span></td></tr>
</tbody></table>
</div>
<!-- Activity: colour-coded timeline -->
<div data-view="activity" class="record-panel"
role="tabpanel" aria-label="Activity">
<div class="record-activity">
<div class="record-activity-dot"
style="background:var(--sui-success-soft);
color:var(--sui-success-strong);">✓</div>
<div><strong>Jane Cooper</strong> sent proposal
<div class="sui-meta">Today</div></div>
</div>
<!-- Repeat for each event -->
</div>
<!-- Notes: elevated cards -->
<div data-view="notes" class="record-panel"
role="tabpanel" aria-label="Notes">
<div class="record-note">
<strong>Jane Cooper</strong>
<span class="sui-meta">Feb 20</span>
<p class="sui-mt-2">Client wants multi-year pricing...</p>
</div>
</div>
<div class="record-footer">
<button class="sui-btn sui-btn-ghost sui-btn-sm">Edit</button>
<button class="sui-btn sui-btn-primary sui-btn-sm">Send SOW</button>
</div>
</div>
<style>
.panel-header { display: flex; align-items: center; gap: var(--sui-space-2);
padding: var(--sui-space-3) var(--sui-space-4); border-bottom: 1px solid var(--sui-border); }
.panel-header h3 { margin: 0; flex: 1; }
.panel-body { flex: 1; overflow-y: auto; padding: var(--sui-space-4); }
.panel-footer { display: flex; gap: var(--sui-space-2); justify-content: flex-end;
padding: var(--sui-space-3) var(--sui-space-4); border-top: 1px solid var(--sui-border); }
</style>
<!-- Use with SUI.panel.open('#my-panel') -->
<div class="sui-panel" id="my-panel" aria-label="Edit contact">
<div class="panel-header">
<h3>Edit Contact</h3>
<button class="sui-panel-close sui-btn sui-btn-ghost sui-btn-sm"
aria-label="Close">×</button>
</div>
<div class="panel-body">...</div>
<div class="panel-footer">
<button class="sui-btn sui-btn-ghost">Cancel</button>
<button class="sui-btn sui-btn-primary">Save</button>
</div>
</div>
<!-- Sizing: override --sui-panel-width in your CSS -->
<!-- .sui-panel { --sui-panel-width: 480px; } for wide panels -->
Panel Push Mode (Content Resize)
Instead of overlaying, the panel resizes main content. Toggle a CSS class on a wrapper to switch grid-template-columns. Custom CSS: ~8 lines.
This area resizes when the panel opens. Content reflows instead of being covered by an overlay.
<div class="app-layout" id="pushLayout">
<main class="app-main">Main content</main>
<aside class="app-push-panel">Panel content</aside>
</div>
<style>
.app-layout {
display: grid;
grid-template-columns: 1fr;
transition: grid-template-columns 250ms ease-out;
}
.app-layout.app-panel-open {
grid-template-columns: 1fr 400px;
}
.app-push-panel {
display: none;
border-left: 1px solid var(--sui-border);
overflow-y: auto;
}
.app-layout.app-panel-open .app-push-panel {
display: block;
}
</style>
<script>
document.getElementById('toggleBtn').addEventListener('click', () => {
document.getElementById('pushLayout').classList.toggle('app-panel-open');
});
</script>
Popover
Interactive content region: more than a tooltip, less than a dialog. Light (dropdown-style, click-outside dismissal) and heavy (focus-trap, explicit close) patterns. Custom CSS: ~12 lines.
<style>
.my-popover-wrap { position: relative; display: inline-block; }
.my-popover {
position: absolute; top: calc(100% + var(--sui-space-1)); left: 0;
z-index: var(--sui-z-dropdown); background: var(--sui-bg-card);
border: 1px solid var(--sui-border); border-radius: var(--sui-radius-md);
box-shadow: var(--sui-shadow-md); padding: var(--sui-space-3);
min-width: 240px; display: none;
}
.my-popover.is-open { display: block; }
</style>
<!-- Light popover (no focus trap, click-outside close) -->
<div class="my-popover-wrap">
<button class="sui-btn" aria-haspopup="true"
aria-expanded="false">Filter</button>
<div class="my-popover" role="group">...</div>
</div>
<!-- Heavy popover (focus trap, explicit close) -->
<div class="my-popover-wrap">
<button class="sui-btn" aria-haspopup="dialog">Quick add</button>
<div class="my-popover" role="dialog" aria-label="Quick add">
<!-- Form + Save/Cancel -->
</div>
</div>
Profile Page
User profile with avatar, metadata, stats, and activity feed. Composes from: sui-avatar-xl, sui-meta, sui-timeline, sui-badge, sui-card. Custom CSS: ~3 lines.
Recent Activity
<div class="recipe-profile-header">
<span class="sui-avatar sui-avatar-xl">AS</span>
<div>
<h2>Name</h2>
<div class="sui-meta">
<span>Location</span>
<span>Experience</span>
</div>
<div class="sui-flex sui-gap-2">
<span class="sui-chip sui-chip-info">Skill</span>
</div>
</div>
</div>
<div class="sui-timeline" role="feed">
<div class="sui-timeline-item" role="article">
<span class="sui-avatar sui-avatar-sm">AS</span>
<div class="sui-timeline-content">
<strong>Event title</strong>
<div class="sui-meta"><span>2 days ago</span></div>
</div>
</div>
</div>
<!-- Custom CSS (~3 lines) -->
<style>
.recipe-profile-header { display: flex; gap: var(--sui-space-4); align-items: flex-start; }
@media (max-width: 640px) { .recipe-profile-header { flex-direction: column; align-items: center; } }
</style>
Quill Integration
SUI-native rich text editing using Quill (Snow theme). Token-based overrides for light/dark mode, focus ring, SUI font family. This is an integration recipe — Quill is not part of SUI. You bring the library; SUI makes it match.
Live demo — toggle the theme to see dark mode. Full toolbar with formatting options.
Start typing here — this is a Quill rich text editor styled with SUI tokens.
What this recipe provides: token-based overrides for toolbar, editor surface, picker dropdowns, active states, and focus ring. Both themes. What you bring: Quill CSS + JS from CDN, your editor configuration.
<!-- Quill Integration Recipe — copy into your project -->
<!-- 1. Load Quill (CDN) -->
<link href="https://cdn.jsdelivr.net/npm/quill@2/dist/quill.snow.css"
rel="stylesheet" />
<script src="https://cdn.jsdelivr.net/npm/quill@2/dist/quill.js"
defer></script>
<!-- 2. SUI token overrides -->
<style>
.ql-toolbar.ql-snow {
border: 1px solid var(--sui-border);
border-radius: var(--sui-radius-md) var(--sui-radius-md) 0 0;
background: var(--sui-bg-elevated);
font-family: var(--sui-font-primary);
}
.ql-container.ql-snow {
border: 1px solid var(--sui-border);
border-top: none;
border-radius: 0 0 var(--sui-radius-md) var(--sui-radius-md);
font-family: var(--sui-font-primary);
color: var(--sui-text-primary);
background: var(--sui-bg-card);
}
.ql-editor { min-height: 120px; color: var(--sui-text-primary); }
.ql-editor.ql-blank::before {
color: var(--sui-text-muted);
font-style: normal;
}
.ql-snow .ql-stroke { stroke: var(--sui-text-secondary); }
.ql-snow .ql-fill { fill: var(--sui-text-secondary); }
.ql-snow .ql-picker { color: var(--sui-text-secondary); }
.ql-snow .ql-picker-options {
background: var(--sui-bg-card);
border-color: var(--sui-border);
border-radius: var(--sui-radius-sm);
box-shadow: var(--sui-shadow-lg);
}
.ql-snow.ql-toolbar button:hover .ql-stroke,
.ql-snow.ql-toolbar button.ql-active .ql-stroke {
stroke: var(--sui-blue-primary);
}
.ql-snow.ql-toolbar button:hover .ql-fill,
.ql-snow.ql-toolbar button.ql-active .ql-fill {
fill: var(--sui-blue-primary);
}
.ql-container.ql-snow:focus-within {
outline: 3px solid var(--sui-blue-soft);
outline-offset: 2px;
}
</style>
<!-- 3. Create the editor -->
<div id="myEditor">
<p>Start typing…</p>
</div>
<script>
new Quill('#myEditor', { theme: 'snow' });
</script>
Search Bar
Input + action button, non-wrapping. Composes from: sui-input, sui-btn, sui-flex. Custom CSS: ~4 lines.
<style>
.my-search-bar { display: flex; gap: var(--sui-space-2); }
.my-search-bar .sui-input { flex: 1; min-width: 0; }
/* Toolbar variant: constrain select width */
.my-search-bar .sui-select { width: auto; min-width: 120px; }
</style>
<!-- Simple search bar -->
<div class="my-search-bar">
<input class="sui-input" type="search" placeholder="Search..."
aria-label="Search contacts">
<button class="sui-btn sui-btn-primary" aria-label="Search">
<svg class="sui-icon" viewBox="0 0 24 24" aria-hidden="true"><use href="#sui-icon-act-search"/></svg>
</button>
</div>
<!-- Toolbar variant (select + search + action) -->
<div class="my-search-bar">
<select class="sui-select" aria-label="Filter type">...</select>
<input class="sui-input" type="search" placeholder="Search..." aria-label="Search">
<button class="sui-btn sui-btn-ghost sui-btn-sm">Clear</button>
<button class="sui-btn sui-btn-primary">Search</button>
</div>
Settings / Preferences
Vertical stack of labelled sections with controls. Composes from: sui-card, sui-toggle, sui-segmented, sui-input, sui-divider. Custom CSS: ~4 lines.
Notifications
Appearance
<div class="sui-card">
<h3>Notifications</h3>
<div class="recipe-settings-row">
<div>
<strong>Email notifications</strong>
<div class="sui-text-muted">Description text</div>
</div>
<label class="sui-toggle"><input type="checkbox" checked><span class="sui-toggle-track"></span></label>
</div>
<hr class="sui-divider">
<h3>Appearance</h3>
<div class="recipe-settings-row">
<div><strong>Theme</strong></div>
<div class="sui-segmented" role="radiogroup">
<button class="sui-segment" role="radio" aria-checked="true">Light</button>
<button class="sui-segment" role="radio" aria-checked="false">Dark</button>
</div>
</div>
</div>
<!-- Custom CSS (~4 lines) -->
<style>
.recipe-settings-row { display: flex; justify-content: space-between; align-items: center;
padding: var(--sui-space-3) 0; gap: var(--sui-space-3); }
.recipe-settings-row:not(:last-child) { border-bottom: 1px solid var(--sui-border); }
</style>
Split Pane / Master-Detail
Two-column list + detail layout. Collapses to stacked on mobile. Composes from: CSS grid, sui-card, sui-avatar, sui-badge. Custom CSS: ~8 lines per variant.
Enterprise licence renewal. Multi-year contract signed Q4. Expansion opportunity for Analytics add-on.
<div class="recipe-split recipe-split-40-60">
<div class="recipe-split-list">
<div class="recipe-split-list-item is-active">...</div>
<div class="recipe-split-list-item">...</div>
</div>
<div class="recipe-split-detail">...</div>
</div>
<!-- Custom CSS (~8 lines) -->
<style>
.recipe-split { display: grid; gap: 0; border: 1px solid var(--sui-border);
border-radius: var(--sui-radius-lg); overflow: hidden; min-height: 320px; }
.recipe-split-40-60 { grid-template-columns: 2fr 3fr; }
.recipe-split-list { border-right: 1px solid var(--sui-border); overflow-y: auto; }
.recipe-split-list-item { padding: var(--sui-space-3) var(--sui-space-4);
border-bottom: 1px solid var(--sui-border); cursor: pointer; }
.recipe-split-list-item:hover { background: var(--sui-bg-elevated); }
.recipe-split-list-item.is-active { background: var(--sui-blue-soft); }
.recipe-split-detail { padding: var(--sui-space-4); }
@media (max-width: 640px) {
.recipe-split { grid-template-columns: 1fr; }
.recipe-split-list { border-right: none; max-height: 200px; }
}
</style>
Stepper / Wizard
sui-stepper in the Components tab above.Tom Select Integration
SUI-native enhanced selects using Tom Select. Token-based overrides for light/dark mode, tagging, autocomplete. This is an integration recipe — Tom Select is not part of SUI. You bring the library; SUI makes it match.
Live demo — toggle the theme to see dark mode. Try keyboard navigation and typing to filter.
What this recipe provides: token-based overrides for the control, dropdown, tags, active/hover states, option group headers, and focus ring. Both themes. What you bring: Tom Select CSS + JS from CDN, your select configuration.
<!-- Tom Select Integration Recipe — copy into your project -->
<!-- 1. Load Tom Select (CDN) -->
<link href="https://cdn.jsdelivr.net/npm/tom-select@2.5.1/dist/css/tom-select.css"
rel="stylesheet" />
<script src="https://cdn.jsdelivr.net/npm/tom-select@2.5.1/dist/js/tom-select.complete.min.js"
defer></script>
<!-- 2. SUI token overrides -->
<style>
.ts-control {
border: 1px solid var(--sui-border);
border-radius: var(--sui-radius-sm);
background: var(--sui-bg-card);
color: var(--sui-text-primary);
font-family: var(--sui-font-primary);
min-height: 38px;
}
.full .ts-control { background: var(--sui-bg-card); }
.ts-control .item { color: var(--sui-text-primary); }
.focus .ts-control {
border-color: var(--sui-blue-primary);
box-shadow: 0 0 0 3px var(--sui-blue-soft);
}
.ts-wrapper.multi .ts-control > div {
background: var(--sui-blue-soft);
color: var(--sui-text-primary);
border-radius: var(--sui-radius-sm);
}
.ts-dropdown {
border: 1px solid var(--sui-border);
background: var(--sui-bg-card);
box-shadow: var(--sui-shadow-lg);
color: var(--sui-text-primary);
z-index: 9999;
}
.ts-dropdown .active {
background: var(--sui-bg-elevated);
color: var(--sui-text-primary);
}
.ts-dropdown [data-selectable] .highlight {
background: var(--sui-blue-soft);
}
</style>
<!-- 3. Use with a native select -->
<label class="sui-input-label">Choose a department</label>
<select id="mySelect">
<option value="">Select…</option>
<option value="eng">Engineering</option>
<option value="des">Design</option>
</select>
<script>
new TomSelect('#mySelect');
</script>