Analyzing Oat — Ultra-Lightweight Semantic UI Library
Overview
Oat is a ~8KB (minified + gzipped) zero-dependendency UI component library built with vanilla HTML, CSS, and JavaScript by Kailash Nadh. It was written out of frustration with the complexity and dependency churn of mainstream JavaScript UI frameworks. The project prioritizes semantic HTML, accessibility (ARIA roles, keyboard navigation), and long-term stability over feature breadth. Components like tabs and dropdowns use native WebComponents with progressive enhancement — they render meaningful HTML without JS, and enhance with interactivity when JS loads. The build system is a 30-line Makefile invoking esbuild; no Node.js ecosystem bloat.
How It Works
The library has two independent halves that are concatenated/bundled into a single CSS file and a single JS file:
CSS (6KB min+gz): 22 source files organized into CSS @layer cascades (theme → base → components → animations → utilities). The theme layer defines ~40 CSS custom properties for colors, spacing, typography, radii, shadows, and transitions. The base layer resets and styles native HTML elements (<h1>-<h6>, <p>, <button>, <input>, etc.) using element selectors and ARIA attribute selectors ([role="tab"]) rather than utility classes. Component layers then target complex composites like dialog, menu.buttons, and [popover].
JS (2.2KB min+gz): Five source files. A base class (OtBase) that all WebComponents extend, plus four component-specific files. The JS is a pure IIFE bundle — no module system, no imports at runtime, just window.ot.toast() API and custom element registration.
Note
There are no runtime dependencies. No build step is required to use Oat — just link
oat.min.cssandoat.min.jsin any HTML page.
Architecture
src/
css/
00-base.css # CSS reset + element styling + @layer declarations
01-theme.css # :root CSS custom properties (colors, spacing, radii, etc.)
accordion.css
alert.css
animations.css
avatar.css
badge.css
button.css
card.css
dialog.css
dropdown.css
form.css
grid.css
progress.css
sidebar.css
skeleton.css
spinner.css
table.css
tabs.css
toast.css
tooltip.css
utilities.css
js/
base.js # OtBase WebComponent superclass
index.js # Entry point — registers all components, exposes window.ot API
dropdown.js # OtDropdown custom element
sidebar.js # Sidebar toggle (no class, pure event delegation)
tabs.js # OtTabs custom element
toast.js # Toast notification system (module-style functions)
tooltip.js # title → data-tooltip progressive enhancement
The entry point registers:
<ot-tabs>— keyboard-navigable tabs with ARIA<ot-dropdown>— dropdown menu with arrow-key nav and viewport-flip positioning<ot-toast>,ot.toast()— toast notification API- Tooltip enhancement via
MutationObserver(convertstitleattributes) - Sidebar toggle via global event delegation
command/commandforpolyfill for<dialog>control
The Spine
Entry point for the user: Link oat.min.css + oat.min.js in HTML. No initialization call needed.
Entry point for JS: src/js/index.js imports all modules and attaches window.ot globals. When the <script> executes, it calls customElements.define() for each component. The browser handles registration; when matching elements appear in the DOM (or are already present), the custom element constructors run.
Request lifecycle for a <ot-tabs> interaction:
- User clicks a tab button.
- Browser fires
clickon the<ot-tabs>shadow-equivalent container (event bubbles). OtTabs.handleEvent()dispatches toonclick()via theonclickmethod naming convention.onclick()finds the clicked tab index, calls#activate(idx).#activate()updatesaria-selected,tabindex, andhiddenon all tabs/panels.this.emit('ot-tab-change', ...)fires aCustomEventon the custom element.
Request lifecycle for ot.toast('message'):
toast()creates an<output>element withdata-variant._show()appends it to a container<div>, calls.showPopover()(Popover API).- Double
requestAnimationFrametriggers CSS enter animation. setTimeoutqueues removal afterdurationms._remove()addsdata-exitingattribute → CSS transition plays →transitionendfires cleanup.
Key Patterns
Semantic-first, classless styling
CSS targets native elements and ARIA attributes directly:
/* No class required — works on <button> anywhere */
:is(button, [type=submit]) { ... }
/* ARIA-driven — just add role="tablist" */
[role="tablist"] { display: flex; }
[role="tab"] { /* tab styling */ }CSS @layer cascade
Layers provide a clean specificity contract. Theme variables are in the lowest layer; component styles in higher layers can override without specificity battles:
@layer theme, base, components, animations, utilities;WebComponent base class (OtBase)
All interactive components extend OtBase, which provides:
connectedCallback/disconnectedCallbackorchestration with init-once guardhandleEvent()method naming convention —onclick,onkeydown,ontogglemethods are auto-routedkeyNav()— roving keyboard navigation helper (arrow keys, home/end)emit()— firesCustomEventwithbubbles: true, composed: trueuid()— generates a random ID string (10-char, base-36)$(selector)/$$(selector)— query helpers scoped to the component
Progressive enhancement via Popover API
Toast containers and dropdowns use the native <div popover> pattern. This gives free backdrop handling, stacking context, click-outside dismissal, and keyboard accessibility without Oat JS implementing any of it.
Command polyfill
A small event listener in base.js handles command/commandfor attributes on buttons, enabling <button commandfor="my-dialog" command="show-modal">Open</button> without a framework. This is a hand-rolled alternative to the command HTML attribute that’s not yet universally supported.
Toast double-RAF animation pattern
requestAnimationFrame(() => {
requestAnimationFrame(() => {
el.removeAttribute('data-entering');
});
});This ensures the browser has computed initial styles (data-entering present) before removing the attribute, so CSS transitions fire correctly. Without this, the “enter” transition wouldn’t play on first load because the class would be removed before the browser painted.
Non-Obvious Details
The @starting-style dialog animations
dialog.css uses the CSS @starting-style at-rule to define the “before-open” state for enter animations. This is a relatively new CSS feature that defines a style for the element at the moment it opens. Combined with the allow-discrete transition syntax, it enables the “opacity 0 + scale 0.95 → opacity 1 + scale 1” dialog open animation without a JS animation library.
Viewport-flip positioning in dropdown
OtDropdown manually calculates and sets top/left on the <menu popover> element on open and on scroll/resize. The Popover API positions menus as position: fixed relative to the viewport, not the trigger. The flip logic (r.bottom + m.height > window.innerHeight ? r.top - m.height : r.bottom) ensures the menu never clips off-screen.
Toast transitionend timeout fallback
toast.js reads getComputedStyle(el).getPropertyValue('--transition') and parses both ms and s units. If transitionend never fires (e.g., user has reduced-motion enabled or the element was hidden), the setTimeout backup fires regardless. This is defensive programming for a cross-browser edge case.
MutationObserver for tooltip enhancement
tooltip.js sets up a MutationObserver on document.body watching childList, subtree, and title attribute changes. This catches dynamically inserted elements with title attributes — not just page load. It also removes the native title attribute to suppress the browser’s default tooltip, replacing it with the custom styled data-tooltip approach.
Touch backdrop dismissal
base.js has a touchstart listener that calls e.preventDefault() on <dialog> elements. This prevents the “ghost tap” problem on touch devices where a tap on the dialog backdrop would propagate to the element below after the dialog closes.
Sidebar is pure event delegation
ssidebar.js is not a WebComponent — it’s a single global event listener on document that handles [data-sidebar-toggle] clicks and dismisses on outside clicks. No class, no registration, just side effects.
Assessment
Strengths:
- Tiny, predictable bundle size — no supply chain risk, no version drift, no hidden bloat.
- Genuine semantic HTML and ARIA correctness — buttons are
<button>, tabs userole="tablist", dialogs are<dialog>. - Uses modern browser APIs (Popover,
@starting-style,light-dark(),color-scheme) without polyfills where avoidable. - Zero tests, zero CI — but the code is simple enough that it arguably doesn’t need a heavy test suite. The author knows what they shipped.
- Framework-agnostic: works in any HTML context regardless of frontend framework.
Concerns:
uid()usesMath.random()— not cryptographically random and guessable. Fine for UI IDs in non-security contexts; notable if IDs are ever used for anything beyond DOM targeting.- No versioning tags in git (
package.jsonhas"version-0.0.0"placeholder). Publishing usesgit describe --tagsbut there are no tags yet. - Sub-v1 with no changelog or migration guide — API surface is small enough that this is manageable, but a stability guarantee would inspire more production confidence.
- No test suite at all. While the simplicity mitigates risk, even a Playwright smoke test for each component would catch regressions.
- No accessibility testing in CI (e.g.,
axe-core,jest-axe).
Recommendations:
- If adopting: pin to a specific commit SHA rather than
latestuntil v1 ships. - If contributing: add Playwright or Playwright-component tests for keyboard nav and ARIA state on
<ot-tabs>and<ot-dropdown>. - Consider
@starting-stylebrowser support — it requires Safari 17.5+ and Chrome 117+. The dialog enter animation will be instant on older browsers, which is graceful degradation rather than a failure, but worth noting.
Tip
The library is well-suited for projects that want a decent-looking UI without a framework, or for embedding a lightweight UI layer inside a larger application that already uses a different primary framework. The shadcn-inspired aesthetic (neutral tones, subtle shadows, clean spacing) hits modern design expectations without effort.