@ryanhelsing/ry-ui
Version:
Framework-agnostic, Light DOM web components. CSS is the source of truth.
552 lines (428 loc) • 18.1 kB
Markdown
# ry-ui
Framework-agnostic, Light DOM web components. Zero dependencies. CSS is the source of truth.
## Setup (2 lines)
```html
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@ryanhelsing/ry-ui/dist/css/ry-ui.css">
<script type="module" src="https://cdn.jsdelivr.net/npm/@ryanhelsing/ry-ui/dist/ry-ui.js"></script>
```
```bash
# or npm
npm install @ryanhelsing/ry-ui
```
Set theme: `<html data-ry-theme="light">` — `light` | `dark` | omit for OS preference
Set body: `<body data-ry-reset style="background: var(--ry-color-bg); color: var(--ry-color-text);">`
## DON'T / DO
DON'T write flexbox/grid CSS for page layout.
DO: `<ry-page><ry-header>H</ry-header><ry-main>M</ry-main><ry-footer>F</ry-footer></ry-page>`
DON'T write a custom modal with backdrop, focus trap, escape handling.
DO: `<ry-button modal="m">Open</ry-button><ry-modal id="m" title="T">Content</ry-modal>`
DON'T write a slide-out drawer with CSS transforms.
DO: `<ry-button drawer="d">Open</ry-button><ry-drawer id="d" side="left">Content</ry-drawer>`
DON'T write tab switching logic or CSS.
DO: `<ry-tabs><ry-tab title="A" active>A</ry-tab><ry-tab title="B">B</ry-tab></ry-tabs>`
DON'T write a custom select dropdown with keyboard navigation.
DO: `<ry-select placeholder="Pick"><ry-option value="a">A</ry-option></ry-select>`
DON'T write CSS variables for colors, spacing, shadows.
DO: Use `--ry-color-*`, `--ry-space-*`, `--ry-radius-*`, `--ry-shadow-*` tokens.
DON'T write button styles with hover/active/focus states.
DO: `<ry-button variant="primary">Click</ry-button>`
DON'T write toast/notification CSS and JS.
DO: `RyToast.success('Saved!')` / `RyToast.error('Failed')`
DON'T write accordion expand/collapse logic.
DO: `<ry-accordion><ry-accordion-item title="Q" open>A</ry-accordion-item></ry-accordion>`
DON'T write a toggle switch from scratch.
DO: `<ry-switch name="notify" checked></ry-switch>`
## Full Page Template
```html
<!DOCTYPE html>
<html lang="en" data-ry-theme="light">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>My App</title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@ryanhelsing/ry-ui/dist/css/ry-ui.css">
</head>
<body data-ry-reset style="background: var(--ry-color-bg); color: var(--ry-color-text);">
<ry-page>
<ry-header sticky>
<ry-cluster>
<strong>My App</strong>
<ry-nav>
<a href="/" aria-current="page">Home</a>
<a href="/about">About</a>
</ry-nav>
</ry-cluster>
<ry-actions>
<ry-theme-toggle themes="light,dark"></ry-theme-toggle>
</ry-actions>
</ry-header>
<ry-main>
<ry-section>
<h1>Hello World</h1>
<p>Your content here.</p>
</ry-section>
</ry-main>
<ry-footer>Built with ry-ui</ry-footer>
</ry-page>
<script type="module" src="https://cdn.jsdelivr.net/npm/@ryanhelsing/ry-ui/dist/ry-ui.js"></script>
</body>
</html>
```
## Clean Syntax
Wrap markup in `<ry>` to use unprefixed tags:
```html
<ry>
<accordion>
<accordion-item title="FAQ" open>No ry- prefix needed.</accordion-item>
</accordion>
</ry>
```
---
## Component Catalog
### Layout (CSS-only, no JS)
| Component | Attributes | Description |
|-----------|-----------|-------------|
| `<ry-page>` | — | Root page container, flex column, min-height 100dvh |
| `<ry-header>` | `sticky` | Flex row, space-between. `sticky` pins to top |
| `<ry-main>` | — | Content area, max-width 1200px, centered |
| `<ry-footer>` | — | Footer with border-top |
| `<ry-section>` | — | Block section with bottom margin |
| `<ry-grid>` | `cols="1-6\|auto-fit\|auto-fill"`, `cols-sm`, `cols-md`, `cols-lg` | CSS grid. [Details](docs/components/layout.md) |
| `<ry-stack>` | `gap="sm\|md\|lg\|xl"` | Vertical flex column |
| `<ry-cluster>` | `gap="sm\|md\|lg"` | Horizontal flex row, wraps |
| `<ry-split>` | `resizable`, `persist="key"` | Two-column with drag resize. [Details](docs/components/layout.md) |
| `<ry-center>` | — | Flex center (both axes) |
| `<ry-nav>` | — | Horizontal nav links. Active: `a[aria-current="page"]` |
| `<ry-logo>` | — | Inline-flex, bold text |
| `<ry-actions>` | — | Flex row for action buttons |
| `<ry-divider>` | `vertical` | Horizontal line; `vertical` for inline separator |
| `<ry-aside>` | — | Sidebar content area |
### Interactive Components
| Component | Key Attributes | Events |
|-----------|---------------|--------|
| `<ry-button>` | `variant="primary\|secondary\|outline\|ghost\|danger\|accent"`, `size="sm\|lg"`, `disabled`, `pressed`, `modal="id"`, `drawer="id"` | `ry:click` |
| `<ry-toggle-button>` | `pressed`, `name`, `value`, `size`, `icon`, `disabled` | `ry:change` `{pressed, value}` |
| `<ry-modal>` | `id`, `title` | Trigger: `<ry-button modal="id">`. [Details](docs/components/modal.md) |
| `<ry-drawer>` | `id`, `position="left\|right"`, `size` | Trigger: `<ry-button drawer="id">`. [Details](docs/components/drawer.md) |
| `<ry-accordion>` | — | Container for accordion-items. [Details](docs/components/accordion.md) |
| `<ry-accordion-item>` | `title`, `open` | Collapsible section |
| `<ry-tabs>` | — | Children: `<ry-tab title="..." active>`. [Details](docs/components/tabs.md) |
| `<ry-dropdown>` | — | [Details](docs/components/dropdown.md) |
| `<ry-select>` | `placeholder`, `name`, `value`, `disabled` | `ry:change` `{value}`. Children: `<ry-option>` |
| `<ry-combobox>` | `placeholder`, `name`, `value`, `disabled` | `ry:change` `{value, label}`, `ry:input` — searchable dropdown |
| `<ry-switch>` | `checked`, `disabled`, `name` | `ry:change` `{value, label}` — value is `"true"`/`"false"` string |
| `<ry-tooltip>` | `content`, `position` | [Details](docs/components/tooltip.md) |
| `<ry-toast>` | — | `RyToast.success()`, `.error()`, `.warning()`, `.info()`. [Details](docs/components/toast.md) |
| `<ry-slider>` | `min`, `max`, `step`, `value`, `color`, `disabled` | `ry:change` `{value}`. [Details](docs/components/slider.md) |
| `<ry-knob>` | `min`, `max`, `step`, `value`, `color`, `size` | `ry:change` `{value}`. [Details](docs/components/knob.md) |
| `<ry-number-select>` | `min`, `max`, `step`, `value`, `arrows`, `prefix`, `suffix` | `ry:change` `{value}`. [Details](docs/components/number-select.md) |
| `<ry-color-picker>` | `value`, `format` | `ry:change` `{value}`. [Details](docs/components/color.md) |
| `<ry-color-input>` | `value`, `format` | `ry:change` `{value}` |
| `<ry-gradient-picker>` | `value` | `ry:change` `{value}` |
| `<ry-tree>` | `data` (JSON) | `ry:select`, `ry:move`. [Details](docs/components/tree.md) |
| `<ry-tag>` | `removable` | `ry:remove` |
| `<ry-tag-input>` | `name`, `value`, `placeholder` | `ry:change` `{tags}` |
| `<ry-carousel>` | `autoplay`, `interval` | `ry:change` `{index}` |
| `<ry-theme-toggle>` | `themes="light,dark"` | Cycles through themes |
| `<ry-theme-panel>` | `theme`, `mode` | Floating theme/mode selector. Persists to localStorage |
| `<ry-testimonial>` | `stars`, `avatar`, `name`, `role` | Quote card. Plain text children = quote |
### Display Components
| Component | Key Attributes | Description |
|-----------|---------------|-------------|
| `<ry-card>` | `interactive`, `href` | Card container. `interactive` adds click/keyboard. `href` navigates |
| `<ry-badge>` | `variant="primary\|success\|warning\|danger\|accent"` | Pill badge. Custom: `style="--ry-badge-color: #8B5CF6"` |
| `<ry-alert>` | `type="info\|success\|warning\|danger"` | Alert box |
| `<ry-field>` | `label`, `error`, `hint` | Form field wrapper. [Details](docs/components/forms.md) |
| `<ry-icon>` | `name` | SVG icon from registry |
| `<ry-code>` | `language`, `title` | Syntax-highlighted code block |
| `<ry-hero>` | `size="sm\|lg"`, `full-bleed`, `align="left"` | Marketing hero section |
| `<ry-stat>` | `value`, `label`, `size="sm\|lg"` | Stat card |
| `<ry-feature>` | `icon` | Feature card with icon |
| `<ry-feature-grid>` | `cols="2\|3\|4"` | Responsive grid for feature cards |
| `<ry-pricing>` | — | Container for pricing cards |
| `<ry-pricing-card>` | `title`, `price`, `featured` | Pricing tier. `featured` scales up with bold border |
| `<ry-heading>` | `size="sm\|lg"`, `align="center\|right"`, `divider`, `sub` | Section heading with optional subtitle |
---
## Patterns
### Grid
```html
<!-- Fixed columns (auto-responsive: 3-6 → 2 at ≤1024px → 1 at ≤640px) -->
<ry-grid cols="3">...</ry-grid>
<!-- Explicit per-breakpoint -->
<ry-grid cols="5" cols-md="3" cols-sm="1">...</ry-grid>
<!-- Fluid auto-fit -->
<ry-grid cols="auto-fit">...</ry-grid>
<ry-grid cols="auto-fit" style="--ry-grid-min: 240px">...</ry-grid>
```
### Split Layout
```html
<ry-split resizable persist="sidebar" style="--ry-split-width: 400px">
<div>Main content</div>
<div>Resizable sidebar — drag, arrow keys, double-click to reset</div>
</ry-split>
```
CSS vars: `--ry-split-width`, `--ry-split-min-width`, `--ry-split-max-width`
Keyboard: Arrow (±10px), Shift+Arrow (±50px), Home/End, double-click reset
Event: `ry:resize` `{ width }`
### Forms
```html
<ry-field label="Email" hint="We'll never share your email">
<input type="email" placeholder="you@example.com">
</ry-field>
<ry-field label="Password" error="Must be at least 8 characters">
<input type="password">
</ry-field>
```
Error hides hint automatically. Set `error=""` to clear.
### Button Variants
```html
<ry-button>Default</ry-button>
<ry-button variant="primary">Primary</ry-button>
<ry-button variant="secondary">Secondary</ry-button>
<ry-button variant="outline">Outline</ry-button>
<ry-button variant="ghost">Ghost</ry-button>
<ry-button variant="danger">Danger</ry-button>
<ry-button variant="accent">Accent</ry-button>
<ry-button size="sm">Small</ry-button>
<ry-button size="lg">Large</ry-button>
```
### Modal & Drawer
```html
<ry-button modal="confirm">Open Modal</ry-button>
<ry-modal id="confirm" title="Confirm Action">
<p>Are you sure?</p>
<ry-cluster>
<ry-button variant="danger">Delete</ry-button>
<ry-button variant="ghost">Cancel</ry-button>
</ry-cluster>
</ry-modal>
<ry-button drawer="settings">Settings</ry-button>
<ry-drawer id="settings" position="right" size="400px">
<h3>Settings</h3>
</ry-drawer>
```
### Nav Bar
```html
<ry-header sticky>
<ry-cluster>
<ry-logo>MyApp</ry-logo>
<ry-divider vertical></ry-divider>
<ry-nav>
<a href="/" aria-current="page">Home</a>
<a href="/docs">Docs</a>
</ry-nav>
</ry-cluster>
<ry-actions>
<ry-button variant="ghost" size="sm">Login</ry-button>
<ry-button size="sm">Sign Up</ry-button>
</ry-actions>
</ry-header>
```
### Hero
```html
<ry-hero>
<h1>Build faster with ry-ui</h1>
<p>Framework-agnostic components for any app.</p>
<ry-cluster>
<ry-button size="lg">Get Started</ry-button>
<ry-button variant="outline" size="lg">View Docs</ry-button>
</ry-cluster>
</ry-hero>
```
### Pricing
```html
<ry-pricing>
<ry-pricing-card title="Free" price="$0/mo">
<ul class="ry-check-list">
<li>3 projects</li>
<li>Basic support</li>
</ul>
<ry-button variant="outline">Get Started</ry-button>
</ry-pricing-card>
<ry-pricing-card featured title="Pro" price="$19/mo">
<ul class="ry-check-list">
<li>Unlimited projects</li>
<li>Priority support</li>
</ul>
<ry-button>Upgrade</ry-button>
</ry-pricing-card>
</ry-pricing>
```
### Interactive Card Grid
```html
<ry-grid cols="3">
<ry-card interactive href="/feature-a">
<h3>Feature A</h3>
<p>Description</p>
</ry-card>
</ry-grid>
```
### Dropdown Menu
```html
<ry-dropdown>
<ry-button>Actions</ry-button>
<ry-menu>
<ry-menu-item>Edit</ry-menu-item>
<ry-menu-item>Duplicate</ry-menu-item>
<ry-menu-item>Delete</ry-menu-item>
</ry-menu>
</ry-dropdown>
```
---
## Events
All events prefixed with `ry:`:
```javascript
element.addEventListener('ry:change', (e) => console.log(e.detail));
element.addEventListener('ry:open', () => {});
element.addEventListener('ry:close', () => {});
element.addEventListener('ry:click', () => {});
```
### Programmatic Control
```javascript
document.querySelector('ry-modal').open();
document.querySelector('ry-modal').close();
document.querySelector('ry-drawer').toggle();
document.querySelector('ry-select').value = 'new-value';
```
---
## CSS Token System
All visual properties use CSS custom properties. Override in your own CSS to customize.
### Colors
| Token | Purpose |
|-------|---------|
| `--ry-color-primary` / `-hover` / `-active` | Primary action color |
| `--ry-color-secondary` / `-hover` / `-active` | Secondary muted color |
| `--ry-color-accent` / `-hover` / `-active` | Accent/highlight color |
| `--ry-color-success` | Green for positive states |
| `--ry-color-warning` | Yellow/orange for caution |
| `--ry-color-danger` / `-hover` | Red for destructive actions |
| `--ry-color-info` | Blue for informational |
| `--ry-color-text` / `-muted` / `-inverse` | Text colors |
| `--ry-color-bg` / `-subtle` / `-muted` | Background colors |
| `--ry-color-border` / `-strong` | Border colors |
| `--ry-color-overlay` | Modal/drawer backdrop |
Each color also has `-bg` and `-text` variants for alert/badge backgrounds.
### Spacing
`--ry-space-{0,1,2,3,4,5,6,8,10,12,16,20}` — 0 to 5rem
### Typography
| Token | Value |
|-------|-------|
| `--ry-font-sans` | system-ui stack |
| `--ry-font-mono` | ui-monospace stack |
| `--ry-text-{xs,sm,base,lg,xl,2xl,3xl,4xl}` | 0.75rem to 2.25rem |
| `--ry-font-{normal,medium,semibold,bold}` | 400 to 700 |
### Borders & Shadows
| Token | Value |
|-------|-------|
| `--ry-radius-{none,sm,md,lg,xl,2xl,full}` | 0 to 9999px |
| `--ry-shadow-{sm,md,lg,xl}` | Elevation shadows |
| `--ry-border-width` | 1px |
### Transitions
| Token | Value |
|-------|-------|
| `--ry-duration-{fast,normal,slow}` | 100ms, 200ms, 300ms |
| `--ry-ease` / `-in` / `-out` | Cubic bezier easing |
### Z-Index
| Token | Value |
|-------|-------|
| `--ry-z-dropdown` | 1000 |
| `--ry-z-sticky` | 1020 |
| `--ry-z-modal-backdrop` | 1040 |
| `--ry-z-modal` | 1050 |
| `--ry-z-tooltip` | 1070 |
| `--ry-z-toast` | 1080 |
---
## Theming
Three CSS layers, loaded in order:
1. **ry-tokens.css** — CSS custom properties (colors, spacing, etc.)
2. **ry-structure.css** — Pure layout (no colors) + Preflight reset via `data-ry-reset`
3. **ry-theme.css** — All visual styling (colors, shadows, borders)
### Preflight Reset
Add `data-ry-reset` to `<body>` to normalize all elements (Tailwind Preflight-equivalent):
```html
<body data-ry-reset>
```
Resets box-sizing, margins, padding, form element inheritance, media elements, lists, tables. Without it, only ry-* components are reset.
### Custom Theme
Load structure-only and bring your own:
```html
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@ryanhelsing/ry-ui/dist/css/ry-structure.css">
<link rel="stylesheet" href="your-tokens.css">
<link rel="stylesheet" href="your-theme.css">
```
### Override Tokens
No build step needed:
```css
:root {
--ry-color-primary: oklch(0.541 0.218 293);
--ry-color-primary-hover: oklch(0.491 0.234 292);
--ry-radius-md: 0;
}
```
### Themes & Modes
Theme and mode are independent:
```html
<html data-ry-theme="ocean" data-ry-mode="dark">
```
Themes: `default`, `ocean`, `none` (structure only)
Modes: `auto` (OS preference), `light`, `dark`
Use `<ry-theme-panel>` for an interactive floating selector.
---
## TypeScript
```ts
import { RyElement, RyButton, RyToast } from '@ryanhelsing/ry-ui';
RyToast.success('Saved!');
document.querySelector('ry-select')?.addEventListener('ry:change', (e: CustomEvent) => {
console.log(e.detail.value);
});
// Extend components
class MyWidget extends RyElement {
setup() {
this.on(this, 'click', () => this.emit('activate'));
}
}
```
## Icon Registry
Built-in: `settings`, `heart`, `star`, `chevron-up`, `chevron-down`, `chevron-left`, `chevron-right`, `check`, `x`, `plus`, `minus`, `search`, `sun`, `moon`, `copy`, `trash`, `edit`, `eye`, `folder`, `file`, `drag`
```ts
import { registerIcon, registerIcons } from '@ryanhelsing/ry-ui';
registerIcon('custom', '<svg>...</svg>');
registerIcons({ 'app-logo': '<svg>...</svg>' });
```
## Vendoring
Copy into your project instead of using CDN:
```bash
npm pack @ryanhelsing/ry-ui && tar -xf ryanhelsing-ry-ui-*.tgz
cp -r package/dist ./vendor/ry-ui && rm -rf package ryanhelsing-ry-ui-*.tgz
```
```html
<link rel="stylesheet" href="/vendor/ry-ui/css/ry-ui.css">
<script type="module" src="/vendor/ry-ui/ry-ui.js"></script>
```
---
## Detailed Docs
Per-component docs with full attributes, events, and examples:
| Doc | Components |
|-----|-----------|
| [layout](docs/components/layout.md) | page, header, main, footer, section, grid, stack, cluster, split, center, card, nav, divider |
| [button](docs/components/button.md) | button, toggle-button |
| [accordion](docs/components/accordion.md) | accordion, accordion-item |
| [tabs](docs/components/tabs.md) | tabs, tab |
| [modal](docs/components/modal.md) | modal |
| [drawer](docs/components/drawer.md) | drawer |
| [dropdown](docs/components/dropdown.md) | dropdown, menu, menu-item |
| [tooltip](docs/components/tooltip.md) | tooltip |
| [toast](docs/components/toast.md) | toast |
| [forms](docs/components/forms.md) | field, select, switch |
| [slider](docs/components/slider.md) | slider |
| [knob](docs/components/knob.md) | knob |
| [number-select](docs/components/number-select.md) | number-select |
| [color](docs/components/color.md) | color-picker, color-input, gradient-picker |
| [tree](docs/components/tree.md) | tree |
| [display](docs/components/display.md) | badge, alert, icon, code |
| [theme-toggle](docs/components/theme-toggle.md) | theme-toggle |
| [theming](docs/theming.md) | tokens, custom themes, structure-only loading |
## AI-Friendly
This package includes a `.claude/skills/ry-ui-builder` skill so Claude Code can build with these components automatically. The detailed docs above serve as the complete agent reference.
## License
MIT