Components
Oat Glassed 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 Glassed</summary>
<p>Oat Glassed 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>
<details name="same">
<summary>This is grouped with the next one</summary>
<p>Using the <code>name</code> attribute groups items like radio.</p>
</details>
<details name="same">
<summary>This is grouped with the previous one</summary>
<p>Using the <code>name</code> attribute groups items like radio.</p>
</details>
# Alert
Use role="alert" for alert styling. Set data-variant for success, warning, or error.
<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">
<strong>Info</strong> This is a default alert message.
</div>
<div role="alert" data-variant="error">
<strong>Error!</strong> Something went wrong.
</div>
# Avatar
Use .avatar class. Displays an image or initials as a circular avatar.
<span class="avatar small">S</span>
<span class="avatar">JD</span>
<span class="avatar large">AK</span>
<span class="avatar"><img src="https://i.pravatar.cc/96?img=1" alt="User"></span>
Sizes
| Class | Size |
|---|---|
.avatar.small | 1.75rem |
.avatar | 2.5rem (default) |
.avatar.large | 3.5rem |
Avatar group
Wrap avatars in .avatar-group for overlapping display.
<div class="avatar-group">
<span class="avatar"><img src="https://i.pravatar.cc/96?img=1" alt="User 1"></span>
<span class="avatar"><img src="https://i.pravatar.cc/96?img=2" alt="User 2"></span>
<span class="avatar"><img src="https://i.pravatar.cc/96?img=3" alt="User 3"></span>
<span class="avatar">+4</span>
</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="hstack">
<button class="outline">Cancel</button>
<button>Save</button>
</footer>
</article>
# Command Palette
Use <ot-command> WebComponent. Provides a searchable command palette dialog triggered globally with ⌘K / Ctrl+K. Includes fuzzy filtering, keyboard navigation, and section grouping.
<ot-command>
<dialog id="my-cmd" closedby="any">
<input type="search" placeholder="Type a command...">
<div role="listbox">
<span>Navigation</span>
<button role="option">Home</button>
<button role="option">Dashboard</button>
<button role="option">Settings</button>
<span>Actions</span>
<button role="option">New Project</button>
<button role="option">Import Data</button>
</div>
</dialog>
</ot-command>
With icons and keyboard shortcuts
Add SVGs for icons and <kbd> for shortcut hints.
<button role="option">
<svg>...</svg>
New Project
<kbd>⌘N</kbd>
</button>
Opening programmatically
Besides the global ⌘K shortcut, open via JavaScript:
document.querySelector('ot-command').open();
Handling selection
Listen for clicks on options:
document.querySelector('ot-command [role="listbox"]').addEventListener('click', e => {
const option = e.target.closest('[role="option"]');
if (option) console.log('Selected:', option.textContent);
});
Keyboard navigation
| Key | Action |
|---|---|
⌘K / Ctrl+K | Toggle command palette |
↑ / ↓ | Navigate options |
Home / End | Jump to first / last option |
Enter | Select highlighted option |
Escape | Close palette |
Structure
| Element | Purpose |
|---|---|
<ot-command> | WebComponent wrapper |
<dialog> | Modal container (auto-created if omitted) |
<input type="search"> | Search/filter input |
<div role="listbox"> | Options container |
<span> inside listbox | Section label |
<button role="option"> | Selectable option |
<kbd> inside option | Keyboard shortcut hint (auto right-aligned) |
# 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" closedby="any">
<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 class="vstack">
<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 target. If a dropdown <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>
Popover
<ot-dropdown> can also be used to show popover dropdown elements.
<ot-dropdown>
<button popovertarget="demo-confirm" class="outline">
Confirm
</button>
<article class="card" popover id="demo-confirm">
<header>
<h4>Are you sure?</h4>
<p>This action cannot be undone.</p>
</header>
<br />
<footer>
<button class="outline small" popovertarget="demo-confirm">Cancel</button>
<button data-variant="danger" class="small" popovertarget="demo-confirm">Delete</button>
</footer>
</article>
</ot-dropdown>
# Empty State
Use data-empty on a container. Centers content vertically with icon, heading, description, and optional action button.
<div data-empty>
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
<path d="M20 7h-9"/><path d="M14 17H5"/>
<circle cx="17" cy="17" r="3"/><circle cx="7" cy="7" r="3"/>
</svg>
<h4>No results found</h4>
<p>Try adjusting your search or filters to find what you're looking for.</p>
<button class="outline small">Clear filters</button>
</div>
Without action
<div data-empty>
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
<path d="M14.5 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7.5L14.5 2z"/>
<polyline points="14 2 14 8 20 8"/>
</svg>
<h4>No documents yet</h4>
<p>Create your first document to get started.</p>
</div>
Structure
| Element | Purpose |
|---|---|
[data-empty] | Container with centered flex layout |
<svg> or <img> | Illustration (3rem, semi-transparent) |
<h4> (any heading) | Title |
<p> | Description (max-width 28rem) |
<button> or <a> | Action button |
# 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>Select</label>
<select aria-label="Select an option">
<option value="">Select an option</option>
<option value="a">Option A</option>
<option value="b">Option B</option>
<option value="c">Option C</option>
<option value="d">Option D</option>
<option value="e">Option E</option>
<option value="f">Option F</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<br />
<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 aria-label="Select a subdomain">
<option value="" disabled selected>Select</option>
<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-describedby="error-message" 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>
# Pagination
Pagination does not use any special markup or classes and re-uses the exiting buttons <menu>.
<nav aria-label="Pagination">
<menu class="buttons">
<li><a href="#pagination" class="button outline small">← Previous</a></li>
<li><a href="#pagination" class="button outline small">1</a></li>
<li><a href="#pagination" class="button outline small">2</a></li>
<li><a href="#pagination" class="button small" aria-current="page">3</a></li>
<li><a href="#pagination" class="button outline small">4</a></li>
<li><a href="#pagination" class="button outline small">5</a></li>
<li><a href="#pagination" class="button outline small">Next →</a></li>
</menu>
</nav>
# 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 aria-busy="true" on any element to show a loading indicator. Size with data-spinner="small|large".
<div class="hstack" style="gap: var(--space-8)">
<div aria-busy="true" data-spinner="small"></div>
<div aria-busy="true"></div>
<div aria-busy="true" data-spinner="large"></div>
<button aria-busy="true" data-spinner="small" disabled>Loading</button>
</div>
Overlay
Adding data-spinner="overlay" dims contents of the container and overlays the spinner on top.
<article class="card" aria-busy="true" data-spinner="large overlay">
<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>
# 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. Wrap in a class="table" container to get a horizontal scrollbar on small screens.
<div class="table">
<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>
</div>
# Tag
Use .tag class. Like badges but designed for interactive use — filtering, selections, and dismissible items.
<span class="tag">Default</span>
<span class="tag primary">Primary</span>
<span class="tag success">Success</span>
<span class="tag warning">Warning</span>
<span class="tag danger">Danger</span>
Dismissible
Add a <button> inside the tag for dismiss functionality.
<span class="tag">React <button aria-label="Remove">×</button></span>
<span class="tag success">Published <button aria-label="Remove">×</button></span>
Handle removal with a click listener:
container.addEventListener('click', e => {
const btn = e.target.closest('.tag > button');
if (btn) btn.parentElement.remove();
});
Variants
| Class | Description |
|---|---|
.tag | Default (secondary background with glass) |
.tag.primary | Primary color |
.tag.success | Success / green |
.tag.warning | Warning / amber |
.tag.danger | Danger / red |
# 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. Replaced elements like <img>, <iframe> etc. need to be wrapped in a parent with the title attribute.
<button title="Save your changes">Save</button>
<button title="Delete this item" data-variant="danger">Delete</button>
<a href="#" title="View your profile">Profile</a>
<span title="Images need a parent with title"><img src="https://good-lly.github.io/oat-glassed/logo.svg" height="32" /></span>
# Toast
Show toast notifications with ot.toast(message, title?, 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' })" data-variant="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 attention', { 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.toast.el(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 data-variant="secondary" class="small" onclick="this.closest('.toast').remove()">Okay</button>
</output>
</template>
<button onclick="ot.toast.el(document.querySelector('#undo-toast'), { duration: 8000 })">
Toast with action
</button>
From a template:
ot.toast.el(document.querySelector('#my-template'))
ot.toast.el(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.toast.el(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>
# Utils and helpers
See utilities.css for commonly used utility and helper classes.
Link