Components
Oat is an ultra-lightweight HTML + CSS + minimal JS, semantic UI component library with zero dependencies. No framework or build or dev dependencies of any kind. Just include the tiny CSS and JS bundles.
Semantic tags and attributes are styled contextually out of the box without classes, thereby forcing best practices. A few dynamic components are WebComponents.
# Typography
Base text elements are styled automatically. No classes needed.
<h1>Heading 1</h1>
<h2>Heading 2</h2>
<h3>Heading 3</h3>
<h4>Heading 4</h4>
<h5>Heading 5</h5>
<h6>Heading 6</h6>
<p>This is a paragraph with <strong>bold text</strong>, <em>italic text</em>, and <a href="#">a link</a>.</p>
<p>Here's some <code>inline code</code> and a code block:</p>
<pre><code>function hello() {
console.log('Hello, World!');
}</code></pre>
<blockquote>
This is a blockquote. It's styled automatically.
</blockquote>
<hr>
<ul>
<li>Unordered list item 1</li>
<li>Unordered list item 2</li>
<li>Unordered list item 3</li>
</ul>
<ol>
<li>Ordered list item 1</li>
<li>Ordered list item 2</li>
<li>Ordered list item 3</li>
</ol>
# Accordion
Use native <details> and <summary> for collapsible content.
<details>
<summary>What is Oat?</summary>
<p>Oat is a minimal, semantic-first UI component library with zero dependencies.</p>
</details>
<details>
<summary>How do I use it?</summary>
<p>Include the CSS and JS files, then write semantic HTML. Most elements are styled by default.</p>
</details>
<details>
<summary>Is it accessible?</summary>
<p>Yes! It uses semantic HTML and ARIA attributes. Keyboard navigation works out of the box.</p>
</details>
# Alert
Use role="alert" for alert styling. Set data-variant for success, warning, or error.
<div role="alert">
<strong>Default Alert</strong> - This is a default alert message.
</div>
<div role="alert" data-variant="success">
<strong>Success!</strong> - Your changes have been saved.
</div>
<div role="alert" data-variant="warning">
<strong>Warning!</strong> - Please review before continuing.
</div>
<div role="alert" data-variant="error">
<strong>Error!</strong> - Something went wrong.
</div>
# Badge
Use .badge class with variant modifiers.
<span class="badge">Default</span>
<span class="badge secondary">Secondary</span>
<span class="badge outline">Outline</span>
<span class="badge success">Success</span>
<span class="badge warning">Warning</span>
<span class="badge danger">Danger</span>
# Card
Use class="card" for a visual box-like card look.
<article class="card">
<header>
<h3>Card Title</h3>
<p>Card description goes here.</p>
</header>
<p>This is the card content. It can contain any HTML.</p>
<footer class="flex gap-2 mt-4">
<button class="outline">Cancel</button>
<button>Save</button>
</footer>
</article>
# Dialog
Fully semantic, zero-Javascript, dynamic dialog with <dialog>. Use commandfor and command="show-modal" attributes on an element to open a target dialog. Focus trapping, z placement, keyboard shortcuts all work out of the box.
<button commandfor="demo-dialog" command="show-modal">Open dialog</button>
<dialog id="demo-dialog">
<form method="dialog">
<header>
<h3>Title</h3>
<p>This is a dialog description.</p>
</header>
<div>
<p>Dialog content goes here. You can put any HTML inside.</p>
<p>Click outside or press Escape to close.</p>
</div>
<footer>
<button type="button" commandfor="demo-dialog" command="close" class="outline">Cancel</button>
<button value="confirm">Confirm</button>
</footer>
</form>
</dialog>
With form fields
Forms inside dialogs work naturally. Use command="close" on cancel buttons to close.
<button commandfor="demo-dialog-form" command="show-modal">Open form dialog</button>
<dialog id="demo-dialog-form">
<form method="dialog">
<header>
<h3>Edit form</h3>
</header>
<div>
<label>Name <input name="name" required></label>
<label>Email <input name="email" type="email"></label>
</div>
<footer>
<button type="button" commandfor="demo-dialog-form" command="close" class="outline">Cancel</button>
<button value="save">Save</button>
</footer>
</form>
</dialog>
Handling return value
Listen to the native close event to get the button value:
const dialog = document.querySelector("#demo-dialog");
dialog.addEventListener('close', (e) => {
console.log(dialog.returnValue); // "confirm"
});
or use onclose inline:
<dialog id="my-dialog" onclose="console.log(this.returnValue)">
Link
# Dropdown
Wrap in <ot-dropdown>. Use popovertarget on the trigger and popover on the <menu>. Items use role="menuitem".
<ot-dropdown>
<button popovertarget="demo-menu" class="outline">
Options
<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>
<menu popover id="demo-menu">
<button role="menuitem">Profile</button>
<button role="menuitem">Settings</button>
<button role="menuitem">Help</button>
<hr>
<button role="menuitem">Logout</button>
</menu>
</ot-dropdown>
# Form elements
Form elements are styled automatically. Wrap inputs in <label> for proper association.
<form>
<label data-field>
Name
<input type="text" placeholder="Enter your name" />
</label>
<label data-field>
Email
<input type="email" placeholder="you@example.com" />
</label>
<label data-field>
Password
<input type="password" placeholder="Password" aria-describedby="password-hint" />
<small id="password-hint" data-hint>This is a small hint</small>
</label>
<div data-field>
<label>Country</label>
<select>
<option value="">Select a country</option>
<option value="us">United States</option>
<option value="uk">United Kingdom</option>
<option value="ca">Canada</option>
</select>
</div>
<label data-field>
Message
<textarea placeholder="Your message..."></textarea>
</label>
<label data-field>
Disabled
<input type="text" placeholder="Disabled" disabled />
</label>
<label data-field>
File
<input type="file" placeholder="Pick a file..." />
</label>
<label data-field>
Date and time
<input type="datetime-local" />
</label>
<label data-field>
Date
<input type="date" />
</label>
<label data-field>
<input type="checkbox" /> I agree to the terms
</label>
<fieldset class="hstack">
<legend>Preference</legend>
<label><input type="radio" name="pref">OptionA</label>
<label><input type="radio" name="pref">Option B</label>
<label><input type="radio" name="pref">Option C</label>
</fieldset>
<label data-field>
Volume
<input type="range" min="0" max="100" value="50" />
</label>
<button type="submit">Submit</button>
</form>
Input group
Use .group on a <fieldset> to combine inputs with buttons or labels.
<fieldset class="group">
<legend>https://</legend>
<input type="url" placeholder="Subdomain">
<select placeholder="Select">
<option>.example.com</option>
<option>.example.net</option>
</select>
<button>Go</button>
</fieldset>
<fieldset class="group">
<input type="text" placeholder="Search" />
<button>Go</button>
</fieldset>
Validation error
Use data-field="error" on field containers to reveal and style error messages.
<div data-field="error">
<label for="error-input">Email</label>
<input type="email" aria-invalid="true" aria-errormessage="error-input"
id="error-input" value="invalid-email" />
<div id="error-message" class="error" role="status">Please enter a valid email address.</div>
</div>
# Meter
Use <meter> for values within a known range. Browser shows colors based on low/high/optimum attributes.
<meter value="0.8" min="0" max="1" low="0.3" high="0.7" optimum="1"></meter>
<meter value="0.5" min="0" max="1" low="0.3" high="0.7" optimum="1"></meter>
<meter value="0.2" min="0" max="1" low="0.3" high="0.7" optimum="1"></meter>
# Progress
Use the native <progress> element.
<progress value="60" max="100"></progress>
<progress value="30" max="100"></progress>
<progress value="90" max="100"></progress>
# Spinner
Use .spinner with role="status" for loading indicators. Size with .small or .large.
<div role="status" class="spinner small"></div>
<div role="status" class="spinner"></div>
<div role="status" class="spinner large"></div>
# Skeleton
Use .skeleton with role="status" for loading placeholders. Add .line for text or .box for images.
<div role="status" class="skeleton line"></div>
<div role="status" class="skeleton box"></div>
Skeleton card
Put skeleton loader inside <article> to get a card layout.
<article style="display: flex; gap: var(--space-3); padding: var(--space-6);">
<div role="status" class="skeleton box"></div>
<div style="flex: 1; display: flex; flex-direction: column; gap: var(--space-1);">
<div role="status" class="skeleton line"></div>
<div role="status" class="skeleton line" style="width: 60%"></div>
</div>
</article>
# Switch
Add role="switch" to a checkbox for toggle switch styling.
<label>
<input type="checkbox" role="switch"> Notifications
</label>
<label>
<input type="checkbox" role="switch" checked> Confabulation
</label>
Disabled
<label>
<input type="checkbox" role="switch" disabled> Disabled off
</label>
<label>
<input type="checkbox" role="switch" checked disabled> Disabled on
</label>
# Table
Tables are styled by default. Use <thead> and <tbody> tags.
<table>
<thead>
<tr>
<th>Name</th>
<th>Email</th>
<th>Role</th>
<th>Status</th>
</tr>
</thead>
<tbody>
<tr>
<td>Alice Johnson</td>
<td>alice@example.com</td>
<td>Admin</td>
<td><span class="badge success">Active</span></td>
</tr>
<tr>
<td>Bob Smith</td>
<td>bob@example.com</td>
<td>Editor</td>
<td><span class="badge">Active</span></td>
</tr>
<tr>
<td>Carol White</td>
<td>carol@example.com</td>
<td>Viewer</td>
<td><span class="badge secondary">Pending</span></td>
</tr>
</tbody>
</table>
# Tabs
Wrap tab buttons and panels in <ot-tabs>. Use role="tablist", role="tab", and role="tabpanel".
<ot-tabs>
<div role="tablist">
<button role="tab">Account</button>
<button role="tab">Password</button>
<button role="tab">Notifications</button>
</div>
<div role="tabpanel">
<h3>Account Settings</h3>
<p>Manage your account information here.</p>
</div>
<div role="tabpanel">
<h3>Password Settings</h3>
<p>Change your password here.</p>
</div>
<div role="tabpanel">
<h3>Notification Settings</h3>
<p>Configure your notification preferences.</p>
</div>
</ot-tabs>
# Tooltip
Use the standard title attribute on any element to render a tooltip with smooth transition.
<button title="Save your changes">Save</button>
<button title="Delete this item" class="danger">Delete</button>
<a href="#" title="View your profile">Profile</a>
# Toast
Show toast notifications with ot.toast(message, options?).
<button onclick="ot.toast('Action completed successfully', 'All good', { variant: 'success' })">Success</button>
<button onclick="ot.toast('Something went wrong', 'Oops', { variant: 'danger', placement: 'top-left' })" class="danger">Danger</button>
<button onclick="ot.toast('Please review this warning', 'Warning', { variant: 'warning', placement: 'bottom-right' })" class="outline">Warning</button>
<button onclick="ot.toast('New notification', 'For your attenton', { placement: 'top-center' })">Info</button>
Placement
ot.toast('Top left', '', { placement: 'top-left' })
ot.toast('Top center', '',{ placement: 'top-center' })
ot.toast('Top right', '',{ placement: 'top-right' }) // default
ot.toast('Bottom left', '', { placement: 'bottom-left' })
ot.toast('Bottom center', '', { placement: 'bottom-center' })
ot.toast('Bottom right', '',{ placement: 'bottom-right' })
Options
| Option | Default | Description |
|---|---|---|
variant | '' | 'success', 'danger', 'warning' |
placement | 'top-right' | Position on screen |
duration | 4000 | Auto-dismiss in ms (0 = persistent) |
Custom markup
Use ot.toastEl(element, options?) to show toasts with custom HTML content.
<template id="undo-toast">
<output class="toast" data-variant="success">
<h6 class="toast-title">Changes saved</h6>
<p>Your document has been updated.</p>
<button class="secondary small" onclick="this.closest('.toast').remove()">Okay</button>
</output>
</template>
<button onclick="ot.toastEl(document.querySelector('#undo-toast'), { duration: 8000 })">
Toast with action
</button>
From a template:
ot.toastEl(document.querySelector('#my-template'))
ot.toastEl(document.querySelector('#my-template'), { duration: 8000, placement: 'bottom-center' })
Dynamic element:
const el = document.createElement('output');
el.className = 'toast';
el.setAttribute('data-variant', 'warning');
el.innerHTML = '<h6 class="toast-title">Warning</h6><p>Custom content here</p>';
ot.toastEl(el);
The element is cloned before display, so templates can be reused.
Clearing toasts
ot.toast.clear() // Clear all
ot.toast.clear('top-right') // Clear specific placement
Link
# Grid
A 12-column grid system using CSS grid. Use .container, .row, and .col classes. Column widths use .col-{n} where n is 1-12.
<div class="container demo-grid">
<div class="row">
<div class="col-4">col-4</div>
<div class="col-4">col-4</div>
<div class="col-4">col-4</div>
</div>
<div class="row">
<div class="col-6">col-6</div>
<div class="col-6">col-6</div>
</div>
<div class="row">
<div class="col-3">col-3</div>
<div class="col-6">col-6</div>
<div class="col-3">col-3</div>
</div>
<div class="row">
<div class="col-4 offset-2">col-4 offset-2</div>
<div class="col-4">col-4</div>
</div>
<div class="row">
<div class="col-3">col-3</div>
<div class="col-4 col-end">col-4 col-end</div>
</div>
</div>