Recipes
Examples for various composable widgets using Oat components.
Split button
Use menu.buttons for joined controls and ot-dropdown for secondary actions.
<ot-dropdown>
<menu class="buttons">
<li><button class="outline">Save</button></li>
<li>
<button class="outline" popovertarget="save-actions" aria-label="More save actions">
More
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="m6 9 6 6 6-6" /></svg>
</button>
</li>
</menu>
<menu popover id="save-actions">
<button role="menuitem" class="ghost">Save draft</button>
<button role="menuitem" class="ghost">Save and publish</button>
<button role="menuitem" class="ghost">Duplicate</button>
</menu>
</ot-dropdown>
Radio cards
Wrap each option in a <label> so the whole card is selectable, and group them in a <fieldset> with a <legend>.
<fieldset class="w-100">
<legend>Billing</legend>
<p class="text-light">Select a billing cycle</p>
<div class="row">
<label class="col-4 card vstack">
<span class="w-100 hstack justify-between">
<strong>Monthly</strong>
<input type="radio" name="billing">
</span>
<span class="text-light">$12 / mo</span>
</label>
<label class="col-4 card vstack">
<span class="w-100 hstack justify-between">
<strong>Yearly</strong>
<input type="radio" name="billing">
</span>
<span class="text-light">$96 / yr ยท save 33%</span>
</label>
<label class="col-4 card vstack">
<span class="w-100 hstack justify-between">
<strong>Lifetime</strong>
<input type="radio" name="billing" checked>
</span>
<span class="text-light">$299 once</span>
</label>
</div>
</fieldset>
Form card
Group related form fields inside a card with standard field containers and actions.
<article class="card">
<header>
<h3>Profile</h3>
<p class="text-light">Update account information</p>
</header>
<div class="mt-4">
<label data-field>
Name
<input type="text" value="Your name" />
</label>
<label data-field>
Email
<input type="email" value="mila@example.com" />
</label>
<label data-field>
<input type="checkbox" role="switch" checked> Email notifications
</label>
</div>
<footer class="hstack justify-end mt-4">
<button class="outline">Cancel</button>
<button>Save</button>
</footer>
</article>
Empty state
Use a card, text, and primary actions for list/result empty states.
<article class="card align-center">
<h3>Nothing here yet</h3>
<p class="text-light">Why don't you create something?</p>
<footer class="hstack justify-center mt-4">
<button>New something</button>
</footer>
</article>
Stats cards
Compose dashboard metrics with grid, card, badge, and progress/meter.
<div class="container">
<div class="row">
<article class="card col-4">
<header class="hstack justify-between items-center">
<h4>Revenue</h4>
<span class="badge" data-variant="success">+12%</span>
</header>
<h2>$42,200</h2>
<p class="text-light">vs last month</p>
<progress value="72" max="100"></progress>
</article>
<article class="card col-4">
<header class="hstack justify-between items-center">
<h4>Completion</h4>
<span class="badge" data-variant="warning">-2%</span>
</header>
<h2>4.6%</h2>
<p class="text-light">checkout completion</p>
<meter value="0.46" min="0" max="1" low="0.3" high="0.7" optimum="1"></meter>
</article>
<article class="card col-4">
<header class="hstack justify-between items-center">
<h4>Tickets</h4>
<span class="badge">14</span>
</header>
<h2>14</h2>
<p class="text-light">support queue</p>
<progress value="35" max="100"></progress>
</article>
</div>
</div>
Download button
Add the download attribute to a hyperlink to trigger a file download. Style it with .button and any button variant, same as any other link.
<a href="/oat.min.css" download class="button">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><path d="M12 4v12m0 0-4-4m4 4 4-4" /><path d="M4 16v4h16v-4" /></svg>
Download CSS
</a>
<a href="/oat.min.js" download class="outline">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><path d="M12 4v12m0 0-4-4m4 4 4-4" /><path d="M4 16v4h16v-4" /></svg>
Download JS
</a>
Copy-to-clipboard button
An icon button that copies text and briefly swaps to a checkmark icon to confirm the action, using the Clipboard API and the native hidden attribute. No new component or CSS state needed.
<button class="icon outline" aria-label="Copy install command" onclick="navigator.clipboard.writeText('npm install oat-ui'); this.querySelector('[data-icon=copy]').hidden = true; this.querySelector('[data-icon=check]').hidden = false; setTimeout(() => { this.querySelector('[data-icon=copy]').hidden = false; this.querySelector('[data-icon=check]').hidden = true }, 1500)">
<svg data-icon="copy" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="9" y="9" width="13" height="13" rx="2" ry="2" /><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1" /></svg>
<svg data-icon="check" hidden width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><polyline points="20 6 9 17 4 12" /></svg>
</button>
For anything beyond a one-liner, wire the same swap up as a real event listener instead of an inline attribute:
button.addEventListener('click', async () => {
await navigator.clipboard.writeText('npm install oat-ui');
button.querySelector('[data-icon=copy]').hidden = true;
button.querySelector('[data-icon=check]').hidden = false;
setTimeout(() => {
button.querySelector('[data-icon=copy]').hidden = false;
button.querySelector('[data-icon=check]').hidden = true;
}, 1500);
});Loading button
Toggle aria-busy="true" and disabled on submit to show the built-in spinner while an action is pending. No new markup or JS beyond the one-line handler.
<form onsubmit="event.preventDefault(); const b = this.querySelector('button'); b.setAttribute('aria-busy', 'true'); b.disabled = true">
<button type="submit" data-spinner="small">Save</button>
</form>
Confirm-before-delete dialog
Reuse the native <dialog> commandfor/command pattern to confirm a destructive action before it runs. Zero JavaScript.
<button data-variant="danger" class="outline" commandfor="confirm-delete" command="show-modal">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><polyline points="3 6 5 6 21 6" /><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2" /><line x1="10" y1="11" x2="10" y2="17" /><line x1="14" y1="11" x2="14" y2="17" /></svg>
Delete
</button>
<dialog id="confirm-delete" closedby="any">
<form method="dialog">
<header>
<h3>Delete item</h3>
<p>This action cannot be undone.</p>
</header>
<footer>
<button type="button" commandfor="confirm-delete" command="close" class="outline">Cancel</button>
<button value="confirm" data-variant="danger">Delete</button>
</footer>
</form>
</dialog>