15 / 20
All 20 in 20 Tailwind CSS Kbd Keys
- 01Tailwind CSS Kbd Tag Styling for Inline Keyboard Keys
- 02Tailwind 3D Keycap Button with Press-Down Effect
- 03Tailwind Keyboard Shortcut Combo with Plus Separators
- 04Flat Minimalist Tailwind Kbd Key for Body Text
- 05Responsive Tailwind Kbd Sizing with Em Units
- 06Tailwind Mac Cmd vs Windows Ctrl Shortcut Toggle
- 07Tailwind Arrow Keys and Navigation Key Layout
- 08Auto-Detect OS Keyboard Shortcuts (Cmd or Ctrl) in Tailwind
- 09Tailwind Keyboard Layout with Function Keys and Space Bar
- 10Tailwind Icon Keycaps with Inline SVG
- 11Tailwind Search Input with Cmd+K Shortcut Hint
- 12Tailwind Keyboard Shortcuts Cheat Sheet Grid
- 13Tailwind Tooltip with Keyboard Shortcut Hint
- 14Tailwind Keyboard Shortcut Settings Table
- 15Live Key Press Visualizer with Tailwind CSS
- 16Neumorphic Keycap Design in Tailwind CSS
- 17Glassmorphism Keyboard Keys in Tailwind CSS
- 18Retro Typewriter Round Keys in Tailwind CSS
- 19Terminal-Style Monospace Kbd Keys in Tailwind
- 20Dark Mode Kbd Keys with Tailwind CSS Variables
Live Key Press Visualizer with Tailwind CSS
A keystroke visualizer for screencasts and docs: focus the pad, press anything, and each press appears as a cap group — modifiers included, so Ctrl + Shift + K shows as one chord. Caps animate in with zero JS via starting: (@starting-style), leave via a data-out: variant, and a readout shows event.key beside event.code so the difference is visible.
Untitled UI Pro: 5,000+ React + Tailwind CSS v4 components · from $349 one-time
Untitled UI React · Affiliate link
The code
<section id="kbd-15-root" class="kbd-15 relative w-full min-h-svh scheme-light kbd15-dark:scheme-dark bg-kbd15-bg bg-[radial-gradient(70%_55%_at_50%_45%,var(--color-kbd15-glow),transparent_70%)] text-kbd15-ink font-kbd15-sans" aria-label="Live key press feedback demo">
<div class="grid min-h-svh place-items-center px-5 pt-20 pb-12">
<div class="grid w-full min-w-0 max-w-[40rem] justify-items-center gap-6 text-center">
<button type="button" data-pad class="group grid min-h-56 w-full place-content-center gap-5 rounded-3xl border border-dashed border-kbd15-rim bg-kbd15-pad p-6 transition-[border-color,box-shadow] focus:border-solid focus:border-kbd15-acc focus:shadow-[0_0_0_4px_var(--color-kbd15-ring)] focus:outline-none motion-reduce:transition-none">
<span data-idle class="text-[15px] text-kbd15-mut"><span class="group-focus:hidden">Click here, then press any key</span><span class="hidden group-focus:inline">Listening — press any key or chord</span></span>
<span data-list aria-live="polite" class="flex min-h-14 flex-wrap items-center justify-center gap-3"></span>
</button>
<p data-readout class="min-h-5 font-kbd15-mono text-[12.5px] text-kbd15-mut">key · code readout appears here</p>
<p class="text-[12.5px] text-kbd15-mut">One group per physical press · repeats suppressed · Tab still moves focus</p>
<kbd data-chip hidden class="inline-flex items-center gap-1.5 rounded-2xl bg-kbd15-tray p-1.5 ring-1 ring-kbd15-rule transition-[opacity,translate,scale] duration-300 ease-out starting:translate-y-3 starting:scale-90 starting:opacity-0 data-out:-translate-y-2 data-out:opacity-0 motion-reduce:transition-none"></kbd>
<kbd data-cap hidden class="inline-flex h-14 min-w-14 items-center justify-center rounded-xl border border-kbd15-caprim bg-linear-to-b from-kbd15-captop to-kbd15-cap px-4 font-kbd15-mono text-lg font-semibold text-kbd15-capink shadow-[0_4px_0_var(--color-kbd15-edge)]"></kbd>
</div>
</div>
<div class="absolute top-3.5 right-4 z-10">
<input id="kbd-15-theme" type="checkbox" class="peer sr-only">
<label for="kbd-15-theme" class="inline-flex min-h-11 cursor-pointer select-none items-center gap-2.5 rounded-full border border-current/55 px-4 font-sans text-[11px] font-bold uppercase leading-none tracking-[0.16em] text-kbd15-ink transition-colors hover:border-current peer-focus-visible:outline-3 peer-focus-visible:outline-offset-2 peer-focus-visible:outline-kbd15-acc">
<span aria-hidden="true" class="size-3 flex-none rounded-full bg-current transition-[background-color,box-shadow] duration-300 kbd15-dark:bg-transparent kbd15-dark:shadow-[inset_-4px_-2px_0_0_currentColor] motion-reduce:transition-none"></span>
<span class="kbd15-dark:hidden">Light</span><span class="hidden kbd15-dark:inline">Dark</span><span class="sr-only"> theme</span>
</label>
</div>
</section><section id="kbd-15-root" class="kbd-15 relative w-full min-h-svh scheme-light kbd15-dark:scheme-dark bg-kbd15-bg bg-[radial-gradient(70%_55%_at_50%_45%,var(--color-kbd15-glow),transparent_70%)] text-kbd15-ink font-kbd15-sans" aria-label="Live key press feedback demo">
<div class="grid min-h-svh place-items-center px-5 pt-20 pb-12">
<div class="grid w-full min-w-0 max-w-[40rem] justify-items-center gap-6 text-center">
<button type="button" data-pad class="group grid min-h-56 w-full place-content-center gap-5 rounded-3xl border border-dashed border-kbd15-rim bg-kbd15-pad p-6 transition-[border-color,box-shadow] focus:border-solid focus:border-kbd15-acc focus:shadow-[0_0_0_4px_var(--color-kbd15-ring)] focus:outline-none motion-reduce:transition-none">
<span data-idle class="text-[15px] text-kbd15-mut"><span class="group-focus:hidden">Click here, then press any key</span><span class="hidden group-focus:inline">Listening — press any key or chord</span></span>
<span data-list aria-live="polite" class="flex min-h-14 flex-wrap items-center justify-center gap-3"></span>
</button>
<p data-readout class="min-h-5 font-kbd15-mono text-[12.5px] text-kbd15-mut">key · code readout appears here</p>
<p class="text-[12.5px] text-kbd15-mut">One group per physical press · repeats suppressed · Tab still moves focus</p>
<kbd data-chip hidden class="inline-flex items-center gap-1.5 rounded-2xl bg-kbd15-tray p-1.5 ring-1 ring-kbd15-rule transition-[opacity,translate,scale] duration-300 ease-out starting:translate-y-3 starting:scale-90 starting:opacity-0 data-out:-translate-y-2 data-out:opacity-0 motion-reduce:transition-none"></kbd>
<kbd data-cap hidden class="inline-flex h-14 min-w-14 items-center justify-center rounded-xl border border-kbd15-caprim bg-linear-to-b from-kbd15-captop to-kbd15-cap px-4 font-kbd15-mono text-lg font-semibold text-kbd15-capink shadow-[0_4px_0_var(--color-kbd15-edge)]"></kbd>
</div>
</div>
<div class="absolute top-3.5 right-4 z-10">
<input id="kbd-15-theme" type="checkbox" class="peer sr-only">
<label for="kbd-15-theme" class="inline-flex min-h-11 cursor-pointer select-none items-center gap-2.5 rounded-full border border-current/55 px-4 font-sans text-[11px] font-bold uppercase leading-none tracking-[0.16em] text-kbd15-ink transition-colors hover:border-current peer-focus-visible:outline-3 peer-focus-visible:outline-offset-2 peer-focus-visible:outline-kbd15-acc">
<span aria-hidden="true" class="size-3 flex-none rounded-full bg-current transition-[background-color,box-shadow] duration-300 kbd15-dark:bg-transparent kbd15-dark:shadow-[inset_-4px_-2px_0_0_currentColor] motion-reduce:transition-none"></span>
<span class="kbd15-dark:hidden">Light</span><span class="hidden kbd15-dark:inline">Dark</span><span class="sr-only"> theme</span>
</label>
</div>
</section>@import "tailwindcss";
/* Theme state: follows the OS; the corner switch flips it for THIS demo only. */
@custom-variant kbd15-dark {
@media (prefers-color-scheme: dark) {
&:where(.kbd-15:not(:has(#kbd-15-theme:checked)), .kbd-15:not(:has(#kbd-15-theme:checked)) *) {
@slot;
}
}
@media (prefers-color-scheme: light) {
&:where(.kbd-15:has(#kbd-15-theme:checked), .kbd-15:has(#kbd-15-theme:checked) *) {
@slot;
}
}
}
/* Every colour is light-dark(day, night): the root's scheme-light / scheme-dark utility picks the side. */
@theme {
--color-kbd15-bg: light-dark(#eef0f4, #08090c);
--color-kbd15-bg: light-dark(oklch(0.955 0.006 264.5), oklch(0.14 0.007 269.7));
--color-kbd15-glow: light-dark(#dfe5ff, #141a33);
--color-kbd15-glow: light-dark(oklch(0.925 0.036 275.4), oklch(0.226 0.049 271.8));
--color-kbd15-pad: light-dark(#ffffff, #0f1116);
--color-kbd15-pad: light-dark(oklch(1 0 0), oklch(0.178 0.011 268.1));
--color-kbd15-ring: light-dark(#4f6bed33, #7c93ff40);
--color-kbd15-ring: light-dark(oklch(0.579 0.198 270 / 0.2), oklch(0.694 0.16 272.5 / 0.25));
--color-kbd15-ink: light-dark(#151821, #e9ebf2);
--color-kbd15-ink: light-dark(oklch(0.21 0.018 270.4), oklch(0.941 0.01 273.4));
--color-kbd15-mut: light-dark(#585f6e, #979eae);
--color-kbd15-mut: light-dark(oklch(0.485 0.026 265.6), oklch(0.699 0.025 266.8));
--color-kbd15-rim: light-dark(#c4cad6, #2f3442);
--color-kbd15-rim: light-dark(oklch(0.838 0.018 264.5), oklch(0.326 0.026 269.6));
--color-kbd15-rule: light-dark(#dde1ea, #23283a);
--color-kbd15-rule: light-dark(oklch(0.909 0.013 266.7), oklch(0.28 0.034 272.1));
--color-kbd15-tray: light-dark(#f1f3f8, #161a26);
--color-kbd15-tray: light-dark(oklch(0.964 0.007 268.5), oklch(0.22 0.024 270.1));
--color-kbd15-cap: light-dark(#f6f7fb, #222838);
--color-kbd15-cap: light-dark(oklch(0.977 0.005 275), oklch(0.278 0.031 268.5));
--color-kbd15-captop: light-dark(#ffffff, #2d3449);
--color-kbd15-captop: light-dark(oklch(1 0 0), oklch(0.328 0.039 270));
--color-kbd15-caprim: light-dark(#c9cfdb, #3d4560);
--color-kbd15-caprim: light-dark(oklch(0.853 0.018 264.5), oklch(0.395 0.047 271.4));
--color-kbd15-edge: light-dark(#a9b0c0, #040509);
--color-kbd15-edge: light-dark(oklch(0.757 0.024 266.9), oklch(0.116 0.012 270.7));
--color-kbd15-capink: light-dark(#1c2030, #eef0f8);
--color-kbd15-capink: light-dark(oklch(0.247 0.031 273.1), oklch(0.956 0.011 274.9));
--color-kbd15-acc: light-dark(#4f6bed, #7c93ff);
--color-kbd15-acc: light-dark(oklch(0.579 0.198 270), oklch(0.694 0.16 272.5));
--font-kbd15-sans: 'Space Grotesk', system-ui, sans-serif;
--font-kbd15-mono: 'JetBrains Mono', ui-monospace, monospace;
}
/* Root rule: the full-bleed contract, stated in CSS as well as utilities so the stage is sized before the browser build compiles. */
.kbd-15 {
width: 100%;
min-height: 100svh;
position: relative;
}@import "tailwindcss";
/* Theme state: follows the OS; the corner switch flips it for THIS demo only. */
@custom-variant kbd15-dark {
@media (prefers-color-scheme: dark) {
&:where(.kbd-15:not(:has(#kbd-15-theme:checked)), .kbd-15:not(:has(#kbd-15-theme:checked)) *) {
@slot;
}
}
@media (prefers-color-scheme: light) {
&:where(.kbd-15:has(#kbd-15-theme:checked), .kbd-15:has(#kbd-15-theme:checked) *) {
@slot;
}
}
}
/* Every colour is light-dark(day, night): the root's scheme-light / scheme-dark utility picks the side. */
@theme {
--color-kbd15-bg: light-dark(#eef0f4, #08090c);
--color-kbd15-bg: light-dark(oklch(0.955 0.006 264.5), oklch(0.14 0.007 269.7));
--color-kbd15-glow: light-dark(#dfe5ff, #141a33);
--color-kbd15-glow: light-dark(oklch(0.925 0.036 275.4), oklch(0.226 0.049 271.8));
--color-kbd15-pad: light-dark(#ffffff, #0f1116);
--color-kbd15-pad: light-dark(oklch(1 0 0), oklch(0.178 0.011 268.1));
--color-kbd15-ring: light-dark(#4f6bed33, #7c93ff40);
--color-kbd15-ring: light-dark(oklch(0.579 0.198 270 / 0.2), oklch(0.694 0.16 272.5 / 0.25));
--color-kbd15-ink: light-dark(#151821, #e9ebf2);
--color-kbd15-ink: light-dark(oklch(0.21 0.018 270.4), oklch(0.941 0.01 273.4));
--color-kbd15-mut: light-dark(#585f6e, #979eae);
--color-kbd15-mut: light-dark(oklch(0.485 0.026 265.6), oklch(0.699 0.025 266.8));
--color-kbd15-rim: light-dark(#c4cad6, #2f3442);
--color-kbd15-rim: light-dark(oklch(0.838 0.018 264.5), oklch(0.326 0.026 269.6));
--color-kbd15-rule: light-dark(#dde1ea, #23283a);
--color-kbd15-rule: light-dark(oklch(0.909 0.013 266.7), oklch(0.28 0.034 272.1));
--color-kbd15-tray: light-dark(#f1f3f8, #161a26);
--color-kbd15-tray: light-dark(oklch(0.964 0.007 268.5), oklch(0.22 0.024 270.1));
--color-kbd15-cap: light-dark(#f6f7fb, #222838);
--color-kbd15-cap: light-dark(oklch(0.977 0.005 275), oklch(0.278 0.031 268.5));
--color-kbd15-captop: light-dark(#ffffff, #2d3449);
--color-kbd15-captop: light-dark(oklch(1 0 0), oklch(0.328 0.039 270));
--color-kbd15-caprim: light-dark(#c9cfdb, #3d4560);
--color-kbd15-caprim: light-dark(oklch(0.853 0.018 264.5), oklch(0.395 0.047 271.4));
--color-kbd15-edge: light-dark(#a9b0c0, #040509);
--color-kbd15-edge: light-dark(oklch(0.757 0.024 266.9), oklch(0.116 0.012 270.7));
--color-kbd15-capink: light-dark(#1c2030, #eef0f8);
--color-kbd15-capink: light-dark(oklch(0.247 0.031 273.1), oklch(0.956 0.011 274.9));
--color-kbd15-acc: light-dark(#4f6bed, #7c93ff);
--color-kbd15-acc: light-dark(oklch(0.579 0.198 270), oklch(0.694 0.16 272.5));
--font-kbd15-sans: 'Space Grotesk', system-ui, sans-serif;
--font-kbd15-mono: 'JetBrains Mono', ui-monospace, monospace;
}
/* Root rule: the full-bleed contract, stated in CSS as well as utilities so the stage is sized before the browser build compiles. */
.kbd-15 {
width: 100%;
min-height: 100svh;
position: relative;
}(() => {
const root = document.getElementById('kbd-15-root');
if (!root || root.dataset.wired) return;
root.dataset.wired = '1';
const pad = root.querySelector('[data-pad]'), list = root.querySelector('[data-list]');
const chip = root.querySelector('[data-chip]'), cap = root.querySelector('[data-cap]'), readout = root.querySelector('[data-readout]');
const isMac = /mac/i.test(navigator.userAgentData?.platform || navigator.platform);
const names = { ' ': 'Space', Escape: 'Esc', Control: 'Ctrl', Meta: isMac ? '\u2318' : 'Win', ArrowUp: '\u2191', ArrowDown: '\u2193', ArrowLeft: '\u2190', ArrowRight: '\u2192' };
const label = (k) => names[k] ?? (k.length === 1 ? k.toUpperCase() : k);
const clone = (el) => { const c = el.cloneNode(); c.removeAttribute('hidden'); return c; };
pad.addEventListener('keydown', (e) => {
if (e.repeat || e.key === 'Tab') return;
e.preventDefault();
const mods = [e.ctrlKey && 'Control', e.altKey && 'Alt', e.shiftKey && 'Shift', e.metaKey && 'Meta'].filter((m) => m && m !== e.key);
const g = clone(chip);
[...mods, e.key].forEach((k) => { const c = clone(cap); c.textContent = label(k); g.append(c); });
list.append(g);
readout.textContent = 'key: "' + e.key + '" \u00b7 code: "' + e.code + '"';
while (list.childElementCount > 4) list.firstElementChild.remove();
setTimeout(() => { g.setAttribute('data-out', ''); setTimeout(() => g.remove(), 300); }, 1600);
});
})();(() => {
const root = document.getElementById('kbd-15-root');
if (!root || root.dataset.wired) return;
root.dataset.wired = '1';
const pad = root.querySelector('[data-pad]'), list = root.querySelector('[data-list]');
const chip = root.querySelector('[data-chip]'), cap = root.querySelector('[data-cap]'), readout = root.querySelector('[data-readout]');
const isMac = /mac/i.test(navigator.userAgentData?.platform || navigator.platform);
const names = { ' ': 'Space', Escape: 'Esc', Control: 'Ctrl', Meta: isMac ? '\u2318' : 'Win', ArrowUp: '\u2191', ArrowDown: '\u2193', ArrowLeft: '\u2190', ArrowRight: '\u2192' };
const label = (k) => names[k] ?? (k.length === 1 ? k.toUpperCase() : k);
const clone = (el) => { const c = el.cloneNode(); c.removeAttribute('hidden'); return c; };
pad.addEventListener('keydown', (e) => {
if (e.repeat || e.key === 'Tab') return;
e.preventDefault();
const mods = [e.ctrlKey && 'Control', e.altKey && 'Alt', e.shiftKey && 'Shift', e.metaKey && 'Meta'].filter((m) => m && m !== e.key);
const g = clone(chip);
[...mods, e.key].forEach((k) => { const c = clone(cap); c.textContent = label(k); g.append(c); });
list.append(g);
readout.textContent = 'key: "' + e.key + '" \u00b7 code: "' + e.code + '"';
while (list.childElementCount > 4) list.firstElementChild.remove();
setTimeout(() => { g.setAttribute('data-out', ''); setTimeout(() => g.remove(), 300); }, 1600);
});
})();Setup — do this once per project
Skip this if your project already runs Tailwind v4.
<script src="https://cdn.jsdelivr.net/npm/@tailwindcss/browser@4"></script>
<script src="https://cdn.jsdelivr.net/npm/@tailwindcss/browser@4"></script>Compiles in the browser — perfect for a CodePen or a quick test, not for production.
npm install tailwindcss @tailwindcss/vite
npm install tailwindcss @tailwindcss/vite// vite.config.js ("type": "module" in package.json)
import { defineConfig } from 'vite'
import tailwindcss from '@tailwindcss/vite'
export default defineConfig({
plugins: [tailwindcss()],
})// vite.config.js ("type": "module" in package.json)
import { defineConfig } from 'vite'
import tailwindcss from '@tailwindcss/vite'
export default defineConfig({
plugins: [tailwindcss()],
})Any v4 setup works — Vite, PostCSS or the CLI. Nothing in this demo is bundler-specific.
@import "tailwindcss";
/* Theme state: follows the OS; the corner switch flips it for THIS demo only. */
@custom-variant kbd15-dark {
@media (prefers-color-scheme: dark) {
&:where(.kbd-15:not(:has(#kbd-15-theme:checked)), .kbd-15:not(:has(#kbd-15-theme:checked)) *) {
@slot;
}
}
@media (prefers-color-scheme: light) {
&:where(.kbd-15:has(#kbd-15-theme:checked), .kbd-15:has(#kbd-15-theme:checked) *) {
@slot;
}
}
}
/* Every colour is light-dark(day, night): the root's scheme-light / scheme-dark utility picks the side. */
@theme {
--color-kbd15-bg: light-dark(#eef0f4, #08090c);
--color-kbd15-bg: light-dark(oklch(0.955 0.006 264.5), oklch(0.14 0.007 269.7));
--color-kbd15-glow: light-dark(#dfe5ff, #141a33);
--color-kbd15-glow: light-dark(oklch(0.925 0.036 275.4), oklch(0.226 0.049 271.8));
--color-kbd15-pad: light-dark(#ffffff, #0f1116);
--color-kbd15-pad: light-dark(oklch(1 0 0), oklch(0.178 0.011 268.1));
--color-kbd15-ring: light-dark(#4f6bed33, #7c93ff40);
--color-kbd15-ring: light-dark(oklch(0.579 0.198 270 / 0.2), oklch(0.694 0.16 272.5 / 0.25));
--color-kbd15-ink: light-dark(#151821, #e9ebf2);
--color-kbd15-ink: light-dark(oklch(0.21 0.018 270.4), oklch(0.941 0.01 273.4));
--color-kbd15-mut: light-dark(#585f6e, #979eae);
--color-kbd15-mut: light-dark(oklch(0.485 0.026 265.6), oklch(0.699 0.025 266.8));
--color-kbd15-rim: light-dark(#c4cad6, #2f3442);
--color-kbd15-rim: light-dark(oklch(0.838 0.018 264.5), oklch(0.326 0.026 269.6));
--color-kbd15-rule: light-dark(#dde1ea, #23283a);
--color-kbd15-rule: light-dark(oklch(0.909 0.013 266.7), oklch(0.28 0.034 272.1));
--color-kbd15-tray: light-dark(#f1f3f8, #161a26);
--color-kbd15-tray: light-dark(oklch(0.964 0.007 268.5), oklch(0.22 0.024 270.1));
--color-kbd15-cap: light-dark(#f6f7fb, #222838);
--color-kbd15-cap: light-dark(oklch(0.977 0.005 275), oklch(0.278 0.031 268.5));
--color-kbd15-captop: light-dark(#ffffff, #2d3449);
--color-kbd15-captop: light-dark(oklch(1 0 0), oklch(0.328 0.039 270));
--color-kbd15-caprim: light-dark(#c9cfdb, #3d4560);
--color-kbd15-caprim: light-dark(oklch(0.853 0.018 264.5), oklch(0.395 0.047 271.4));
--color-kbd15-edge: light-dark(#a9b0c0, #040509);
--color-kbd15-edge: light-dark(oklch(0.757 0.024 266.9), oklch(0.116 0.012 270.7));
--color-kbd15-capink: light-dark(#1c2030, #eef0f8);
--color-kbd15-capink: light-dark(oklch(0.247 0.031 273.1), oklch(0.956 0.011 274.9));
--color-kbd15-acc: light-dark(#4f6bed, #7c93ff);
--color-kbd15-acc: light-dark(oklch(0.579 0.198 270), oklch(0.694 0.16 272.5));
--font-kbd15-sans: 'Space Grotesk', system-ui, sans-serif;
--font-kbd15-mono: 'JetBrains Mono', ui-monospace, monospace;
}
/* Root rule: the full-bleed contract, stated in CSS as well as utilities so the stage is sized before the browser build compiles. */
.kbd-15 {
width: 100%;
min-height: 100svh;
position: relative;
}@import "tailwindcss";
/* Theme state: follows the OS; the corner switch flips it for THIS demo only. */
@custom-variant kbd15-dark {
@media (prefers-color-scheme: dark) {
&:where(.kbd-15:not(:has(#kbd-15-theme:checked)), .kbd-15:not(:has(#kbd-15-theme:checked)) *) {
@slot;
}
}
@media (prefers-color-scheme: light) {
&:where(.kbd-15:has(#kbd-15-theme:checked), .kbd-15:has(#kbd-15-theme:checked) *) {
@slot;
}
}
}
/* Every colour is light-dark(day, night): the root's scheme-light / scheme-dark utility picks the side. */
@theme {
--color-kbd15-bg: light-dark(#eef0f4, #08090c);
--color-kbd15-bg: light-dark(oklch(0.955 0.006 264.5), oklch(0.14 0.007 269.7));
--color-kbd15-glow: light-dark(#dfe5ff, #141a33);
--color-kbd15-glow: light-dark(oklch(0.925 0.036 275.4), oklch(0.226 0.049 271.8));
--color-kbd15-pad: light-dark(#ffffff, #0f1116);
--color-kbd15-pad: light-dark(oklch(1 0 0), oklch(0.178 0.011 268.1));
--color-kbd15-ring: light-dark(#4f6bed33, #7c93ff40);
--color-kbd15-ring: light-dark(oklch(0.579 0.198 270 / 0.2), oklch(0.694 0.16 272.5 / 0.25));
--color-kbd15-ink: light-dark(#151821, #e9ebf2);
--color-kbd15-ink: light-dark(oklch(0.21 0.018 270.4), oklch(0.941 0.01 273.4));
--color-kbd15-mut: light-dark(#585f6e, #979eae);
--color-kbd15-mut: light-dark(oklch(0.485 0.026 265.6), oklch(0.699 0.025 266.8));
--color-kbd15-rim: light-dark(#c4cad6, #2f3442);
--color-kbd15-rim: light-dark(oklch(0.838 0.018 264.5), oklch(0.326 0.026 269.6));
--color-kbd15-rule: light-dark(#dde1ea, #23283a);
--color-kbd15-rule: light-dark(oklch(0.909 0.013 266.7), oklch(0.28 0.034 272.1));
--color-kbd15-tray: light-dark(#f1f3f8, #161a26);
--color-kbd15-tray: light-dark(oklch(0.964 0.007 268.5), oklch(0.22 0.024 270.1));
--color-kbd15-cap: light-dark(#f6f7fb, #222838);
--color-kbd15-cap: light-dark(oklch(0.977 0.005 275), oklch(0.278 0.031 268.5));
--color-kbd15-captop: light-dark(#ffffff, #2d3449);
--color-kbd15-captop: light-dark(oklch(1 0 0), oklch(0.328 0.039 270));
--color-kbd15-caprim: light-dark(#c9cfdb, #3d4560);
--color-kbd15-caprim: light-dark(oklch(0.853 0.018 264.5), oklch(0.395 0.047 271.4));
--color-kbd15-edge: light-dark(#a9b0c0, #040509);
--color-kbd15-edge: light-dark(oklch(0.757 0.024 266.9), oklch(0.116 0.012 270.7));
--color-kbd15-capink: light-dark(#1c2030, #eef0f8);
--color-kbd15-capink: light-dark(oklch(0.247 0.031 273.1), oklch(0.956 0.011 274.9));
--color-kbd15-acc: light-dark(#4f6bed, #7c93ff);
--color-kbd15-acc: light-dark(oklch(0.579 0.198 270), oklch(0.694 0.16 272.5));
--font-kbd15-sans: 'Space Grotesk', system-ui, sans-serif;
--font-kbd15-mono: 'JetBrains Mono', ui-monospace, monospace;
}
/* Root rule: the full-bleed contract, stated in CSS as well as utilities so the stage is sized before the browser build compiles. */
.kbd-15 {
width: 100%;
min-height: 100svh;
position: relative;
}<link rel="preconnect" href="https://fonts.googleapis.com"> <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> <link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@400..700&family=JetBrains+Mono:wght@500;600;700&display=swap">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@400..700&family=JetBrains+Mono:wght@500;600;700&display=swap">What changed in the port
Which parts became utilities, which stayed CSS, and why.
- Runtime class strings (
'kbd-15__key kbd-15__key--wide') became cloned prototypes, so the Tailwind browser build has seen every utility before the script uses it — and the script contains no class names at all. - The exit animation moved from a JS-timed class to a
data-out:variant; the entry is purestarting:. - UI upgrade: modifier chords now render as one group (
Ctrl + Shift + K) instead of three unrelated caps, and a liveevent.key/event.codereadout teaches the difference. - UI upgrade: the listener is scoped to the pad instead of
document, so the demo no longer swallows keys typed elsewhere on the page, and Tab still escapes.
Here's a working CSS demo from CodeFronts. Use it as-is or adapt to your framework. All classes are scoped under a unique prefix so the code won't collide with your existing styles. MIT licensed.
Demo: Live Key Press Visualizer with Tailwind CSS
Source: https://codefronts.com/snippets/tailwind-css-kbd-keys/live-key-press-visualizer-tailwind-css/
A keystroke visualizer for screencasts and docs: focus the pad, press anything, and each press appears as a cap group — modifiers included, so Ctrl + Shift + K shows as one chord. Caps animate in with zero JS via starting: (@starting-style), leave via a data-out: variant, and a readout shows event.key beside event.code so the difference is visible.
## HTML
```html
<section id="kbd-15-root" class="kbd-15 relative w-full min-h-svh scheme-light kbd15-dark:scheme-dark bg-kbd15-bg bg-[radial-gradient(70%_55%_at_50%_45%,var(--color-kbd15-glow),transparent_70%)] text-kbd15-ink font-kbd15-sans" aria-label="Live key press feedback demo">
<div class="grid min-h-svh place-items-center px-5 pt-20 pb-12">
<div class="grid w-full min-w-0 max-w-[40rem] justify-items-center gap-6 text-center">
<button type="button" data-pad class="group grid min-h-56 w-full place-content-center gap-5 rounded-3xl border border-dashed border-kbd15-rim bg-kbd15-pad p-6 transition-[border-color,box-shadow] focus:border-solid focus:border-kbd15-acc focus:shadow-[0_0_0_4px_var(--color-kbd15-ring)] focus:outline-none motion-reduce:transition-none">
<span data-idle class="text-[15px] text-kbd15-mut"><span class="group-focus:hidden">Click here, then press any key</span><span class="hidden group-focus:inline">Listening — press any key or chord</span></span>
<span data-list aria-live="polite" class="flex min-h-14 flex-wrap items-center justify-center gap-3"></span>
</button>
<p data-readout class="min-h-5 font-kbd15-mono text-[12.5px] text-kbd15-mut">key · code readout appears here</p>
<p class="text-[12.5px] text-kbd15-mut">One group per physical press · repeats suppressed · Tab still moves focus</p>
<kbd data-chip hidden class="inline-flex items-center gap-1.5 rounded-2xl bg-kbd15-tray p-1.5 ring-1 ring-kbd15-rule transition-[opacity,translate,scale] duration-300 ease-out starting:translate-y-3 starting:scale-90 starting:opacity-0 data-out:-translate-y-2 data-out:opacity-0 motion-reduce:transition-none"></kbd>
<kbd data-cap hidden class="inline-flex h-14 min-w-14 items-center justify-center rounded-xl border border-kbd15-caprim bg-linear-to-b from-kbd15-captop to-kbd15-cap px-4 font-kbd15-mono text-lg font-semibold text-kbd15-capink shadow-[0_4px_0_var(--color-kbd15-edge)]"></kbd>
</div>
</div>
<div class="absolute top-3.5 right-4 z-10">
<input id="kbd-15-theme" type="checkbox" class="peer sr-only">
<label for="kbd-15-theme" class="inline-flex min-h-11 cursor-pointer select-none items-center gap-2.5 rounded-full border border-current/55 px-4 font-sans text-[11px] font-bold uppercase leading-none tracking-[0.16em] text-kbd15-ink transition-colors hover:border-current peer-focus-visible:outline-3 peer-focus-visible:outline-offset-2 peer-focus-visible:outline-kbd15-acc">
<span aria-hidden="true" class="size-3 flex-none rounded-full bg-current transition-[background-color,box-shadow] duration-300 kbd15-dark:bg-transparent kbd15-dark:shadow-[inset_-4px_-2px_0_0_currentColor] motion-reduce:transition-none"></span>
<span class="kbd15-dark:hidden">Light</span><span class="hidden kbd15-dark:inline">Dark</span><span class="sr-only"> theme</span>
</label>
</div>
</section>
```
## CSS
```css
@import "tailwindcss";
/* Theme state: follows the OS; the corner switch flips it for THIS demo only. */
@custom-variant kbd15-dark {
@media (prefers-color-scheme: dark) {
&:where(.kbd-15:not(:has(#kbd-15-theme:checked)), .kbd-15:not(:has(#kbd-15-theme:checked)) *) {
@slot;
}
}
@media (prefers-color-scheme: light) {
&:where(.kbd-15:has(#kbd-15-theme:checked), .kbd-15:has(#kbd-15-theme:checked) *) {
@slot;
}
}
}
/* Every colour is light-dark(day, night): the root's scheme-light / scheme-dark utility picks the side. */
@theme {
--color-kbd15-bg: light-dark(#eef0f4, #08090c);
--color-kbd15-bg: light-dark(oklch(0.955 0.006 264.5), oklch(0.14 0.007 269.7));
--color-kbd15-glow: light-dark(#dfe5ff, #141a33);
--color-kbd15-glow: light-dark(oklch(0.925 0.036 275.4), oklch(0.226 0.049 271.8));
--color-kbd15-pad: light-dark(#ffffff, #0f1116);
--color-kbd15-pad: light-dark(oklch(1 0 0), oklch(0.178 0.011 268.1));
--color-kbd15-ring: light-dark(#4f6bed33, #7c93ff40);
--color-kbd15-ring: light-dark(oklch(0.579 0.198 270 / 0.2), oklch(0.694 0.16 272.5 / 0.25));
--color-kbd15-ink: light-dark(#151821, #e9ebf2);
--color-kbd15-ink: light-dark(oklch(0.21 0.018 270.4), oklch(0.941 0.01 273.4));
--color-kbd15-mut: light-dark(#585f6e, #979eae);
--color-kbd15-mut: light-dark(oklch(0.485 0.026 265.6), oklch(0.699 0.025 266.8));
--color-kbd15-rim: light-dark(#c4cad6, #2f3442);
--color-kbd15-rim: light-dark(oklch(0.838 0.018 264.5), oklch(0.326 0.026 269.6));
--color-kbd15-rule: light-dark(#dde1ea, #23283a);
--color-kbd15-rule: light-dark(oklch(0.909 0.013 266.7), oklch(0.28 0.034 272.1));
--color-kbd15-tray: light-dark(#f1f3f8, #161a26);
--color-kbd15-tray: light-dark(oklch(0.964 0.007 268.5), oklch(0.22 0.024 270.1));
--color-kbd15-cap: light-dark(#f6f7fb, #222838);
--color-kbd15-cap: light-dark(oklch(0.977 0.005 275), oklch(0.278 0.031 268.5));
--color-kbd15-captop: light-dark(#ffffff, #2d3449);
--color-kbd15-captop: light-dark(oklch(1 0 0), oklch(0.328 0.039 270));
--color-kbd15-caprim: light-dark(#c9cfdb, #3d4560);
--color-kbd15-caprim: light-dark(oklch(0.853 0.018 264.5), oklch(0.395 0.047 271.4));
--color-kbd15-edge: light-dark(#a9b0c0, #040509);
--color-kbd15-edge: light-dark(oklch(0.757 0.024 266.9), oklch(0.116 0.012 270.7));
--color-kbd15-capink: light-dark(#1c2030, #eef0f8);
--color-kbd15-capink: light-dark(oklch(0.247 0.031 273.1), oklch(0.956 0.011 274.9));
--color-kbd15-acc: light-dark(#4f6bed, #7c93ff);
--color-kbd15-acc: light-dark(oklch(0.579 0.198 270), oklch(0.694 0.16 272.5));
--font-kbd15-sans: 'Space Grotesk', system-ui, sans-serif;
--font-kbd15-mono: 'JetBrains Mono', ui-monospace, monospace;
}
/* Root rule: the full-bleed contract, stated in CSS as well as utilities so the stage is sized before the browser build compiles. */
.kbd-15 {
width: 100%;
min-height: 100svh;
position: relative;
}
```
## JavaScript
```js
(() => {
const root = document.getElementById('kbd-15-root');
if (!root || root.dataset.wired) return;
root.dataset.wired = '1';
const pad = root.querySelector('[data-pad]'), list = root.querySelector('[data-list]');
const chip = root.querySelector('[data-chip]'), cap = root.querySelector('[data-cap]'), readout = root.querySelector('[data-readout]');
const isMac = /mac/i.test(navigator.userAgentData?.platform || navigator.platform);
const names = { ' ': 'Space', Escape: 'Esc', Control: 'Ctrl', Meta: isMac ? '\u2318' : 'Win', ArrowUp: '\u2191', ArrowDown: '\u2193', ArrowLeft: '\u2190', ArrowRight: '\u2192' };
const label = (k) => names[k] ?? (k.length === 1 ? k.toUpperCase() : k);
const clone = (el) => { const c = el.cloneNode(); c.removeAttribute('hidden'); return c; };
pad.addEventListener('keydown', (e) => {
if (e.repeat || e.key === 'Tab') return;
e.preventDefault();
const mods = [e.ctrlKey && 'Control', e.altKey && 'Alt', e.shiftKey && 'Shift', e.metaKey && 'Meta'].filter((m) => m && m !== e.key);
const g = clone(chip);
[...mods, e.key].forEach((k) => { const c = clone(cap); c.textContent = label(k); g.append(c); });
list.append(g);
readout.textContent = 'key: "' + e.key + '" \u00b7 code: "' + e.code + '"';
while (list.childElementCount > 4) list.firstElementChild.remove();
setTimeout(() => { g.setAttribute('data-out', ''); setTimeout(() => g.remove(), 300); }, 1600);
});
})();
```Here's a working CSS demo from CodeFronts. Use it as-is or adapt to your framework. All classes are scoped under a unique prefix so the code won't collide with your existing styles. MIT licensed.
Demo: Live Key Press Visualizer with Tailwind CSS
Source: https://codefronts.com/snippets/tailwind-css-kbd-keys/live-key-press-visualizer-tailwind-css/
A keystroke visualizer for screencasts and docs: focus the pad, press anything, and each press appears as a cap group — modifiers included, so Ctrl + Shift + K shows as one chord. Caps animate in with zero JS via starting: (@starting-style), leave via a data-out: variant, and a readout shows event.key beside event.code so the difference is visible.
## HTML
```html
<section id="kbd-15-root" class="kbd-15 relative w-full min-h-svh scheme-light kbd15-dark:scheme-dark bg-kbd15-bg bg-[radial-gradient(70%_55%_at_50%_45%,var(--color-kbd15-glow),transparent_70%)] text-kbd15-ink font-kbd15-sans" aria-label="Live key press feedback demo">
<div class="grid min-h-svh place-items-center px-5 pt-20 pb-12">
<div class="grid w-full min-w-0 max-w-[40rem] justify-items-center gap-6 text-center">
<button type="button" data-pad class="group grid min-h-56 w-full place-content-center gap-5 rounded-3xl border border-dashed border-kbd15-rim bg-kbd15-pad p-6 transition-[border-color,box-shadow] focus:border-solid focus:border-kbd15-acc focus:shadow-[0_0_0_4px_var(--color-kbd15-ring)] focus:outline-none motion-reduce:transition-none">
<span data-idle class="text-[15px] text-kbd15-mut"><span class="group-focus:hidden">Click here, then press any key</span><span class="hidden group-focus:inline">Listening — press any key or chord</span></span>
<span data-list aria-live="polite" class="flex min-h-14 flex-wrap items-center justify-center gap-3"></span>
</button>
<p data-readout class="min-h-5 font-kbd15-mono text-[12.5px] text-kbd15-mut">key · code readout appears here</p>
<p class="text-[12.5px] text-kbd15-mut">One group per physical press · repeats suppressed · Tab still moves focus</p>
<kbd data-chip hidden class="inline-flex items-center gap-1.5 rounded-2xl bg-kbd15-tray p-1.5 ring-1 ring-kbd15-rule transition-[opacity,translate,scale] duration-300 ease-out starting:translate-y-3 starting:scale-90 starting:opacity-0 data-out:-translate-y-2 data-out:opacity-0 motion-reduce:transition-none"></kbd>
<kbd data-cap hidden class="inline-flex h-14 min-w-14 items-center justify-center rounded-xl border border-kbd15-caprim bg-linear-to-b from-kbd15-captop to-kbd15-cap px-4 font-kbd15-mono text-lg font-semibold text-kbd15-capink shadow-[0_4px_0_var(--color-kbd15-edge)]"></kbd>
</div>
</div>
<div class="absolute top-3.5 right-4 z-10">
<input id="kbd-15-theme" type="checkbox" class="peer sr-only">
<label for="kbd-15-theme" class="inline-flex min-h-11 cursor-pointer select-none items-center gap-2.5 rounded-full border border-current/55 px-4 font-sans text-[11px] font-bold uppercase leading-none tracking-[0.16em] text-kbd15-ink transition-colors hover:border-current peer-focus-visible:outline-3 peer-focus-visible:outline-offset-2 peer-focus-visible:outline-kbd15-acc">
<span aria-hidden="true" class="size-3 flex-none rounded-full bg-current transition-[background-color,box-shadow] duration-300 kbd15-dark:bg-transparent kbd15-dark:shadow-[inset_-4px_-2px_0_0_currentColor] motion-reduce:transition-none"></span>
<span class="kbd15-dark:hidden">Light</span><span class="hidden kbd15-dark:inline">Dark</span><span class="sr-only"> theme</span>
</label>
</div>
</section>
```
## CSS
```css
@import "tailwindcss";
/* Theme state: follows the OS; the corner switch flips it for THIS demo only. */
@custom-variant kbd15-dark {
@media (prefers-color-scheme: dark) {
&:where(.kbd-15:not(:has(#kbd-15-theme:checked)), .kbd-15:not(:has(#kbd-15-theme:checked)) *) {
@slot;
}
}
@media (prefers-color-scheme: light) {
&:where(.kbd-15:has(#kbd-15-theme:checked), .kbd-15:has(#kbd-15-theme:checked) *) {
@slot;
}
}
}
/* Every colour is light-dark(day, night): the root's scheme-light / scheme-dark utility picks the side. */
@theme {
--color-kbd15-bg: light-dark(#eef0f4, #08090c);
--color-kbd15-bg: light-dark(oklch(0.955 0.006 264.5), oklch(0.14 0.007 269.7));
--color-kbd15-glow: light-dark(#dfe5ff, #141a33);
--color-kbd15-glow: light-dark(oklch(0.925 0.036 275.4), oklch(0.226 0.049 271.8));
--color-kbd15-pad: light-dark(#ffffff, #0f1116);
--color-kbd15-pad: light-dark(oklch(1 0 0), oklch(0.178 0.011 268.1));
--color-kbd15-ring: light-dark(#4f6bed33, #7c93ff40);
--color-kbd15-ring: light-dark(oklch(0.579 0.198 270 / 0.2), oklch(0.694 0.16 272.5 / 0.25));
--color-kbd15-ink: light-dark(#151821, #e9ebf2);
--color-kbd15-ink: light-dark(oklch(0.21 0.018 270.4), oklch(0.941 0.01 273.4));
--color-kbd15-mut: light-dark(#585f6e, #979eae);
--color-kbd15-mut: light-dark(oklch(0.485 0.026 265.6), oklch(0.699 0.025 266.8));
--color-kbd15-rim: light-dark(#c4cad6, #2f3442);
--color-kbd15-rim: light-dark(oklch(0.838 0.018 264.5), oklch(0.326 0.026 269.6));
--color-kbd15-rule: light-dark(#dde1ea, #23283a);
--color-kbd15-rule: light-dark(oklch(0.909 0.013 266.7), oklch(0.28 0.034 272.1));
--color-kbd15-tray: light-dark(#f1f3f8, #161a26);
--color-kbd15-tray: light-dark(oklch(0.964 0.007 268.5), oklch(0.22 0.024 270.1));
--color-kbd15-cap: light-dark(#f6f7fb, #222838);
--color-kbd15-cap: light-dark(oklch(0.977 0.005 275), oklch(0.278 0.031 268.5));
--color-kbd15-captop: light-dark(#ffffff, #2d3449);
--color-kbd15-captop: light-dark(oklch(1 0 0), oklch(0.328 0.039 270));
--color-kbd15-caprim: light-dark(#c9cfdb, #3d4560);
--color-kbd15-caprim: light-dark(oklch(0.853 0.018 264.5), oklch(0.395 0.047 271.4));
--color-kbd15-edge: light-dark(#a9b0c0, #040509);
--color-kbd15-edge: light-dark(oklch(0.757 0.024 266.9), oklch(0.116 0.012 270.7));
--color-kbd15-capink: light-dark(#1c2030, #eef0f8);
--color-kbd15-capink: light-dark(oklch(0.247 0.031 273.1), oklch(0.956 0.011 274.9));
--color-kbd15-acc: light-dark(#4f6bed, #7c93ff);
--color-kbd15-acc: light-dark(oklch(0.579 0.198 270), oklch(0.694 0.16 272.5));
--font-kbd15-sans: 'Space Grotesk', system-ui, sans-serif;
--font-kbd15-mono: 'JetBrains Mono', ui-monospace, monospace;
}
/* Root rule: the full-bleed contract, stated in CSS as well as utilities so the stage is sized before the browser build compiles. */
.kbd-15 {
width: 100%;
min-height: 100svh;
position: relative;
}
```
## JavaScript
```js
(() => {
const root = document.getElementById('kbd-15-root');
if (!root || root.dataset.wired) return;
root.dataset.wired = '1';
const pad = root.querySelector('[data-pad]'), list = root.querySelector('[data-list]');
const chip = root.querySelector('[data-chip]'), cap = root.querySelector('[data-cap]'), readout = root.querySelector('[data-readout]');
const isMac = /mac/i.test(navigator.userAgentData?.platform || navigator.platform);
const names = { ' ': 'Space', Escape: 'Esc', Control: 'Ctrl', Meta: isMac ? '\u2318' : 'Win', ArrowUp: '\u2191', ArrowDown: '\u2193', ArrowLeft: '\u2190', ArrowRight: '\u2192' };
const label = (k) => names[k] ?? (k.length === 1 ? k.toUpperCase() : k);
const clone = (el) => { const c = el.cloneNode(); c.removeAttribute('hidden'); return c; };
pad.addEventListener('keydown', (e) => {
if (e.repeat || e.key === 'Tab') return;
e.preventDefault();
const mods = [e.ctrlKey && 'Control', e.altKey && 'Alt', e.shiftKey && 'Shift', e.metaKey && 'Meta'].filter((m) => m && m !== e.key);
const g = clone(chip);
[...mods, e.key].forEach((k) => { const c = clone(cap); c.textContent = label(k); g.append(c); });
list.append(g);
readout.textContent = 'key: "' + e.key + '" \u00b7 code: "' + e.code + '"';
while (list.childElementCount > 4) list.firstElementChild.remove();
setTimeout(() => { g.setAttribute('data-out', ''); setTimeout(() => g.remove(), 300); }, 1600);
});
})();
```How this works
The script clones; Tailwind styles. Classes added at runtime must exist somewhere Tailwind can scan, so the chip and cap are real elements with hidden, cloned on each press:
<kbd data-chip hidden class="… transition duration-300
starting:opacity-0 starting:translate-y-3 data-out:opacity-0">key, not code. event.key is the character the layout produced (z on AZERTY's W position); event.code is the physical key (KeyW). A display of what the user typed wants key — the readout shows both. Repeat events are dropped so holding a key does not flood the pad.
Make it yours
- Longer on-screen time for recordings: raise the
1600ms timeout. - Bigger caps for 4K screencasts:
h-20 min-w-20 text-2xlon the cap prototype. - Global capture: move the listener to
document— only for a dedicated overlay page.
Gotchas — read before shipping
- Browser-reserved shortcuts (
Ctrl + W,Cmd + Q) never reach the page;preventDefault()cannot stop them. - Tab is passed through untouched so keyboard users can leave the pad.
@starting-styleneeds Chrome 117 / Safari 17.5 / Firefox 129; older engines show caps without the entry animation.
Browser support
| Chrome | Safari | Firefox | Edge |
|---|---|---|---|
| 123+ | 17.5+ | 129+ | 123+ |
starting: compiles to @starting-style (Firefox 129+); without it caps appear instantly. Tailwind v4 + light-dark() set the rest of the floor.