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>
Link

# 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>
Link

# 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>
Link

# 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

ClassSize
.avatar.small1.75rem
.avatar2.5rem (default)
.avatar.large3.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>
Link

# 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>
Link

# Button

The <button> element is styled by default. Use data-variant="primary|secondary|danger" for semantic variants and classes for visual styles.

<button>Primary</button>
<button data-variant="secondary">Secondary</button>
<button data-variant="danger">Danger</button>
<button class="outline">Outline</button>
<button data-variant="danger" class="outline">Danger</button>
<button class="ghost">Ghost</button>
<button disabled>Disabled</button>

Sizes

Use .small or .large for size variants.

<button class="small">Small</button>
<button>Default</button>
<button class="large">Large</button>
<a href="#button" class="button">Hyperlink</a>

Button group

Wrap buttons in <menu class="buttons"> for connected buttons.

<menu class="buttons">
  <li><button class="outline">Left</button></li>
  <li><button class="outline">Center</button></li>
  <li><button class="outline">Right</button></li>
</menu>
Link

# 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>
Link

# 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

KeyAction
⌘K / Ctrl+KToggle command palette
/ Navigate options
Home / EndJump to first / last option
EnterSelect highlighted option
EscapeClose palette

Structure

ElementPurpose
<ot-command>WebComponent wrapper
<dialog>Modal container (auto-created if omitted)
<input type="search">Search/filter input
<div role="listbox">Options container
<span> inside listboxSection label
<button role="option">Selectable option
<kbd> inside optionKeyboard shortcut hint (auto right-aligned)
Link

# 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

# 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

ElementPurpose
[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
Link

# 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>
Link

# 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>
Link

# 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">&larr; 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 &rarr;</a></li>
    </menu>
</nav>
Link

# Progress

Use the native <progress> element.

<progress value="60" max="100"></progress>
<progress value="30" max="100"></progress>
<progress value="90" max="100"></progress>
Link

# 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>
Link

# 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>
Link

# 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>
Link

# 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>
Link

# 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">&times;</button></span>
<span class="tag success">Published <button aria-label="Remove">&times;</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

ClassDescription
.tagDefault (secondary background with glass)
.tag.primaryPrimary color
.tag.successSuccess / green
.tag.warningWarning / amber
.tag.dangerDanger / red
Link

# 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>
Link

# 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>
Link

# 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

OptionDefaultDescription
variant'''success', 'danger', 'warning'
placement'top-right'Position on screen
duration4000Auto-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>
Link

# Utils and helpers

See utilities.css for commonly used utility and helper classes.

Link