@ryanhelsing/ry-ui
Version:
Framework-agnostic, Light DOM web components. CSS is the source of truth.
1,440 lines (1,317 loc) • 62.2 kB
HTML
<ry-page>
<ry-header sticky>
<ry-cluster>
<strong>ry-ui</strong>
<span>Kitchen Sink</span>
</ry-cluster>
</ry-header>
<ry-theme-panel></ry-theme-panel>
<ry-main>
<!-- Sticky -->
<ry-section>
<h2>Sticky</h2>
<ry-example title="sticky" stacked>
<template>
<div style="height: 150px; overflow-y: auto; border: 1px solid var(--ry-color-border); border-radius: var(--ry-radius-lg);">
<header sticky style="background: var(--ry-color-bg); padding: var(--ry-space-3); border-bottom: 1px solid var(--ry-color-border);">
<strong>Sticky header</strong>
</header>
<stack style="padding: var(--ry-space-4);">
<p>Scroll down...</p>
<p>Keep scrolling...</p>
<p>The header sticks!</p>
<p>More content here...</p>
<p>Almost there...</p>
<p>Bottom reached.</p>
</stack>
</div>
</template>
</ry-example>
<table>
<thead><tr><th>Attribute</th><th>Values</th><th>Description</th></tr></thead>
<tbody>
<tr><td><code>sticky</code></td><td>boolean</td><td>Sticks to top of scroll container</td></tr>
</tbody>
</table>
</ry-section>
<!-- Grid -->
<ry-section>
<h2>Grid</h2>
<ry-example title="grid">
<template>
<grid cols="3">
<card>One</card>
<card>Two</card>
<card>Three</card>
</grid>
</template>
</ry-example>
<ry-example title="grid auto-fit">
<template>
<grid cols="auto-fit" style="--ry-grid-min: 200px">
<card>Auto</card>
<card>Fit</card>
<card>Cards</card>
<card>Wrap</card>
<card>Fluidly</card>
</grid>
</template>
</ry-example>
<ry-example title="grid responsive breakpoints">
<template>
<grid cols="5" cols-md="3" cols-sm="1">
<card>1</card>
<card>2</card>
<card>3</card>
<card>4</card>
<card>5</card>
</grid>
</template>
</ry-example>
<table>
<thead><tr><th>Attribute</th><th>Values</th><th>Description</th></tr></thead>
<tbody>
<tr><td><code>cols</code></td><td>1-6 | auto-fit | auto-fill</td><td>Number of columns or fluid mode</td></tr>
<tr><td><code>cols-sm</code></td><td>1-3</td><td>Columns at ≤640px</td></tr>
<tr><td><code>cols-md</code></td><td>1-4</td><td>Columns at 641-1024px</td></tr>
<tr><td><code>cols-lg</code></td><td>2-6</td><td>Columns at ≥1025px</td></tr>
<tr><td><code>--ry-grid-min</code></td><td>CSS length</td><td>Min column width for auto-fit/auto-fill (default 280px)</td></tr>
</tbody>
</table>
</ry-section>
<!-- Stack -->
<ry-section>
<h2>Stack</h2>
<ry-example title="stack">
<template>
<stack gap="sm">
<card>Stacked item 1</card>
<card>Stacked item 2</card>
<card>Stacked item 3</card>
</stack>
</template>
</ry-example>
<table>
<thead><tr><th>Attribute</th><th>Values</th><th>Description</th></tr></thead>
<tbody>
<tr><td><code>gap</code></td><td>sm | md | lg</td><td>Vertical spacing</td></tr>
</tbody>
</table>
</ry-section>
<!-- Cluster -->
<ry-section>
<h2>Cluster</h2>
<ry-example title="cluster">
<template>
<cluster>
<badge>Tag 1</badge>
<badge>Tag 2</badge>
<badge>Tag 3</badge>
<badge>Tag 4</badge>
</cluster>
</template>
</ry-example>
<table>
<thead><tr><th>Attribute</th><th>Values</th><th>Description</th></tr></thead>
<tbody>
<tr><td><code>gap</code></td><td>sm | md | lg</td><td>Horizontal spacing</td></tr>
</tbody>
</table>
</ry-section>
<!-- Card -->
<ry-section>
<h2>Card</h2>
<ry-example title="card">
<template>
<card>
<h3>Card Title</h3>
<p>Card content goes here.</p>
<actions>
<button>Action</button>
<button variant="ghost">Cancel</button>
</actions>
</card>
</template>
</ry-example>
<ry-example title="interactive cards" stacked>
<template>
<grid cols="3">
<card interactive>
<h3>Hover Me</h3>
<p>All cards lift on hover. Interactive cards get a stronger effect with primary border.</p>
</card>
<card interactive href="#card-demo">
<h3>With Link</h3>
<p>Interactive cards with <code>href</code> navigate on click or Enter key.</p>
</card>
<card interactive>
<h3>Keyboard Accessible</h3>
<p>Tab to focus, Enter or Space to activate. Emits <code>ry:click</code>.</p>
</card>
</grid>
</template>
</ry-example>
<table>
<thead><tr><th>Attribute</th><th>Values</th><th>Description</th></tr></thead>
<tbody>
<tr><td><code>interactive</code></td><td>boolean</td><td>Clickable card with hover lift, focus ring, keyboard support</td></tr>
<tr><td><code>href</code></td><td>URL</td><td>Navigate on click (requires <code>interactive</code>)</td></tr>
</tbody>
</table>
</ry-section>
<!-- Button -->
<ry-section>
<h2>Button</h2>
<ry-example title="button">
<template>
<cluster>
<button>Primary</button>
<button variant="secondary">Secondary</button>
<button variant="outline">Outline</button>
<button variant="ghost">Ghost</button>
<button variant="danger">Danger</button>
<button variant="accent">Accent</button>
<button pressed>Pressed</button>
</cluster>
</template>
</ry-example>
<table>
<thead><tr><th>Attribute</th><th>Values</th><th>Description</th></tr></thead>
<tbody>
<tr><td><code>variant</code></td><td>primary | secondary | outline | ghost | danger | accent</td><td>Button style</td></tr>
<tr><td><code>size</code></td><td>sm | md | lg</td><td>Button size</td></tr>
<tr><td><code>pressed</code></td><td>boolean</td><td>Pressed/active toggle state</td></tr>
<tr><td><code>modal</code></td><td>modal-id</td><td>Opens modal on click</td></tr>
<tr><td><code>drawer</code></td><td>drawer-id</td><td>Opens drawer on click</td></tr>
</tbody>
</table>
</ry-section>
<!-- Split -->
<ry-section>
<h2>Split</h2>
<ry-example title="split" stacked>
<template>
<split style="--ry-split-width: 200px; height: 120px;">
<card>Main content (flex: 1)</card>
<card>Sidebar (200px)</card>
</split>
</template>
</ry-example>
<ry-example title="split resizable" stacked>
<template>
<split resizable style="height: 150px;">
<card>Drag the handle to resize →</card>
<card>Resizable sidebar (default 300px)</card>
</split>
</template>
</ry-example>
<ry-example title="split resizable + persist" stacked>
<template>
<split resizable persist="demo-panel" style="height: 150px; --ry-split-width: 250px;">
<card>Width persists across page reloads. Double-click handle to reset.</card>
<card>Persisted sidebar</card>
</split>
</template>
<script slot="js" type="text/plain">
const split = document.querySelector('ry-split[resizable]');
// Listen for resize
split.addEventListener('ry:resize', (e) => {
console.log(e.detail.width); // new width in px
});
// Keyboard: Arrow keys (±10px), Shift+Arrow (±50px)
// Home = min width, End = max width
// Double-click handle = reset to default
</script>
</ry-example>
<table>
<thead><tr><th>Attribute</th><th>Values</th><th>Description</th></tr></thead>
<tbody>
<tr><td><code>resizable</code></td><td>boolean</td><td>Enable drag-to-resize handle</td></tr>
<tr><td><code>persist</code></td><td>string</td><td>localStorage key — saves width across reloads</td></tr>
<tr><td><code>--ry-split-width</code></td><td>CSS length</td><td>Sidebar width (default 300px)</td></tr>
<tr><td><code>--ry-split-min-width</code></td><td>CSS length</td><td>Min width during resize (default 100px)</td></tr>
<tr><td><code>--ry-split-max-width</code></td><td>CSS length</td><td>Max width during resize (default 80% of container)</td></tr>
</tbody>
</table>
</ry-section>
<!-- Check List -->
<ry-section>
<h2>Check List</h2>
<ry-example title="check-list">
<template>
<ul class="ry-check-list">
<li>Unlimited projects</li>
<li>Priority support</li>
<li>Advanced analytics</li>
<li>Custom domains</li>
</ul>
</template>
</ry-example>
<table>
<thead><tr><th>Class</th><th>Description</th></tr></thead>
<tbody>
<tr><td><code>.ry-check-list</code></td><td>Green checkmark list (CSS-only, no JS)</td></tr>
</tbody>
</table>
</ry-section>
<!-- Accordion -->
<ry-section>
<h2>Accordion</h2>
<ry-example title="accordion">
<template>
<accordion>
<accordion-item title="What is ry-ui?" open>
A framework-agnostic, Light DOM component library.
</accordion-item>
<accordion-item title="Do I need a build step?">
Nope! Just link the CSS and JS files.
</accordion-item>
</accordion>
</template>
<script slot="js" type="text/plain">
const accordion = document.querySelector('ry-accordion');
const item = document.querySelector('ry-accordion-item');
// Listen for open/close
item.addEventListener('ry:toggle', (e) => {
console.log(e.detail.open); // true or false
});
// Open/close programmatically
item.open = true; // Expand
item.open = false; // Collapse
// Toggle
item.toggle();
</script>
</ry-example>
<table>
<thead><tr><th>Attribute</th><th>Values</th><th>Description</th></tr></thead>
<tbody>
<tr><td><code>title</code></td><td>string</td><td>Header text (on accordion-item)</td></tr>
<tr><td><code>open</code></td><td>boolean</td><td>Initially expanded (on accordion-item)</td></tr>
</tbody>
</table>
</ry-section>
<!-- Tabs -->
<ry-section>
<h2>Tabs</h2>
<ry-example title="tabs">
<template>
<tabs>
<tab title="Overview" active>
<p>This is the overview tab content.</p>
</tab>
<tab title="Features">
<p>This is the features tab content.</p>
</tab>
</tabs>
</template>
<script slot="js" type="text/plain">
const tabs = document.querySelector('ry-tabs');
// Listen for tab changes
tabs.addEventListener('ry:change', (e) => {
console.log(e.detail.index); // 0, 1, 2...
console.log(e.detail.title); // "Overview"
});
// Switch tabs programmatically
tabs.activeIndex = 1; // Switch to second tab
</script>
</ry-example>
<table>
<thead><tr><th>Attribute</th><th>Values</th><th>Description</th></tr></thead>
<tbody>
<tr><td><code>title</code></td><td>string</td><td>Tab label (on tab)</td></tr>
<tr><td><code>active</code></td><td>boolean</td><td>Initially selected (on tab)</td></tr>
</tbody>
</table>
</ry-section>
<!-- Modal -->
<ry-section>
<h2>Modal</h2>
<ry-example title="modal">
<template>
<button modal="example-modal">Open Modal</button>
<modal id="example-modal" title="Modal Title">
<p>Modal content with focus trapping.</p>
<footer>
<button variant="ghost" close>Cancel</button>
<button>Confirm</button>
</footer>
</modal>
</template>
<script slot="js" type="text/plain">
const modal = document.querySelector('ry-modal');
// Listen for open/close
modal.addEventListener('ry:open', () => {
console.log('Modal opened');
});
modal.addEventListener('ry:close', () => {
console.log('Modal closed');
});
// Open/close programmatically
modal.open();
modal.close();
// Check state
if (modal.state === 'open') { ... }
</script>
</ry-example>
<table>
<thead><tr><th>Attribute</th><th>Values</th><th>Description</th></tr></thead>
<tbody>
<tr><td><code>id</code></td><td>string</td><td>Modal identifier</td></tr>
<tr><td><code>title</code></td><td>string</td><td>Header title</td></tr>
<tr><td><code>close</code></td><td>boolean</td><td>Closes modal (on button)</td></tr>
</tbody>
</table>
</ry-section>
<!-- Dropdown -->
<ry-section>
<h2>Dropdown</h2>
<ry-example title="dropdown">
<template>
<dropdown>
<button>Options ▾</button>
<menu>
<menu-item>Edit</menu-item>
<menu-item>Duplicate</menu-item>
<divider></divider>
<menu-item>Delete</menu-item>
</menu>
</dropdown>
</template>
<script slot="js" type="text/plain">
const dropdown = document.querySelector('ry-dropdown');
// Listen for menu item selection
dropdown.addEventListener('ry:select', (e) => {
console.log(e.detail.value); // "Edit", "Delete", etc.
});
// Open/close programmatically
dropdown.open();
dropdown.close();
</script>
</ry-example>
<table>
<thead><tr><th>Attribute</th><th>Values</th><th>Description</th></tr></thead>
<tbody>
<tr><td>first child</td><td>—</td><td>First child element is the trigger</td></tr>
</tbody>
</table>
</ry-section>
<!-- Field -->
<ry-section>
<h2>Field</h2>
<ry-example title="field">
<template>
<stack>
<field label="Email" hint="We'll never share your email">
<input type="email" placeholder="you@example.com">
</field>
<field label="Password" error="Must be at least 8 characters">
<input type="password" placeholder="Enter password">
</field>
<field label="Message">
<textarea rows="3" placeholder="Your message..."></textarea>
</field>
</stack>
</template>
</ry-example>
<table>
<thead><tr><th>Attribute</th><th>Values</th><th>Description</th></tr></thead>
<tbody>
<tr><td><code>label</code></td><td>string</td><td>Field label text</td></tr>
<tr><td><code>error</code></td><td>string</td><td>Error message (hides hint when set)</td></tr>
<tr><td><code>hint</code></td><td>string</td><td>Helper text below input</td></tr>
</tbody>
</table>
</ry-section>
<!-- Checkbox -->
<ry-section>
<h2>Checkbox</h2>
<ry-example title="checkbox">
<template>
<stack gap="sm">
<label>
<input type="checkbox" checked> Accept terms and conditions
</label>
<label>
<input type="checkbox"> Subscribe to newsletter
</label>
<label>
<input type="checkbox" disabled> Disabled option
</label>
</stack>
</template>
</ry-example>
<table>
<thead><tr><th>Attribute</th><th>Values</th><th>Description</th></tr></thead>
<tbody>
<tr><td><code>checked</code></td><td>boolean</td><td>Initially checked</td></tr>
<tr><td><code>disabled</code></td><td>boolean</td><td>Disable interaction</td></tr>
</tbody>
</table>
</ry-section>
<!-- Radio -->
<ry-section>
<h2>Radio</h2>
<ry-example title="radio">
<template>
<stack gap="sm">
<label>
<input type="radio" name="plan" value="free" checked> Free plan
</label>
<label>
<input type="radio" name="plan" value="pro"> Pro plan
</label>
<label>
<input type="radio" name="plan" value="enterprise"> Enterprise
</label>
<label>
<input type="radio" name="plan" value="disabled" disabled> Disabled
</label>
</stack>
</template>
</ry-example>
<table>
<thead><tr><th>Attribute</th><th>Values</th><th>Description</th></tr></thead>
<tbody>
<tr><td><code>name</code></td><td>string</td><td>Groups radios together</td></tr>
<tr><td><code>value</code></td><td>string</td><td>Value when selected</td></tr>
<tr><td><code>checked</code></td><td>boolean</td><td>Initially selected</td></tr>
<tr><td><code>disabled</code></td><td>boolean</td><td>Disable interaction</td></tr>
</tbody>
</table>
</ry-section>
<!-- Badge -->
<ry-section>
<h2>Badge</h2>
<ry-example title="badge">
<template>
<cluster>
<badge>Default</badge>
<badge variant="primary">Primary</badge>
<badge variant="success">Success</badge>
<badge variant="warning">Warning</badge>
<badge variant="danger">Danger</badge>
<badge variant="accent">Accent</badge>
<badge style="--ry-badge-color: oklch(0.6 0.2 150)">Custom Color</badge>
</cluster>
</template>
</ry-example>
<table>
<thead><tr><th>Attribute</th><th>Values</th><th>Description</th></tr></thead>
<tbody>
<tr><td><code>variant</code></td><td>default | primary | success | warning | danger | accent</td><td>Badge style</td></tr>
<tr><td><code>--ry-badge-color</code></td><td>CSS color</td><td>Arbitrary background color via style</td></tr>
<tr><td><code>--ry-badge-text</code></td><td>CSS color</td><td>Arbitrary text color via style</td></tr>
</tbody>
</table>
</ry-section>
<!-- Alert -->
<ry-section>
<h2>Alert</h2>
<ry-example title="alert">
<template>
<stack>
<alert type="info" title="Info">
This is an informational message.
</alert>
<alert type="success" title="Success">
Operation completed successfully.
</alert>
<alert type="warning" title="Warning">
Please review before continuing.
</alert>
<alert type="danger" title="Error">
Something went wrong.
</alert>
</stack>
</template>
</ry-example>
<table>
<thead><tr><th>Attribute</th><th>Values</th><th>Description</th></tr></thead>
<tbody>
<tr><td><code>type</code></td><td>info | success | warning | danger</td><td>Alert style</td></tr>
<tr><td><code>title</code></td><td>string</td><td>Alert heading</td></tr>
</tbody>
</table>
</ry-section>
<!-- Switch -->
<ry-section>
<h2>Switch</h2>
<ry-example title="switch">
<template>
<stack gap="sm">
<switch name="notifications" checked>Enable notifications</switch>
<switch name="darkMode">Dark mode</switch>
<switch disabled>Disabled switch</switch>
</stack>
</template>
<script slot="js" type="text/plain">
const toggle = document.querySelector('ry-switch');
// Listen for changes
toggle.addEventListener('ry:change', (e) => {
console.log(e.detail.checked); // true or false
console.log(e.detail.name); // "notifications"
});
// Get/set programmatically
toggle.checked; // true
toggle.checked = false; // Turn off
// Form usage with hidden input
const hidden = document.querySelector('input[name="notifications"]');
toggle.addEventListener('ry:change', (e) => {
hidden.value = e.detail.checked ? '1' : '0';
});
</script>
</ry-example>
<table>
<thead><tr><th>Attribute</th><th>Values</th><th>Description</th></tr></thead>
<tbody>
<tr><td><code>name</code></td><td>string</td><td>Form field name</td></tr>
<tr><td><code>checked</code></td><td>boolean</td><td>Initially checked</td></tr>
<tr><td><code>disabled</code></td><td>boolean</td><td>Disable interaction</td></tr>
</tbody>
</table>
</ry-section>
<!-- Slider -->
<ry-section>
<h2>Slider</h2>
<ry-example title="slider" stacked>
<template>
<stack gap="lg">
<slider min="0" max="100" value="50"></slider>
<slider min="0" max="100" start="25" end="75" range labeled></slider>
<slider min="0" max="10" step="1" value="5" ticked labeled tooltip></slider>
<stack gap="sm">
<slider min="0" max="100" value="60" color="success"></slider>
<slider min="0" max="100" value="60" color="warning"></slider>
<slider min="0" max="100" value="60" color="danger"></slider>
<slider min="0" max="100" value="60" color="info"></slider>
</stack>
<slider min="0" max="100" value="50" disabled></slider>
</stack>
</template>
<script slot="js" type="text/plain">
const slider = document.querySelector('ry-slider');
// Listen for changes (fires during drag)
slider.addEventListener('ry:input', (e) => {
console.log(e.detail.value); // 50 (single slider)
// For range slider:
// e.detail.start, e.detail.end
});
// Listen for final value (fires on mouseup)
slider.addEventListener('ry:change', (e) => {
saveVolume(e.detail.value);
});
// Get/set programmatically
slider.value; // 50
slider.value = 75; // Update value
// Range slider
rangeSlider.start; // 25
rangeSlider.end; // 75
rangeSlider.start = 10;
</script>
</ry-example>
<table>
<thead><tr><th>Attribute</th><th>Values</th><th>Description</th></tr></thead>
<tbody>
<tr><td><code>min</code></td><td>number</td><td>Minimum value (default: 0)</td></tr>
<tr><td><code>max</code></td><td>number</td><td>Maximum value (default: 100)</td></tr>
<tr><td><code>step</code></td><td>number</td><td>Step increment, 0 for smooth (default: 1)</td></tr>
<tr><td><code>value</code></td><td>number</td><td>Current value (single slider)</td></tr>
<tr><td><code>start</code> / <code>end</code></td><td>number</td><td>Range values (with range attribute)</td></tr>
<tr><td><code>range</code></td><td>boolean</td><td>Enable dual-handle range mode</td></tr>
<tr><td><code>labeled</code></td><td>boolean</td><td>Show value labels</td></tr>
<tr><td><code>ticked</code></td><td>boolean</td><td>Show tick marks</td></tr>
<tr><td><code>tooltip</code></td><td>boolean</td><td>Show value on thumb hover</td></tr>
<tr><td><code>vertical</code></td><td>boolean</td><td>Vertical orientation</td></tr>
<tr><td><code>reversed</code></td><td>boolean</td><td>Reverse direction</td></tr>
<tr><td><code>color</code></td><td>primary | secondary | success | warning | danger | info</td><td>Track color</td></tr>
<tr><td><code>size</code></td><td>sm | lg</td><td>Size variant</td></tr>
<tr><td><code>disabled</code></td><td>boolean</td><td>Disable interaction</td></tr>
</tbody>
</table>
</ry-section>
<!-- Knob -->
<ry-section>
<h2>Knob</h2>
<ry-example title="knob" stacked>
<template>
<stack gap="lg">
<cluster>
<knob min="0" max="100" value="50" label="Volume" description="Master output volume"></knob>
<knob min="0" max="100" value="75" label="Pan" description="Left/right stereo position"></knob>
<knob min="0" max="100" value="25" label="Reverb" description="Room reverb amount"></knob>
</cluster>
<cluster>
<knob min="0" max="3" step="1" value="1" labels="Off,Low,Med,High" label="Mode"></knob>
<knob min="0" max="4" step="1" value="2" labels="∿,△,▢,▲,◇" label="Wave"></knob>
</cluster>
<cluster>
<knob value="60" color="success" label="Success"></knob>
<knob value="60" color="warning" label="Warning"></knob>
<knob value="60" color="danger" label="Danger"></knob>
<knob value="60" color="secondary" label="Secondary"></knob>
</cluster>
<cluster>
<knob size="sm" value="25" label="Small"></knob>
<knob value="50" label="Default"></knob>
<knob size="lg" value="75" label="Large"></knob>
</cluster>
<knob min="0" max="100" value="50" label="Disabled" disabled></knob>
</stack>
</template>
<script slot="js" type="text/plain">
const knob = document.querySelector('ry-knob');
// Listen for changes (fires during drag)
knob.addEventListener('ry:input', (e) => {
console.log(e.detail.value); // 0-100
updateVolume(e.detail.value);
});
// Listen for final value (fires on release)
knob.addEventListener('ry:change', (e) => {
saveSettings({ volume: e.detail.value });
});
// Get/set programmatically
knob.value; // 50
knob.value = 75; // Update value
// For discrete knobs with labels
modeKnob.value; // 1 (index)
// Labels: "Off,Low,Med,High" → value 1 = "Low"
</script>
</ry-example>
<table>
<thead><tr><th>Attribute</th><th>Values</th><th>Description</th></tr></thead>
<tbody>
<tr><td><code>min</code></td><td>number</td><td>Minimum value (default: 0)</td></tr>
<tr><td><code>max</code></td><td>number</td><td>Maximum value (default: 100)</td></tr>
<tr><td><code>step</code></td><td>number</td><td>Step increment, 0 for smooth (default: 0)</td></tr>
<tr><td><code>value</code></td><td>number</td><td>Current value</td></tr>
<tr><td><code>label</code></td><td>string</td><td>Label text below knob</td></tr>
<tr><td><code>labels</code></td><td>string</td><td>Comma-separated display labels for discrete values</td></tr>
<tr><td><code>description</code></td><td>string</td><td>Tooltip text shown on hover</td></tr>
<tr><td><code>color</code></td><td>primary | secondary | success | warning | danger</td><td>Track color</td></tr>
<tr><td><code>size</code></td><td>sm | lg</td><td>Size variant</td></tr>
<tr><td><code>disabled</code></td><td>boolean</td><td>Disable interaction</td></tr>
</tbody>
</table>
</ry-section>
<!-- Number Select -->
<ry-section>
<h2>Number Select</h2>
<ry-example title="number-select" stacked>
<template>
<ry-stack gap="xl">
<p><strong>Arrow placement</strong></p>
<ry-cluster gap="lg">
<ry-number-select min="0" max="100" value="50"></ry-number-select>
<ry-number-select min="0" max="100" value="50" arrows="start"></ry-number-select>
<ry-number-select min="0" max="100" value="50" arrows="end"></ry-number-select>
<ry-number-select min="0" max="100" value="50" arrows="none"></ry-number-select>
<ry-number-select min="0" max="100" value="50" arrows="stacked"></ry-number-select>
<ry-number-select min="0" max="100" value="50" arrows="stacked-end" icons="chevron"></ry-number-select>
<ry-number-select min="0" max="100" value="50" arrows="stacked-start" icons="chevron"></ry-number-select>
</ry-cluster>
<p><strong>Icon styles</strong></p>
<ry-cluster gap="lg">
<ry-number-select min="0" max="100" value="50"></ry-number-select>
<ry-number-select min="0" max="100" value="50" icons="chevron"></ry-number-select>
<ry-number-select min="0" max="100" value="50" icons="arrow"></ry-number-select>
<ry-number-select min="0" max="100" value="50" icons="chevron" arrows="stacked"></ry-number-select>
<ry-number-select min="0" max="100" value="50" icons="arrow" arrows="stacked"></ry-number-select>
</ry-cluster>
<p><strong>Drag direction</strong> — stacked defaults to vertical</p>
<ry-cluster gap="lg">
<ry-number-select min="0" max="100" value="50"></ry-number-select>
<ry-number-select min="0" max="100" value="50" drag="y"></ry-number-select>
<ry-number-select min="0" max="100" value="50" drag="none"></ry-number-select>
</ry-cluster>
<p><strong>Prefix & suffix</strong></p>
<ry-cluster gap="lg">
<ry-number-select min="0" max="1000" value="50" prefix="$"></ry-number-select>
<ry-number-select min="0" max="360" value="90" suffix="°" icons="chevron" arrows="stacked"></ry-number-select>
<ry-number-select min="0" max="100" value="75" suffix="%"></ry-number-select>
<ry-number-select min="0" max="100" value="50" prefix="$" suffix="USD" editable></ry-number-select>
</ry-cluster>
<p><strong>Step intervals</strong></p>
<ry-cluster gap="lg">
<ry-number-select min="0" max="100" value="50" step="1"></ry-number-select>
<ry-number-select min="0" max="100" value="50" step="5"></ry-number-select>
<ry-number-select min="0" max="10" value="5" step="0.1"></ry-number-select>
<ry-number-select min="0" max="1" value="0.5" step="0.01"></ry-number-select>
</ry-cluster>
<p><strong>Behaviors</strong></p>
<ry-cluster gap="lg">
<ry-number-select min="0" max="100" value="50" editable></ry-number-select>
<ry-number-select min="0" max="7" value="3" wrap></ry-number-select>
<ry-number-select min="0" max="100" value="50" disabled></ry-number-select>
</ry-cluster>
<p><strong>Sizes</strong></p>
<ry-cluster gap="lg" style="align-items: center">
<ry-number-select min="0" max="100" value="50" size="xs"></ry-number-select>
<ry-number-select min="0" max="100" value="50" size="sm"></ry-number-select>
<ry-number-select min="0" max="100" value="50"></ry-number-select>
<ry-number-select min="0" max="100" value="50" size="lg"></ry-number-select>
</ry-cluster>
</ry-stack>
</template>
<script slot="js" type="text/plain">
const ns = document.querySelector('ry-number-select');
// Events — ry:input fires during drag, ry:change on commit
ns.addEventListener('ry:input', (e) => {
console.log('input:', e.detail.value);
});
ns.addEventListener('ry:change', (e) => {
console.log('change:', e.detail.value);
});
// Programmatic API
ns.value; // 50
ns.value = 75;
ns.min = 10;
ns.step = 5;
ns.drag = 'y'; // switch drag direction
ns.disabled = true;
</script>
</ry-example>
<table>
<thead><tr><th>Attribute</th><th>Values</th><th>Description</th></tr></thead>
<tbody>
<tr><td><code>min</code></td><td>number</td><td>Minimum value (default: 0)</td></tr>
<tr><td><code>max</code></td><td>number</td><td>Maximum value (default: 100)</td></tr>
<tr><td><code>step</code></td><td>number</td><td>Step increment (default: 1)</td></tr>
<tr><td><code>value</code></td><td>number</td><td>Current value (default: 0)</td></tr>
<tr><td><code>arrows</code></td><td>both | start | end | stacked | none</td><td>Button placement (default: both)</td></tr>
<tr><td><code>icons</code></td><td>plus-minus | chevron | arrow</td><td>Button icon style (default: plus-minus)</td></tr>
<tr><td><code>drag</code></td><td>x | y | none</td><td>Drag direction (default: x, stacked defaults to y)</td></tr>
<tr><td><code>prefix</code></td><td>string</td><td>Text before value (e.g. "$")</td></tr>
<tr><td><code>suffix</code></td><td>string</td><td>Text after value (e.g. "°", "%")</td></tr>
<tr><td><code>editable</code></td><td>boolean</td><td>Allow direct typing on click</td></tr>
<tr><td><code>wrap</code></td><td>boolean</td><td>Wrap around min/max boundaries</td></tr>
<tr><td><code>size</code></td><td>xs | sm | lg</td><td>Size variant</td></tr>
<tr><td><code>disabled</code></td><td>boolean</td><td>Disable interaction</td></tr>
</tbody>
</table>
</ry-section>
<!-- Color Picker -->
<ry-section>
<h2>Color Picker</h2>
<ry-example title="color-picker" stacked>
<template>
<stack gap="lg">
<p><strong>Dropdown mode (default)</strong> - click to open:</p>
<cluster>
<color-picker value="#3b82f6"></color-picker>
<color-picker value="#22c55e"></color-picker>
<color-picker value="#8b5cf6"></color-picker>
<color-picker value="#ef4444" opacity></color-picker>
</cluster>
<p><strong>With swatches:</strong></p>
<color-picker
value="#3b82f6"
swatches="#ef4444;#f59e0b;#22c55e;#3b82f6;#8b5cf6;#ec4899;#1e293b;#ffffff"
></color-picker>
<p><strong>Inline mode</strong> - always visible:</p>
<color-picker value="#3b82f6" inline></color-picker>
<p><strong>With opacity + swatches (inline):</strong></p>
<color-picker
value="rgba(59, 130, 246, 0.7)"
format="rgb"
opacity
inline
swatches="#ef4444;#f59e0b;#22c55e;#3b82f6;#8b5cf6;#ec4899"
></color-picker>
<cluster>
<color-picker value="#64748b" disabled></color-picker>
<span style="color: var(--ry-color-text-muted)">Disabled</span>
</cluster>
</stack>
</template>
<script slot="js" type="text/plain">
// Get the element
const picker = document.querySelector('ry-color-picker');
// Listen for changes (fires during drag)
picker.addEventListener('ry:input', (e) => {
console.log(e.detail.value); // "#3b82f6"
console.log(e.detail.rgb); // { r: 59, g: 130, b: 246 }
console.log(e.detail.hsv); // { h: 217, s: 76, v: 96 }
});
// Listen for final value (fires on mouseup/blur)
picker.addEventListener('ry:change', (e) => {
saveColor(e.detail.value);
});
// Get/set programmatically
picker.value; // "#3b82f6"
picker.value = '#ff0000'; // Set new color
picker.setColor('hsl(200, 100%, 50%)'); // Accepts any format
// Access color in different formats
picker.rgb; // { r: 59, g: 130, b: 246 }
picker.hsl; // { h: 217, s: 89, l: 60 }
picker.hsv; // { h: 217, s: 76, v: 96 }
// Form submission
form.addEventListener('submit', (e) => {
const color = picker.value;
fetch('/api/save', { body: JSON.stringify({ color }) });
});
</script>
</ry-example>
<table>
<thead><tr><th>Attribute</th><th>Values</th><th>Description</th></tr></thead>
<tbody>
<tr><td><code>value</code></td><td>color string</td><td>Initial color (hex, rgb, hsl)</td></tr>
<tr><td><code>format</code></td><td>hex | rgb | hsl</td><td>Output format (default: hex)</td></tr>
<tr><td><code>inline</code></td><td>boolean</td><td>Always show picker (no dropdown)</td></tr>
<tr><td><code>opacity</code></td><td>boolean</td><td>Enable alpha channel slider</td></tr>
<tr><td><code>swatches</code></td><td>string</td><td>Preset colors separated by semicolons</td></tr>
<tr><td><code>disabled</code></td><td>boolean</td><td>Disable interaction</td></tr>
</tbody>
</table>
</ry-section>
<!-- Color Input -->
<ry-section>
<h2>Color Input</h2>
<ry-example title="color-input" stacked>
<template>
<stack gap="lg">
<p>Input-style color picker with editable hex value:</p>
<cluster>
<color-input value="#3b82f6"></color-input>
<color-input value="#22c55e"></color-input>
<color-input value="#ef4444"></color-input>
</cluster>
<p>With opacity support:</p>
<color-input value="rgba(139, 92, 246, 0.8)" opacity></color-input>
<cluster>
<color-input value="#64748b" disabled></color-input>
<span style="color: var(--ry-color-text-muted)">Disabled</span>
</cluster>
</stack>
</template>
<script slot="js" type="text/plain">
const input = document.querySelector('ry-color-input');
// Listen for changes
input.addEventListener('ry:change', (e) => {
console.log(e.detail.value); // "#3b82f6"
});
// Get/set programmatically
input.value; // "#3b82f6"
input.value = '#ff0000';
// Works great in forms
const hidden = document.querySelector('input[name="brandColor"]');
input.addEventListener('ry:change', (e) => {
hidden.value = e.detail.value;
});
</script>
</ry-example>
<table>
<thead><tr><th>Attribute</th><th>Values</th><th>Description</th></tr></thead>
<tbody>
<tr><td><code>value</code></td><td>color string</td><td>Initial color (hex, rgb, hsl)</td></tr>
<tr><td><code>format</code></td><td>hex | rgb | hsl</td><td>Output format (default: hex)</td></tr>
<tr><td><code>opacity</code></td><td>boolean</td><td>Enable alpha channel slider</td></tr>
<tr><td><code>placeholder</code></td><td>string</td><td>Placeholder text for input</td></tr>
<tr><td><code>disabled</code></td><td>boolean</td><td>Disable interaction</td></tr>
</tbody>
</table>
</ry-section>
<!-- Gradient Picker -->
<ry-section>
<h2>Gradient Picker</h2>
<ry-example title="gradient-picker" stacked>
<template>
<stack gap="lg">
<p><strong>Linear gradient</strong> (default):</p>
<gradient-picker value="linear-gradient(90deg, #3b82f6 0%, #8b5cf6 100%)"></gradient-picker>
<p><strong>Multi-stop rainbow with CSS output:</strong></p>
<gradient-picker value="linear-gradient(90deg, #ef4444 0%, #f59e0b 25%, #22c55e 50%, #3b82f6 75%, #8b5cf6 100%)" output></gradient-picker>
<p><strong>Radial gradient:</strong></p>
<gradient-picker value="radial-gradient(circle, #fbbf24 0%, #f97316 50%, #dc2626 100%)"></gradient-picker>
</stack>
</template>
<script slot="js" type="text/plain">
const picker = document.querySelector('ry-gradient-picker');
// Listen for changes
picker.addEventListener('ry:input', (e) => {
console.log(e.detail.value); // CSS gradient string
console.log(e.detail.stops); // [{ id, color, position }, ...]
console.log(e.detail.type); // "linear" | "radial"
console.log(e.detail.angle); // 90 (degrees, linear only)
});
picker.addEventListener('ry:change', (e) => {
applyGradient(e.detail.value);
});
// Get/set programmatically
picker.value; // "linear-gradient(90deg, ...)"
picker.value = 'radial-gradient(circle, #ff0000 0%, #0000ff 100%)';
picker.type; // "linear" | "radial"
picker.angle; // 0-360
picker.stops; // [{ id, color, position }, ...]
// Add/remove stops
picker.addStop('#ff00ff', 50);
picker.removeStop('stop-2');
</script>
</ry-example>
<table>
<thead><tr><th>Attribute</th><th>Values</th><th>Description</th></tr></thead>
<tbody>
<tr><td><code>value</code></td><td>CSS gradient string</td><td>Initial gradient value</td></tr>
<tr><td><code>disabled</code></td><td>boolean</td><td>Disable interaction</td></tr>
</tbody>
</table>
</ry-section>
<!-- Toggle Button -->
<ry-section>
<h2>Toggle Button</h2>
<ry-example title="toggle-button" stacked>
<template>
<stack gap="lg">
<grid cols="3">
<toggle-button name="plan" value="bronze" block>
<strong>Bronze</strong><br>
<small>Standard Network</small><br>
<big><b>$85</b>/month</big><br>
<small>Deductible: $6,000 · Coverage: 60%</small><br>
<small>✓ Preventive care ✓ Generic Rx ✓ Telehealth</small>
</toggle-button>
<toggle-button name="plan" value="silver" block>
<strong>Silver</strong><br>
<small>Enhanced Network</small><br>
<big><b>$145</b>/month</big><br>
<small>Deductible: $3,000 · Coverage: 80%</small><br>
<small>✓ Lower deductible ✓ Specialists ✓ Mental health</small>
</toggle-button>
<toggle-button name="plan" value="gold" block>
<strong>Gold</strong><br>
<small>Premium Network</small><br>
<big><b>$220</b>/month</big><br>
<small>Deductible: $1,000 · Coverage: 90%</small><br>
<small>✓ Lowest out-of-pocket ✓ Full Rx ✓ No referrals</small>
</toggle-button>
</grid>
<cluster>
<toggle-button name="coverage" value="medical" pressed>🏥 Medical</toggle-button>
<toggle-button name="coverage" value="dental">🦷 Dental</toggle-button>
<toggle-button name="coverage" value="vision">👁 Vision</toggle-button>
</cluster>
<stack>
<grid cols="2">
<toggle-button name="selection" value="ppo200" block>
<strong>Blue PPO $200</strong><br>
<small>Employee Only · $200 Deductible</small><br>
<b>$171.28</b>/month
</toggle-button>
<toggle-button name="selection" value="ppo500" block>
<strong>Blue PPO $500</strong><br>
<small>Employee Only · $500 Deductible</small><br>
<b>$142.53</b>/month
</toggle-button>
</grid>
<toggle-button name="selection" value="waive" pressed>Waive $0/month</toggle-button>
</stack>
</stack>
</template>
<script slot="js" type="text/plain">
const buttons = document.querySelectorAll('ry-toggle-button[name="plan"]');
// Listen for changes on each button
buttons.forEach(btn => {
btn.addEventListener('ry:change', (e) => {
console.log(e.detail.pressed); // true or false
console.log(e.detail.value); // "bronze", "silver", etc.
});
});
// Get selected value from a group
function getSelectedPlan() {
const pressed = document.querySelector(
'ry-toggle-button[name="plan"][pressed]'
);
return pressed?.value; // "gold"
}
// Set programmatically
button.pressed = true;
// Form submission
form.addEventListener('submit', (e) => {
const plan = getSelectedPlan();
fetch('/api/enroll', { body: JSON.stringify({ plan }) });
});
</script>
</ry-example>
<table>
<thead><tr><th>Attribute</th><th>Values</th><th>Description</th></tr></thead>
<tbody>
<tr><td><code>name</code></td><td>string</td><td>Groups buttons (same name = only one pressed)</td></tr>
<tr><td><code>value</code></td><td>string</td><td>Value when selected</td></tr>
<tr><td><code>pressed</code></td><td>boolean</td><td>Currently pressed</td></tr>
<tr><td><code>disabled</code></td><td>boolean</td><td>Disable interaction</td></tr>
<tr><td><code>size</code></td><td>sm | md | lg</td><td>Button size</td></tr>
</tbody>
</table>
</ry-section>
<!-- Tooltip -->
<ry-section>
<h2>Tooltip</h2>
<ry-example title="tooltip">
<template>
<cluster>
<tooltip content="Top tooltip" position="top">
<button>Top</button>
</tooltip>
<tooltip content="Bottom tooltip" position="bottom">
<button>Bottom</button>
</tooltip>
<tooltip content="Left tooltip" position="left">
<button>Left</button>
</tooltip>
<tooltip content="Right tooltip" position="right">
<button>Right</button>
</tooltip>
</cluster>
</template>
</ry-example>
<table>
<thead><tr><th>Attribute</th><th>Values</th><th>Description</th></tr></thead>
<tbody>
<tr><td><code>content</code></td><td>string</td><td>Tooltip text</td></tr>
<tr><td><code>position</code></td><td>top | bottom | left | right</td><td>Tooltip position</td></tr>
</tbody>
</table>
</ry-section>
<!-- Drawer -->
<ry-section>
<h2>Drawer</h2>
<ry-example title="drawer">
<template>
<button drawer="example-drawer">Open Drawer</button>
<drawer id="example-drawer" side="right">
<h3>Drawer Title</h3>
<p>Drawer content here.</p>
</drawer>
</template>
<script slot="js" type="text/plain">
const drawer = document.querySelector('ry-drawer');
// Listen for open/close
drawer.addEventListener('ry:open', () => {
console.log('Drawer opened');
});
drawer.addEventListener('ry:close', () => {
console.log('Drawer closed');
});
// Open/close programmatically
drawer.open();
drawer.close();
// Check state
if (drawer.state === 'open') { ... }
</script>
</ry-example>
<table>
<thead><tr><th>Attribute</th><th>Values</th><th>Description</th></tr></thead>
<tbody>
<tr><td><code>id</code></td><td>string</td><td>Drawer identifier</td></tr>
<tr><td><code>side</code></td><td>left | right | bottom</td><td>Slide direction</td></tr>
</tbody>
</table>
</ry-section>
<!-- Toast -->
<ry-section>
<h2>Toast</h2>
<ry-example title="toast">
<template>
<cluster>
<button onclick="RyToast.success('Saved!')">Success</button>
<button onclick="RyToast.error('Failed!')">Error</button>
<button onclick="RyToast.warning('Warning!')">Warning</button>
<button onclick="RyToast.info('Info!')">Info</button>
</cluster>
</template>
<script slot="js" type="text/plain">
// Show toasts (global API)
RyToast.success('Changes saved!');
RyToast.error('Something went wrong');
RyToast.warning('Please review');
RyToast.info('New update available');
// With options
RyToast.success('Saved!', {
duration: 5000, // ms (default: 3000)
});
// Common patterns
async function saveData() {
try {
await api.save(data);
RyToast.success('Saved!');
} catch (err) {
RyToast.error(err.message);
}
}
</script>
</ry-example>
<table>
<thead><tr><th>Method</th><th>Description</th></tr></thead>
<tbody>
<tr><td><code>RyToast.success(msg)</code></td><td>Show success toast</td></tr>
<tr><td><code>RyToast.error(msg)</code></td><td>Show error toast</td></tr>
<tr><td><code>RyToast.warning(msg)</code></td><td>Show warning toast</td></tr>
<tr><td><code>RyToast.info(msg)</code></td><td>Show info toast</td></tr>
</tbody>
</table>
</ry-section>
<!-- Select -->
<ry-section>
<h2>Select</h2>
<ry-example title="select">
<template>
<select placeholder="Choose a country" name="country">
<option value="us">United States</option>
<option value="uk">United Kingdom</option>
<option value="ca">Canada</option>
<option value="au">Australia</option>
</select>
</template>
<script slot="js" type="text/plain">
const select = document.querySelector('ry-select');
// Listen for selection changes
select.addEventListener('ry:change', (e) => {
console.log(e.detail.value); // "us"
console.log(e.detail.label); // "United States"
});
// Get/set programmatically
select.value; // "us"
select.value = 'uk'; // Select United Kingdom
// Open/close the dropdown
select.open();
select.close();
// Form integration
const formData = new FormData(form);
formData.get('country'); // "us"
</script>
</ry-example>
<table>
<thead><tr><th>Attribute</th><th>Values</th><th>Description</th></tr></thead>
<tbody>
<tr><td><code>placeholder</code></td><td>string</td><td>Placeholder text</td></tr>
<tr><td><code>name</code></td><td>string</td><td>Form field name</td></tr>
<tr><td><code>disabled</code></td><td>boolean</td><td>Disable select (on option)</td></tr>
<tr><td><code>multiple</code></td><td>boolean</td><td>Enable multi-select with tags</td></tr>
<tr><td><code>clearable</code></td><td>boolean</td><td>Show clear button (multi)</td></tr>
<tr><td><code>max-selections</code></td><td>number</td><td>Max selections (multi)</td></tr>
</tbody>
</table>
<h3 style="margin-top: var(--ry-space-6);">Multi-Select</h3>
<ry-example title="select-multi" stacked>
<template>
<select multiple clearable placeholder="Choose countries...">
<option value="us">United States</option>
<option value="uk">United Kingdom</option>
<option value="ca">Canada</option>
<option value="au">Australia</option>
<option value="de">Germany</option>
<option value="fr">France</option>
</select>
</template>
</ry-example>
</ry-section>
<!-- Combobox -->
<ry-section>
<h2>Combobox</h2>
<ry-example title="combobox">
<template>
<combobox placeholder="Search countries..." name="country">
<option value="us">United States</option>
<option value="uk">United Kingdom</option>
<option value="ca">Canada</option>
<option value="au">Australia</option>
<option value="de">Germany</option>
<option value="fr">France</option>
<option value="jp">Japan</option>
<option value="br">Brazil</option>
</combobox>
</template>
<script slot="js" type="text/plain">
const combobox = document.querySelector('ry-combobox');
// Listen for selection
combobox.addEventListener('ry:change', (e) => {
console.log(e.detail.value); // "us"
console.log(e.detail.label); // "United States"
});
// Get/set programmatically
combobox.value; // "us"
combobox.value = 'uk'; // Select United Kingdom
// Form integration
const formData = new FormData(form);
formData.get('country'); // "us"
</script>
</ry-example>
<table>
<thead><tr><th>Attribute</th><th>Values</th><th>Description</th></tr></thead>
<tbody>
<tr><td><code>placeholder</code></td><td>string</td><td>Placeholder text for input</td></tr>
<tr><td><code>name</code></td><td>string</td><td>Form field name</td></tr>
<tr><td><code>value</code></td><td>string</td><td>Currently selected value</td></tr>
<tr><td><code>disabled</code></td><td>boolean</td><td>Disable combobox</td></tr>
</tbody>
</table>
</ry-section>
<!-- Code -->
<ry-section>
<h2>Code</h2>
<ry-example title="ry-code">
<template>
<ry-code language="js" title="app.js">
const greeting = "Hello!";
console.log(greeting);
</ry-code>
</template>
</ry-example>
<table>
<thead><tr><th>Attribute</th><th>Values</th><th>Description</th></tr></thead>
<tbody>
<tr><td><code>language</code></td><td>js | css | html | json</td><td>Syntax highlighting</td></tr>
<tr><td><code>title</code></td><td>string</td><td>Header title</td></tr>
<tr><td><code>line-numbers</code></td><td>boolean</td><td>Show line numbers</td></tr>
</tbody>
</table>
<p><em>Note: Use <code><ry-code></code> with prefix to avoid conflict with native <code><code></code> tag.</em></p>
</ry-section>
<!-- Table -->
<ry-section>
<h2>Table</h2>
<ry-example title="table">
<template>
<table>
<thead>
<tr>
<th>Name</th>
<th>Role</th>
<th>Status</th>
</tr>
</thead>
<tbody>
<tr>
<td>Alice Johnson</td>
<td>Engineer</td>
<td>Active</td>
</tr>
<tr>
<td>Bob Smith</td>
<td>Designer</td>
<td>Active</td>
</tr>
<tr>
<td>Carol White</td>
<td>Manager</td>
<td>Away</td>
</tr>
</tbody>
</table>
</template>
</ry-example>
<table>
<thead><tr><th>Attribute</th><th>Values</th><th>Description</th></tr></thead>
<tbody>
<tr><td><code>data-bordered</code></td><td>boolean</td><td>Add borders to all cells</td></tr>
<tr><td><code>data-striped</code></td><td>boolean</td><td>Alternate row backgrounds</td></tr>
</tbody>
</table>
</ry-section>
<!-- Tree -->
<ry-section>
<h2>Tree</h2>
<ry-example title="tree" stacked>
<template>
<tree sortable>
<tree-item label="src" open>
<tree-item label="app" open>