docs: UX-013 — design system documentation (#89827)

* docs: UX-013 — shared design system documentation

* docs: align design system docs with source tokens
This commit is contained in:
Val Alexander
2026-06-13 02:57:11 -05:00
committed by GitHub
parent 4640baa299
commit 751d3db1cc
5 changed files with 436 additions and 0 deletions

View File

@@ -0,0 +1,20 @@
# OpenClaw Design System
OpenClaw's visual language is a **dark-first, glass-surface system** built around a deep charcoal base (`#0e1015`), a punchy signature red accent (`#ff5c5c`), and layered frosted-glass surfaces that create depth without solid panels. Motion is crisp and purposeful — fast micro-interactions (100ms) with spring-loaded expansions. All interactive elements meet WCAG 2.1 AA contrast requirements on dark backgrounds.
## Contents
| File | What it covers |
| ---------------------------------------- | --------------------------------------------------------------------------------------- |
| [glass-surfaces.md](./glass-surfaces.md) | Two glass tiers, exact CSS values, no-solid-panels rule, `@supports` fallback |
| [color-tokens.md](./color-tokens.md) | All design tokens with values, usage, contrast ratios, and anti-patterns |
| [motion.md](./motion.md) | Duration scale, easing functions, `prefers-reduced-motion` pattern, animation inventory |
| [accessibility.md](./accessibility.md) | WCAG checklist: contrast, focus, tap targets, ARIA, skip link, focus trap |
## Guiding Principles
1. **Glass, not solid** — Surfaces use `backdrop-filter` blur + semi-transparent backgrounds. No flat opaque panels in the main chrome.
2. **Depth through layering** — The background scale (`--bg`, `--bg-accent`, `--bg-elevated`, `--bg-hover`, `--bg-muted`) communicates hierarchy without heavy borders.
3. **Accent with restraint** — Signature red (`--accent: #ff5c5c`) for primary actions only; teal (`--accent-2`) for secondary/status.
4. **Motion serves meaning** — Animations telegraph state changes; they never play for decoration alone.
5. **Accessible by default** — Every component ships with focus-visible styles, correct ARIA roles, and ≥4.5:1 contrast on text.

View File

@@ -0,0 +1,140 @@
# Accessibility
OpenClaw targets **WCAG 2.1 AA**. This checklist applies to every UI component — use it when building new components and during PR review.
---
## Contrast Minimums
| Context | Minimum Ratio | Notes |
| ----------------------------------------- | ---------------- | -------------------------------------------------- |
| Normal body text (< 18px / < 14px bold) | **4.5:1** | Use `--text` (#d4d4d8) or stronger on `--bg` |
| Large text (≥ 18px regular / ≥ 14px bold) | **3:1** | Headings in chat thread |
| UI component boundaries (inputs, buttons) | **3:1** | Border colours against adjacent background |
| Focus indicators | **3:1** (AA) | `--focus-ring` / `--focus-glow` already compliant |
| Placeholder text | Best-effort ≥3:1 | `--muted` (#838387) is ~5:1 on `--bg` — acceptable |
> Verify with [WebAIM Contrast Checker](https://webaim.org/resources/contrastchecker/) or browser DevTools accessibility panel.
---
## Checklist
### 1. Focus Visible
- [ ] All interactive elements have a visible `:focus-visible` style
- [ ] Use `--focus-ring` box-shadow pattern from `base.css` — do not remove the outline without replacing it
- [ ] Never use `outline: none` alone; replace with `box-shadow: var(--focus-ring)` and `outline: none`
```css
/* Correct pattern */
.my-button:focus-visible {
outline: none;
box-shadow: var(--focus-ring);
}
```
### 2. Minimum Tap / Click Target Size
- [ ] All interactive controls are ≥ **44×44px** on mobile / touch targets
- [ ] Icon-only buttons add invisible padding to reach 44px (`min-width: 44px; min-height: 44px`)
- [ ] Verify on the mobile breakpoint (`@media (max-width: 768px)`)
### 3. Skip Link
- [ ] A visually-hidden skip link (`#skip-to-main`) is present in the document root
- [ ] The link becomes visible on focus
- [ ] Target element has `tabindex="-1"` to accept programmatic focus
```html
<a id="skip-to-main" href="#main-content" class="skip-link">Skip to main content</a>
```
```css
.skip-link {
position: absolute;
left: -9999px;
top: 0;
z-index: 9999;
}
.skip-link:focus {
left: 1rem;
top: 1rem;
/* ... visible styles */
}
```
### 4. Focus Trap
- [ ] Modal dialogs trap focus inside while open (Tab/Shift+Tab cycle within the modal)
- [ ] Closing the modal returns focus to the trigger element
- [ ] Escape key closes the modal
### 5. ARIA Labels
- [ ] Icon-only buttons have `aria-label` describing the action (not the icon name)
- [ ] Progress bars use `role="progressbar"` with `aria-valuenow`, `aria-valuemin`, `aria-valuemax`
- [ ] Live regions updating async content use `aria-live="polite"` (or `"assertive"` for critical alerts)
- [ ] Decorative SVG icons have `aria-hidden="true"`
```html
<!-- Correct: icon button -->
<button aria-label="Send message">
<svg aria-hidden="true"><!-- ... --></svg>
</button>
<!-- Correct: progress bar -->
<div
role="progressbar"
aria-valuenow="42"
aria-valuemin="0"
aria-valuemax="100"
aria-label="Context window usage"
>
<div style="width: 42%"></div>
</div>
```
### 6. Tab Role Pattern
When building a tab interface:
- [ ] Tab container: `role="tablist"`
- [ ] Each tab: `role="tab"`, `aria-selected="true|false"`, `aria-controls="panel-id"`
- [ ] Each panel: `role="tabpanel"`, `id` matching `aria-controls`, `aria-labelledby` pointing to its tab
- [ ] Keyboard: Arrow keys move between tabs; Tab moves into the panel; Shift+Tab exits
```html
<div role="tablist" aria-label="Main navigation">
<button role="tab" aria-selected="true" aria-controls="chat-panel" id="chat-tab">Chat</button>
<button role="tab" aria-selected="false" aria-controls="settings-panel" id="settings-tab">
Settings
</button>
</div>
<div role="tabpanel" id="chat-panel" aria-labelledby="chat-tab"><!-- ... --></div>
<div role="tabpanel" id="settings-panel" aria-labelledby="settings-tab" hidden><!-- ... --></div>
```
### 7. Reduced Motion
- [ ] No animation plays without being suppressible via `prefers-reduced-motion`
- [ ] The global reset in `base.css` covers transitions — test with "Reduce motion" enabled in OS settings
- [ ] Infinite loaders (`shimmer`, spinners) have explicit `animation: none` in reduced-motion context
### 8. Semantic HTML
- [ ] Use native elements (`<button>`, `<a>`, `<input>`) before adding ARIA to `<div>` / `<span>`
- [ ] Headings (`<h1>``<h6>`) reflect document hierarchy — do not skip levels
- [ ] Lists of items use `<ul>` / `<ol>` + `<li>`, not chains of `<div>`
---
## Testing Tools
| Tool | Purpose |
| ------------------------------------------------------- | ----------------------------------- |
| Chrome DevTools → Accessibility tab | Inspect ARIA tree, contrast |
| axe DevTools (browser extension) | Automated WCAG audit |
| macOS VoiceOver (`Cmd+F5`) | Screen reader smoke test |
| `prefers-reduced-motion: reduce` (DevTools → Rendering) | Verify animation suppression |
| Keyboard-only navigation | Tab through entire UI without mouse |

View File

@@ -0,0 +1,91 @@
# Color Tokens
All tokens are defined in `ui/src/styles/base.css` under `:root` (dark mode default) and `:root[data-theme-mode="light"]` (light override). Theme families may override accent tokens while keeping shared surface tokens.
> Contrast ratios are measured against `--bg` (`#0e1015`) in dark mode using WCAG relative luminance formula. AA requires ≥4.5:1 for normal text, ≥3:1 for large text and UI components.
---
## Background Scale
| Token | Dark Value | Light Value | Use | Don't |
| --------------- | ---------- | ----------- | ----------------------------- | ------------------------------ |
| `--bg` | `#0e1015` | `#f8f9fa` | Page root, deepest layer | Never use on elevated surfaces |
| `--bg-accent` | `#13151b` | `#f1f3f5` | Sidebar, secondary panels | Not for interactive card hover |
| `--bg-elevated` | `#191c24` | `#ffffff` | Raised panels, modals | Not for inline elements |
| `--bg-hover` | `#1f2330` | `#eceef0` | List item hover state | Not for default state |
| `--bg-muted` | `#1f2330` | `#eceef0` | Subtle fills, disabled states | Not for focus states |
## Surface / Card
| Token | Dark Value | Light Value | Use | Don't |
| ---------------------- | ------------------------ | ------------------ | ----------------------------- | --------------- |
| `--card` | `#161920` | `#ffffff` | Card backgrounds, composer | Avoid as border |
| `--card-foreground` | `#f0f0f2` | `#1a1a1e` | Text on cards | — |
| `--card-highlight` | `rgba(255,255,255,0.04)` | `rgba(0,0,0,0.02)` | Inner highlight on hover | Not for text |
| `--popover` | `#191c24` | `#ffffff` | Dropdown, tooltip backgrounds | — |
| `--popover-foreground` | `#f0f0f2` | `#1a1a1e` | Text inside popovers | — |
## Text
| Token | Dark Value | Contrast on `--bg` | Use |
| ---------------- | ---------- | ------------------ | ---------------------------------------------------- |
| `--text` | `#d4d4d8` | ~12.9:1 ✅ | Body copy, labels |
| `--text-strong` | `#f4f4f5` | ~17.3:1 ✅ | Headings, emphasis |
| `--muted` | `#838387` | ~5.0:1 ✅ | Placeholder, metadata |
| `--muted-strong` | `#75757d` | ~4.2:1 | Secondary text, captions; avoid for normal body text |
## Accent (Primary — Red)
| Token | Value | Use | Don't |
| ----------------- | --------------------- | ---------------------------------------------- | ---------------------------------------- |
| `--accent` | `#ff5c5c` | Primary CTA, send button, active tab indicator | Don't use for large filled backgrounds |
| `--accent-hover` | `#ff7070` | Hover state of accent elements | — |
| `--accent-muted` | `#ff5c5c` | Same as accent (aliased) | — |
| `--accent-subtle` | `rgba(255,92,92,0.1)` | Badge backgrounds, tinted fills | Not for text on dark bg (fails contrast) |
| `--accent-glow` | `rgba(255,92,92,0.2)` | Focus rings, glow effects | Not as background |
| `--primary` | `#ff5c5c` | Component library `primary` alias | — |
## Accent 2 (Teal)
| Token | Value | Use |
| ------------------- | ---------------------- | ----------------------------------------- |
| `--accent-2` | `#14b8a6` | Success-adjacent status, secondary badges |
| `--accent-2-muted` | `rgba(20,184,166,0.7)` | Subtle teal fills |
| `--accent-2-subtle` | `rgba(20,184,166,0.1)` | Tinted teal background |
## Semantic
| Token | Dark Value | Light Value | Contrast on `--bg` | Use |
| --------------- | ---------- | ----------- | ------------------ | --------------------------------------------- |
| `--ok` | `#22c55e` | `#15803d` | ~8.4:1 ✅ | Success states, token meter low |
| `--warn` | `#f59e0b` | `#d97706` | ~8.9:1 ✅ | Warnings, degraded states |
| `--danger` | `#ef4444` | `#dc2626` | ~5.1:1 ✅ | Errors, destructive actions, token meter high |
| `--info` | `#3b82f6` | `#2563eb` | ~5.2:1 ✅ | Informational, token meter mid |
| `--destructive` | `#ef4444` | — | ~5.1:1 ✅ | Destructive action labels |
## Border
| Token | Value | Use |
| ----------------- | --------- | -------------------------------- |
| `--border` | `#1e2028` | Default subtle borders, dividers |
| `--border-strong` | `#2e3040` | Active/focused borders |
| `--border-hover` | `#3e4050` | Hover-state borders |
## Focus
| Token | Value | Use |
| -------------- | --------------------------------------------------------------------------------- | ------------------------------ |
| `--ring` | `#ff5c5c` | Focus ring colour |
| `--focus-ring` | `0 0 0 2px var(--bg), 0 0 0 3px color-mix(in srgb, var(--ring) 80%, transparent)` | Standard focus ring box-shadow |
| `--focus-glow` | `0 0 0 2px var(--bg), 0 0 0 3px var(--ring), 0 0 16px var(--accent-glow)` | Elevated interactive elements |
---
## Anti-Patterns
- ❌ Hardcoded hex colours in component CSS — always use tokens
-`--accent-subtle` as text colour — fails contrast on dark backgrounds
- ❌ Mixing `--ok` and `--accent-2` for "green success" — use `--ok` only
- ❌ Using `--danger` for non-error states (e.g. "hot feature") — reserve for errors and destructive actions
-`--muted-strong` for normal body text — below 4.5:1 on dark `--bg`; use `--text` instead

View File

@@ -0,0 +1,94 @@
# Glass Surfaces
OpenClaw uses two glass tiers to establish visual hierarchy. **Never use flat, fully-opaque backgrounds** in the main chrome — glass creates the lived-in depth that defines the visual language.
---
## Tier 1 — Navigation Chrome
Used for: top nav, sidebar, persistent chrome overlays.
```css
/* Exact values from ui/src/styles/layout.css — .topbar */
background: color-mix(in srgb, var(--bg) 82%, transparent);
backdrop-filter: blur(12px) saturate(1.6);
-webkit-backdrop-filter: blur(12px) saturate(1.6);
```
**Why these values:** the topbar mixes the root background with transparency so it stays readable while still refracting content behind it. `saturate(1.6)` brings out colour from blurred layers.
---
## Tier 2 — Card / Input Surfaces
Used for: chat composer input box, modals, popovers, settings cards, dropdowns.
```css
/* Exact values from ui/src/styles/chat/layout.css — .agent-chat__input */
background: var(--card); /* #161920 */
border: 1px solid var(--border); /* #1e2028 */
border-radius: var(--radius-lg); /* 14px */
@supports (backdrop-filter: blur(1px)) {
backdrop-filter: blur(12px) saturate(1.6);
-webkit-backdrop-filter: blur(12px) saturate(1.6);
}
```
Popovers and floating overlays reduce blur slightly:
```css
/* Exact values from source */
backdrop-filter: blur(
8px
); /* components.css action menu, cron-quick-create.css modal backdrop, workboard.css modal backdrop */
backdrop-filter: blur(10px); /* skill-workshop.css revision dialog, dreams.css media lightbox */
backdrop-filter: blur(14px); /* components.css markdown preview dialog backdrop */
```
---
## The No-Solid-Panels Rule
> **Do not** add `background: var(--bg)` or any fully-opaque fill to a surface that appears above other content.
Use the `@supports` fallback pattern below instead. Browsers that lack `backdrop-filter` fall back gracefully to the semi-opaque base without breaking layout.
### `@supports` Fallback Pattern
```css
.my-surface {
/* Fallback for older browsers / Firefox without backdrop-filter */
background: rgba(14, 16, 21, 0.92);
}
@supports (backdrop-filter: blur(1px)) {
.my-surface {
background: rgba(14, 16, 21, 0.75);
backdrop-filter: blur(12px) saturate(1.6);
-webkit-backdrop-filter: blur(12px) saturate(1.6);
}
}
```
---
## When to Use Each Tier
| Context | Tier | Blur |
| ---------------------- | ---- | ---- |
| Top nav / sidebar rail | 1 | 12px |
| Chat composer | 2 | 12px |
| Modal / dialog | 2 | 10px |
| Dropdown / popover | 2 | 8px |
| Tooltip | 2 | 14px |
| Workboard card | 2 | 10px |
---
## Anti-Patterns
-`background: var(--bg)` on a surface element (use glass instead)
-`backdrop-filter` without `-webkit-backdrop-filter` (Safari still requires prefix)
- ❌ Nesting glass surfaces more than 2 levels (causes noticeable blur stacking on macOS)
- ❌ Using `blur(>16px)` outside of modal overlays (hurts performance on mid-tier devices)

View File

@@ -0,0 +1,91 @@
# Motion
OpenClaw uses a three-step duration scale with purpose-matched easing functions. Every animation should serve a functional goal — transitions that exist only for aesthetics add cognitive load without benefit.
---
## Duration Scale
Defined in `ui/src/styles/base.css`:
| Token | Value | Use |
| ------------------- | ------- | --------------------------------------------------------- |
| `--duration-fast` | `100ms` | Micro-interactions: hover colour, focus ring, icon swap |
| `--duration-normal` | `180ms` | Standard transitions: menu open, tab switch, input expand |
| `--duration-slow` | `300ms` | Page-level: sheet slide-in, modal fade, skeleton reveal |
Non-token durations in use (document when adding new ones):
| Context | Value | File |
| ----------------------- | ------------------------------ | ----------------- |
| Theme circle transition | `400ms` | `base.css` |
| Shimmer animation | `1500ms` | `base.css` |
| Composer border/shadow | `var(--duration-fast)` = 100ms | `chat/layout.css` |
---
## Easing Functions
Defined in `ui/src/styles/base.css`:
| Token | Curve | Use |
| --------------- | ----------------------------------- | ------------------------------------------------------- |
| `--ease-out` | `cubic-bezier(0.16, 1, 0.3, 1)` | Most enter/expand transitions — fast start, smooth land |
| `--ease-in-out` | `cubic-bezier(0.4, 0, 0.2, 1)` | Elements that travel across screen (slides, drawers) |
| `--ease-spring` | `cubic-bezier(0.34, 1.56, 0.64, 1)` | Playful/tactile: button press, badge pop, icon bounce |
**Default rule of thumb:** Use `--ease-out` unless the element explicitly moves from point A to point B (use `--ease-in-out`) or needs a bouncy feel (use `--ease-spring`).
---
## `prefers-reduced-motion` Pattern
Every animation or transition **must** be suppressed when the user has requested reduced motion. Use the global reset already present in `base.css` — do not add per-component overrides unless you need to preserve a non-animated state change (e.g. instant opacity change is acceptable, instant position snap is acceptable).
```css
/* Already in base.css — covers all transitions globally */
@media (prefers-reduced-motion: reduce) {
*,
*::before,
*::after {
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.01ms !important;
scroll-behavior: auto !important;
}
}
```
For components with complex animation state (e.g. shimmer skeletons that use `animation-iteration-count: infinite`), add an explicit guard:
```css
@media (prefers-reduced-motion: reduce) {
.my-shimmer {
animation: none;
/* Show a static placeholder instead */
opacity: 0.5;
}
}
```
---
## Animation Inventory
| Name | File | Duration | Purpose |
| ------------------------- | ----------------- | ------------------------- | ----------------------------------------------- |
| `shimmer` | `base.css` | 1500ms, infinite | Skeleton loading placeholders |
| `theme-circle-transition` | `base.css` | 400ms, `--ease-out` | Dark/light mode circle wipe |
| Composer border/shadow | `chat/layout.css` | 100ms (`--duration-fast`) | Focus ring on input area |
| Workboard card glass | `workboard.css` | — | Static (no animation) |
| Dreams diary reveal | `dreams.css` | 1.4s, cubic-bezier | Entry reveal keyframe with blur-to-clear effect |
---
## Anti-Patterns
- ❌ Adding new keyframe animations without a `prefers-reduced-motion` suppression
- ❌ Using `animation-iteration-count: infinite` outside of skeleton loaders
- ❌ Duration > 500ms for UI chrome elements (feels sluggish)
-`linear` easing for enter/exit — always use a curve from the token set
- ❌ CSS transitions on `transform` and `opacity` simultaneously with `filter` — causes GPU layer explosion on mobile