Zero-JS microinteractions are lightweight user interface moments—toggles, tooltips, accordions, small animations—implemented without JavaScript; this guide shows practical, accessible patterns using HTML, CSS (including the CSS @property Houdini-ish feature) and declarative animations so you can replace common JavaScript microinteractions for better performance and maintainability.
Why choose Zero-JS microinteractions?
Shipping fewer scripts reduces page weight, lowers runtime complexity, and avoids layout thrashing from imperative DOM updates. Where possible, native HTML elements (like <details>, <input>, <label>) plus CSS selectors and animations provide the same UX while preserving keyboard support and improving a11y. The trick is using CSS techniques that gracefully degrade and respecting user preferences such as prefers-reduced-motion.
Core patterns and examples
1. Accessible accordion with <details>
<details> is semantic, keyboard-friendly, and exposes an open attribute that CSS can target—no JS required.
<details class="accordion">
<summary>Shipping options</summary>
<div class="panel">
<p>Free shipping over $50. Delivery in 3–5 days.</p>
</div>
</details>
<style>
.accordion summary { cursor: pointer; list-style: none; padding: .75rem; background: #f5f7fa; }
.accordion[open] summary { background: #e8eef8; }
.accordion .panel { padding: .75rem; border-top: 1px solid #e1e6ef; animation: fadeIn .24s ease; }
@keyframes fadeIn { from { opacity: 0; transform: translateY(-4px); } to { opacity: 1; transform: none; } }
:root { color-scheme: light dark; }
/* respect reduced motion */
@media (prefers-reduced-motion: reduce) { .accordion .panel { animation: none; } }
</style>
2. Toggle switches with hidden checkbox
Use an input[type=”checkbox”] with a visually styled label so keyboard users can toggle via space/enter by default.
<input type="checkbox" id="notif" class="visually-hidden">
<label for="notif" class="switch" aria-hidden="false">
<span class="knob"></span> Notifications
</label>
<style>
.visually-hidden { position: absolute; left: -9999px; }
.switch { display:inline-flex; align-items:center; gap:.5rem; cursor:pointer; user-select:none; }
.switch .knob { width:2rem; height:1.1rem; background:#ddd; border-radius:999px; position:relative; transition: background .2s; }
.switch .knob::after { content:""; position:absolute; left:3px; top:3px; width:.9rem; height:.9rem; background:#fff; border-radius:50%; transition: transform .22s; box-shadow: 0 1px 2px rgba(0,0,0,.08); }
#notif:checked + .switch .knob { background: #0570fb; }
#notif:checked + .switch .knob::after { transform: translateX(0.9rem); }
/* keyboard focus */
#notif:focus + .switch { outline: 3px solid #cfe3ff; outline-offset: 2px; }
</style>
3. Tabs with radio inputs (semantic, zero-JS)
Radio inputs grouped by name provide single-selection semantics and keyboard control; CSS shows the selected panel through :checked. Add roles/labels for ARIA where possible; with JS you can sync ARIA attributes later, but radio patterns are usable without it.
<div class="tabs">
<input type="radio" name="tabs" id="t1" checked>
<label for="t1">Overview</label>
<input type="radio" name="tabs" id="t2">
<label for="t2">Specs</label>
<section class="panel p1">Overview content...</section>
<section class="panel p2">Specs content...</section>
</div>
<style>
.tabs input { position:absolute; left:-9999px; }
.tabs .panel { display:none; padding:1rem; border:1px solid #eee; }
#t1:checked ~ .p1, #t2:checked ~ .p2 { display:block; animation: fadeIn .18s ease; }
</style>
4. Tooltips and transient microcopy
Create tooltips with pure CSS using :hover and :focus-within; ensure the trigger has aria-describedby pointing to the tooltip id so screen readers can reference it.
<button aria-describedby="tip1" class="has-tip">Help</button>
<div role="tooltip" id="tip1" class="tooltip">Shortcut: H</div>
<style>
.has-tip + .tooltip { opacity:0; transform: translateY(.25rem); transition:.16s; pointer-events:none; }
.has-tip:focus + .tooltip,
.has-tip:hover + .tooltip { opacity:1; transform:none; pointer-events:auto; }
.tooltip { background:#111; color:#fff; padding:.4rem .6rem; border-radius:4px; font-size:.875rem; }
</style>
5. Smooth progress and counters using @property
@property lets you animate custom properties declaratively in CSS without JS registration, improving transitions for nonstandard values (check browser support). This is very useful for progress bars or animated counters triggered by CSS state changes.
@property --progress {
syntax: '<number>';
inherits: true;
initial-value: 0;
}
.progress { --progress: 0; width: 100%; background: #efefef; border-radius: 8px; height: 12px; overflow:hidden; }
.progress .bar { width: calc(var(--progress) * 1%); height:100%; background:linear-gradient(90deg,#06f,#06f9b); transition: width .5s ease; }
/* toggle state using a checkbox to simulate progress change */
#step2:checked ~ .progress { --progress: 66; }
#step3:checked ~ .progress { --progress: 100; }
Accessibility and progressive enhancement
- Prefer native elements (details, input, radio) because they include keyboard behavior and semantics by default.
- Always support prefers-reduced-motion: remove nonessential animations and use instant transitions when reduced motion is requested.
- If ARIA states depend on user action that CSS cannot update, document that adding a small enhancement script later can sync aria-expanded/aria-selected—until then rely on native semantics where possible.
- Test with keyboard only and a screen reader; patterns that use inputs and labels usually perform best without JS.
Performance and maintainability tips
- Keep animation work on composited properties (transform, opacity) to avoid layout trashing. Use transitions and keyframes that target transform/opacity.
- Use CSS variables for theme and motion tokens so a single change updates many microinteractions consistently.
- Scope styles to components to reduce cascade surprises; avoid heavy selectors that force style recalculation.
When to add JavaScript
Zero-JS microinteractions should be the default for small UI pieces. Add JavaScript only when you need cross-component coordination, complex state, persistent storage, or to sync ARIA attributes for assistive tech—then progressively enhance the CSS-first patterns rather than replacing them.
Final thoughts
Zero-JS microinteractions combine the strengths of semantic HTML, modern CSS (including @property and declarative animations), and built-in browser behaviors to create fast, accessible, and maintainable UI moments. Start small—convert one toggle, one accordion, or one tooltip—and you’ll find less code, fewer bugs, and happier users.
Ready to strip a few kilobytes of JS from your UI? Try converting a single microinteraction on your next release and measure the impact.
