48 / 51
All 51 in 51 Tailwind CSS Buttons
- 01Tailwind Submit Button (Form Primary)
- 02Tailwind Primary and Secondary Button Pair
- 03Tailwind Ghost / Outline Button
- 04Tailwind Icon-Only Button with Accessible Tooltip
- 05Tailwind Gradient CTA Button
- 06Tailwind Loading Spinner Button (State Transition)
- 07Tailwind Disabled Button State (Full State Reference)
- 08Tailwind Button Group Component (Segmented Radio-Like)
- 09Tailwind Add to Cart Button
- 10Tailwind Download Progress Button
- 11Tailwind Like Heart Button
- 12Tailwind Confirm Delete Button
- 13Tailwind Play Pause Button
- 14Tailwind Subscribe Confetti Button
- 15Tailwind Magnetic Mercury Ripple Button
- 16Tailwind Brutalist Glitch Button
- 17Tailwind Neon Plasma Burst Button
- 18Tailwind Clay Press Button
- 19Tailwind Editorial Ink Spread Button
- 20Tailwind Particle Shard Burst Button
- 21Tailwind Brushed Brass Plate Button
- 22Tailwind Washi Paper Crane Button
- 23Tailwind Frosted Ice Cube Button
- 24Tailwind Vintage Typewriter Key Button
- 25Tailwind Lacquered Walnut Wood Button
- 26Tailwind Pharma Gel Capsule Button
- 27Tailwind Aged Copper Coin Button
- 28Tailwind Origami Fortune Cookie Button
- 29Tailwind Holographic Foil Sticker Button
- 30Tailwind Brutalist Concrete Slab Button
- 31Tailwind Mercury Bead Morph Button
- 32Tailwind Soft Pill Modern SaaS Button
- 33Tailwind Masking Tape Label Button
- 34Tailwind Obsidian Glass Shard Button
- 35Tailwind Wine Velvet Ribbon Button
- 36Tailwind LED Segment Display Button
- 37Tailwind Cassette Mixtape Label Button
- 38Tailwind Chalkboard Menu Button
- 39Tailwind Vinyl Record Sleeve Ripple Button
- 40Tailwind Polaroid Snapshot Button
- 41Tailwind Airline Boarding Pass Button
- 42Tailwind Aged Brass Subway Token Button
- 43Tailwind Vintage Rx Pharmacy Label Button
- 44Tailwind Bakelite Radio Dial Toggle Button
- 45Tailwind Regal Postage Stamp Button
- 46Tailwind Victorian Cigar Band Foil Button
- 47Tailwind Vintage Calculator Key Button
- 48Tailwind Toolbox Latch Spotlight Button
- 49Tailwind Receipt Paper Total Count-Up Button
- 50Tailwind IBM Punch Card Magnetic Follow Button
- 51Tailwind Brass Periscope Lens Button
Tailwind Toolbox Latch Spotlight Button
A steel toolbox latch under a work lamp: a spotlight tracks your pointer across the brushed plate, and clicking throws the latch — the hasp swings up over the keeper and the button reports its state through aria-pressed, not just its look.
Untitled UI Pro: 5,000+ React + Tailwind CSS v4 components · from $349 one-time
Untitled UI React · Affiliate link
The code
<div class="tb-48 w-full min-h-svh grid place-items-center font-tb48-sans antialiased p-[clamp(1.5rem,6vw,4rem)]">
<button type="button" aria-pressed="false" class="tb-48__plate group relative overflow-hidden cursor-pointer grid place-items-center w-68 min-h-22 pl-22 pr-4 border border-tb48-edge rounded-lg text-base font-extrabold tracking-[-0.01em] text-tb48-ink [text-shadow:0_1px_0_#ffffff73] shadow-[inset_0_1px_0_#ffffff80,inset_0_-2px_5px_#00000059,0_16px_26px_-14px_#000000cc] transition-[translate,box-shadow] duration-200 ease-tb48-out touch-manipulation [-webkit-tap-highlight-color:transparent] hover:-translate-y-0.5 active:translate-y-px active:shadow-[inset_0_2px_6px_#00000073] aria-pressed:shadow-[inset_0_2px_6px_#00000066,0_8px_16px_-12px_#000000cc] focus-visible:outline-2 focus-visible:outline-offset-4 focus-visible:outline-tb48-lamp motion-reduce:transition-none">
<span aria-hidden="true" class="tb-48__light absolute inset-0 z-30 pointer-events-none"></span>
<span aria-hidden="true" class="tb-48__rivet absolute z-20 top-2 right-2.5 size-2 rounded-full"></span>
<span aria-hidden="true" class="tb-48__rivet absolute z-20 bottom-2 right-2.5 size-2 rounded-full"></span>
<span aria-hidden="true" class="tb-48__keeper absolute z-10 left-4 top-1/2 -mt-[1.15rem] size-9.5 rounded-[0.2rem] shadow-[inset_0_1px_0_#ffffff47,0_2px_4px_#00000080]"></span>
<span aria-hidden="true" class="tb-48__hasp absolute z-20 left-[2.4rem] top-1/2 -mt-[0.55rem] w-10 h-[1.1rem] rounded-[0.2rem] shadow-[inset_0_1px_0_#ffffff99,0_2px_5px_#00000080] [transform-origin:calc(100%-0.55rem)_50%] transition-[rotate] duration-500 ease-tb48-out group-aria-pressed:-rotate-[58deg] motion-reduce:transition-none">
<span class="tb-48__pin absolute right-[0.28rem] top-1/2 size-[0.55rem] -mt-[0.275rem] rounded-full shadow-[inset_0_-1px_1px_#00000080]"></span>
</span>
<span class="relative z-40">Latch the case</span>
</button>
</div><div class="tb-48 w-full min-h-svh grid place-items-center font-tb48-sans antialiased p-[clamp(1.5rem,6vw,4rem)]">
<button type="button" aria-pressed="false" class="tb-48__plate group relative overflow-hidden cursor-pointer grid place-items-center w-68 min-h-22 pl-22 pr-4 border border-tb48-edge rounded-lg text-base font-extrabold tracking-[-0.01em] text-tb48-ink [text-shadow:0_1px_0_#ffffff73] shadow-[inset_0_1px_0_#ffffff80,inset_0_-2px_5px_#00000059,0_16px_26px_-14px_#000000cc] transition-[translate,box-shadow] duration-200 ease-tb48-out touch-manipulation [-webkit-tap-highlight-color:transparent] hover:-translate-y-0.5 active:translate-y-px active:shadow-[inset_0_2px_6px_#00000073] aria-pressed:shadow-[inset_0_2px_6px_#00000066,0_8px_16px_-12px_#000000cc] focus-visible:outline-2 focus-visible:outline-offset-4 focus-visible:outline-tb48-lamp motion-reduce:transition-none">
<span aria-hidden="true" class="tb-48__light absolute inset-0 z-30 pointer-events-none"></span>
<span aria-hidden="true" class="tb-48__rivet absolute z-20 top-2 right-2.5 size-2 rounded-full"></span>
<span aria-hidden="true" class="tb-48__rivet absolute z-20 bottom-2 right-2.5 size-2 rounded-full"></span>
<span aria-hidden="true" class="tb-48__keeper absolute z-10 left-4 top-1/2 -mt-[1.15rem] size-9.5 rounded-[0.2rem] shadow-[inset_0_1px_0_#ffffff47,0_2px_4px_#00000080]"></span>
<span aria-hidden="true" class="tb-48__hasp absolute z-20 left-[2.4rem] top-1/2 -mt-[0.55rem] w-10 h-[1.1rem] rounded-[0.2rem] shadow-[inset_0_1px_0_#ffffff99,0_2px_5px_#00000080] [transform-origin:calc(100%-0.55rem)_50%] transition-[rotate] duration-500 ease-tb48-out group-aria-pressed:-rotate-[58deg] motion-reduce:transition-none">
<span class="tb-48__pin absolute right-[0.28rem] top-1/2 size-[0.55rem] -mt-[0.275rem] rounded-full shadow-[inset_0_-1px_1px_#00000080]"></span>
</span>
<span class="relative z-40">Latch the case</span>
</button>
</div>@import "tailwindcss";
@theme {
--color-tb48-bg: #191b1d;
--color-tb48-bg: oklch(0.21 0.006 240);
--color-tb48-steel: #9ba3aa;
--color-tb48-steel: oklch(0.7 0.008 240);
--color-tb48-steel-hi: #d3d9de;
--color-tb48-steel-hi: oklch(0.87 0.008 240);
--color-tb48-steel-lo: #5f676d;
--color-tb48-steel-lo: oklch(0.48 0.01 240);
--color-tb48-edge: #2a2f33;
--color-tb48-edge: oklch(0.28 0.008 240);
--color-tb48-ink: #1b1f22;
--color-tb48-ink: oklch(0.22 0.008 240);
--color-tb48-lamp: #fff6d6;
--color-tb48-lamp: oklch(0.97 0.05 95);
--font-tb48-sans: "Archivo", ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif;
--ease-tb48-out: cubic-bezier(0.16, 1, 0.3, 1);
}
.tb-48 {
width: 100%;
min-height: 100vh;
min-height: 100svh;
background: radial-gradient(30rem 18rem at 50% 30%, #24282b, transparent 72%), var(--color-tb48-bg);
}
/* Pointer defaults: with no pointer (touch, keyboard) the plate keeps a static
top-light rather than going dead. */
.tb-48__plate {
--mx: 50%;
--my: 0%;
background-color: var(--color-tb48-steel);
background-image: repeating-linear-gradient(90deg, rgba(255,255,255,.1) 0 1px, transparent 1px 2px, rgba(0,0,0,.06) 2px 3px, transparent 3px 6px), linear-gradient(170deg, var(--color-tb48-steel-hi), var(--color-tb48-steel) 45%, var(--color-tb48-steel-lo));
}
.tb-48__light {
mix-blend-mode: screen;
background: radial-gradient(7rem 7rem at var(--mx) var(--my), rgba(255,246,214,.34), rgba(255,246,214,.09) 42%, transparent 68%);
}
.tb-48__keeper {
background: linear-gradient(160deg, var(--color-tb48-steel-lo), #3d4348);
}
.tb-48__keeper::after {
content: '';
position: absolute;
inset: 32% 22%;
border-radius: .1rem;
background: #15181a;
box-shadow: inset 0 1px 2px rgba(0,0,0,.9);
}
.tb-48__hasp {
background: linear-gradient(180deg, var(--color-tb48-steel-hi), var(--color-tb48-steel) 55%, var(--color-tb48-steel-lo));
}
.tb-48__hasp::after {
content: '';
position: absolute;
left: .35rem;
top: .3rem;
width: 1.1rem;
height: .5rem;
border-radius: .1rem;
background: #15181a;
box-shadow: inset 0 1px 2px rgba(0,0,0,.8);
}
.tb-48__pin {
background: radial-gradient(circle at 34% 30%, #eef2f5, #6d757b 62%, #3f4649);
}
.tb-48__rivet {
background: radial-gradient(circle at 34% 30%, #f2f5f7, #7d858b 60%, #4a5155);
box-shadow: inset 0 -1px 1px rgba(0,0,0,.45), 0 1px 0 rgba(255,255,255,.3);
}
@media (prefers-color-scheme: light) {
.tb-48:not([data-theme="dark"]) {
--color-tb48-bg: oklch(0.25 0.006 240);
}
}
.tb-48[data-theme="light"] {
--color-tb48-bg: oklch(0.25 0.006 240);
}
@media (forced-colors: active) {
.tb-48__plate {
background: ButtonFace;
color: ButtonText;
border: 1px solid ButtonText;
box-shadow: none;
text-shadow: none;
}
.tb-48__light,
.tb-48__rivet,
.tb-48__pin {
display: none;
}
.tb-48__hasp,
.tb-48__keeper {
background: ButtonFace;
border: 1px solid ButtonText;
box-shadow: none;
}
}@import "tailwindcss";
@theme {
--color-tb48-bg: #191b1d;
--color-tb48-bg: oklch(0.21 0.006 240);
--color-tb48-steel: #9ba3aa;
--color-tb48-steel: oklch(0.7 0.008 240);
--color-tb48-steel-hi: #d3d9de;
--color-tb48-steel-hi: oklch(0.87 0.008 240);
--color-tb48-steel-lo: #5f676d;
--color-tb48-steel-lo: oklch(0.48 0.01 240);
--color-tb48-edge: #2a2f33;
--color-tb48-edge: oklch(0.28 0.008 240);
--color-tb48-ink: #1b1f22;
--color-tb48-ink: oklch(0.22 0.008 240);
--color-tb48-lamp: #fff6d6;
--color-tb48-lamp: oklch(0.97 0.05 95);
--font-tb48-sans: "Archivo", ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif;
--ease-tb48-out: cubic-bezier(0.16, 1, 0.3, 1);
}
.tb-48 {
width: 100%;
min-height: 100vh;
min-height: 100svh;
background: radial-gradient(30rem 18rem at 50% 30%, #24282b, transparent 72%), var(--color-tb48-bg);
}
/* Pointer defaults: with no pointer (touch, keyboard) the plate keeps a static
top-light rather than going dead. */
.tb-48__plate {
--mx: 50%;
--my: 0%;
background-color: var(--color-tb48-steel);
background-image: repeating-linear-gradient(90deg, rgba(255,255,255,.1) 0 1px, transparent 1px 2px, rgba(0,0,0,.06) 2px 3px, transparent 3px 6px), linear-gradient(170deg, var(--color-tb48-steel-hi), var(--color-tb48-steel) 45%, var(--color-tb48-steel-lo));
}
.tb-48__light {
mix-blend-mode: screen;
background: radial-gradient(7rem 7rem at var(--mx) var(--my), rgba(255,246,214,.34), rgba(255,246,214,.09) 42%, transparent 68%);
}
.tb-48__keeper {
background: linear-gradient(160deg, var(--color-tb48-steel-lo), #3d4348);
}
.tb-48__keeper::after {
content: '';
position: absolute;
inset: 32% 22%;
border-radius: .1rem;
background: #15181a;
box-shadow: inset 0 1px 2px rgba(0,0,0,.9);
}
.tb-48__hasp {
background: linear-gradient(180deg, var(--color-tb48-steel-hi), var(--color-tb48-steel) 55%, var(--color-tb48-steel-lo));
}
.tb-48__hasp::after {
content: '';
position: absolute;
left: .35rem;
top: .3rem;
width: 1.1rem;
height: .5rem;
border-radius: .1rem;
background: #15181a;
box-shadow: inset 0 1px 2px rgba(0,0,0,.8);
}
.tb-48__pin {
background: radial-gradient(circle at 34% 30%, #eef2f5, #6d757b 62%, #3f4649);
}
.tb-48__rivet {
background: radial-gradient(circle at 34% 30%, #f2f5f7, #7d858b 60%, #4a5155);
box-shadow: inset 0 -1px 1px rgba(0,0,0,.45), 0 1px 0 rgba(255,255,255,.3);
}
@media (prefers-color-scheme: light) {
.tb-48:not([data-theme="dark"]) {
--color-tb48-bg: oklch(0.25 0.006 240);
}
}
.tb-48[data-theme="light"] {
--color-tb48-bg: oklch(0.25 0.006 240);
}
@media (forced-colors: active) {
.tb-48__plate {
background: ButtonFace;
color: ButtonText;
border: 1px solid ButtonText;
box-shadow: none;
text-shadow: none;
}
.tb-48__light,
.tb-48__rivet,
.tb-48__pin {
display: none;
}
.tb-48__hasp,
.tb-48__keeper {
background: ButtonFace;
border: 1px solid ButtonText;
box-shadow: none;
}
}(() => {
const btn = document.querySelector('.tb-48__plate');
if (!btn) return;
let box = null;
btn.addEventListener('pointerenter', () => { box = btn.getBoundingClientRect(); });
btn.addEventListener('pointermove', (e) => {
box = box || btn.getBoundingClientRect();
btn.style.setProperty('--mx', (((e.clientX - box.left) / box.width) * 100).toFixed(1) + '%');
btn.style.setProperty('--my', (((e.clientY - box.top) / box.height) * 100).toFixed(1) + '%');
});
btn.addEventListener('pointerleave', () => {
box = null;
btn.style.setProperty('--mx', '50%');
btn.style.setProperty('--my', '0%');
});
// The ONLY state the controller owns is the attribute. CSS reads it from there.
btn.addEventListener('click', () => {
const on = btn.getAttribute('aria-pressed') === 'true';
btn.setAttribute('aria-pressed', String(!on));
});
})();(() => {
const btn = document.querySelector('.tb-48__plate');
if (!btn) return;
let box = null;
btn.addEventListener('pointerenter', () => { box = btn.getBoundingClientRect(); });
btn.addEventListener('pointermove', (e) => {
box = box || btn.getBoundingClientRect();
btn.style.setProperty('--mx', (((e.clientX - box.left) / box.width) * 100).toFixed(1) + '%');
btn.style.setProperty('--my', (((e.clientY - box.top) / box.height) * 100).toFixed(1) + '%');
});
btn.addEventListener('pointerleave', () => {
box = null;
btn.style.setProperty('--mx', '50%');
btn.style.setProperty('--my', '0%');
});
// The ONLY state the controller owns is the attribute. CSS reads it from there.
btn.addEventListener('click', () => {
const on = btn.getAttribute('aria-pressed') === 'true';
btn.setAttribute('aria-pressed', String(!on));
});
})();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 {
--color-tb48-bg: #191b1d;
--color-tb48-bg: oklch(0.21 0.006 240);
--color-tb48-steel: #9ba3aa;
--color-tb48-steel: oklch(0.7 0.008 240);
--color-tb48-steel-hi: #d3d9de;
--color-tb48-steel-hi: oklch(0.87 0.008 240);
--color-tb48-steel-lo: #5f676d;
--color-tb48-steel-lo: oklch(0.48 0.01 240);
--color-tb48-edge: #2a2f33;
--color-tb48-edge: oklch(0.28 0.008 240);
--color-tb48-ink: #1b1f22;
--color-tb48-ink: oklch(0.22 0.008 240);
--color-tb48-lamp: #fff6d6;
--color-tb48-lamp: oklch(0.97 0.05 95);
--font-tb48-sans: "Archivo", ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif;
--ease-tb48-out: cubic-bezier(0.16, 1, 0.3, 1);
}
.tb-48 {
width: 100%;
min-height: 100vh;
min-height: 100svh;
background: radial-gradient(30rem 18rem at 50% 30%, #24282b, transparent 72%), var(--color-tb48-bg);
}
/* Pointer defaults: with no pointer (touch, keyboard) the plate keeps a static
top-light rather than going dead. */
.tb-48__plate {
--mx: 50%;
--my: 0%;
background-color: var(--color-tb48-steel);
background-image: repeating-linear-gradient(90deg, rgba(255,255,255,.1) 0 1px, transparent 1px 2px, rgba(0,0,0,.06) 2px 3px, transparent 3px 6px), linear-gradient(170deg, var(--color-tb48-steel-hi), var(--color-tb48-steel) 45%, var(--color-tb48-steel-lo));
}
.tb-48__light {
mix-blend-mode: screen;
background: radial-gradient(7rem 7rem at var(--mx) var(--my), rgba(255,246,214,.34), rgba(255,246,214,.09) 42%, transparent 68%);
}
.tb-48__keeper {
background: linear-gradient(160deg, var(--color-tb48-steel-lo), #3d4348);
}
.tb-48__keeper::after {
content: '';
position: absolute;
inset: 32% 22%;
border-radius: .1rem;
background: #15181a;
box-shadow: inset 0 1px 2px rgba(0,0,0,.9);
}
.tb-48__hasp {
background: linear-gradient(180deg, var(--color-tb48-steel-hi), var(--color-tb48-steel) 55%, var(--color-tb48-steel-lo));
}
.tb-48__hasp::after {
content: '';
position: absolute;
left: .35rem;
top: .3rem;
width: 1.1rem;
height: .5rem;
border-radius: .1rem;
background: #15181a;
box-shadow: inset 0 1px 2px rgba(0,0,0,.8);
}
.tb-48__pin {
background: radial-gradient(circle at 34% 30%, #eef2f5, #6d757b 62%, #3f4649);
}
.tb-48__rivet {
background: radial-gradient(circle at 34% 30%, #f2f5f7, #7d858b 60%, #4a5155);
box-shadow: inset 0 -1px 1px rgba(0,0,0,.45), 0 1px 0 rgba(255,255,255,.3);
}
@media (prefers-color-scheme: light) {
.tb-48:not([data-theme="dark"]) {
--color-tb48-bg: oklch(0.25 0.006 240);
}
}
.tb-48[data-theme="light"] {
--color-tb48-bg: oklch(0.25 0.006 240);
}
@media (forced-colors: active) {
.tb-48__plate {
background: ButtonFace;
color: ButtonText;
border: 1px solid ButtonText;
box-shadow: none;
text-shadow: none;
}
.tb-48__light,
.tb-48__rivet,
.tb-48__pin {
display: none;
}
.tb-48__hasp,
.tb-48__keeper {
background: ButtonFace;
border: 1px solid ButtonText;
box-shadow: none;
}
}@import "tailwindcss";
@theme {
--color-tb48-bg: #191b1d;
--color-tb48-bg: oklch(0.21 0.006 240);
--color-tb48-steel: #9ba3aa;
--color-tb48-steel: oklch(0.7 0.008 240);
--color-tb48-steel-hi: #d3d9de;
--color-tb48-steel-hi: oklch(0.87 0.008 240);
--color-tb48-steel-lo: #5f676d;
--color-tb48-steel-lo: oklch(0.48 0.01 240);
--color-tb48-edge: #2a2f33;
--color-tb48-edge: oklch(0.28 0.008 240);
--color-tb48-ink: #1b1f22;
--color-tb48-ink: oklch(0.22 0.008 240);
--color-tb48-lamp: #fff6d6;
--color-tb48-lamp: oklch(0.97 0.05 95);
--font-tb48-sans: "Archivo", ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif;
--ease-tb48-out: cubic-bezier(0.16, 1, 0.3, 1);
}
.tb-48 {
width: 100%;
min-height: 100vh;
min-height: 100svh;
background: radial-gradient(30rem 18rem at 50% 30%, #24282b, transparent 72%), var(--color-tb48-bg);
}
/* Pointer defaults: with no pointer (touch, keyboard) the plate keeps a static
top-light rather than going dead. */
.tb-48__plate {
--mx: 50%;
--my: 0%;
background-color: var(--color-tb48-steel);
background-image: repeating-linear-gradient(90deg, rgba(255,255,255,.1) 0 1px, transparent 1px 2px, rgba(0,0,0,.06) 2px 3px, transparent 3px 6px), linear-gradient(170deg, var(--color-tb48-steel-hi), var(--color-tb48-steel) 45%, var(--color-tb48-steel-lo));
}
.tb-48__light {
mix-blend-mode: screen;
background: radial-gradient(7rem 7rem at var(--mx) var(--my), rgba(255,246,214,.34), rgba(255,246,214,.09) 42%, transparent 68%);
}
.tb-48__keeper {
background: linear-gradient(160deg, var(--color-tb48-steel-lo), #3d4348);
}
.tb-48__keeper::after {
content: '';
position: absolute;
inset: 32% 22%;
border-radius: .1rem;
background: #15181a;
box-shadow: inset 0 1px 2px rgba(0,0,0,.9);
}
.tb-48__hasp {
background: linear-gradient(180deg, var(--color-tb48-steel-hi), var(--color-tb48-steel) 55%, var(--color-tb48-steel-lo));
}
.tb-48__hasp::after {
content: '';
position: absolute;
left: .35rem;
top: .3rem;
width: 1.1rem;
height: .5rem;
border-radius: .1rem;
background: #15181a;
box-shadow: inset 0 1px 2px rgba(0,0,0,.8);
}
.tb-48__pin {
background: radial-gradient(circle at 34% 30%, #eef2f5, #6d757b 62%, #3f4649);
}
.tb-48__rivet {
background: radial-gradient(circle at 34% 30%, #f2f5f7, #7d858b 60%, #4a5155);
box-shadow: inset 0 -1px 1px rgba(0,0,0,.45), 0 1px 0 rgba(255,255,255,.3);
}
@media (prefers-color-scheme: light) {
.tb-48:not([data-theme="dark"]) {
--color-tb48-bg: oklch(0.25 0.006 240);
}
}
.tb-48[data-theme="light"] {
--color-tb48-bg: oklch(0.25 0.006 240);
}
@media (forced-colors: active) {
.tb-48__plate {
background: ButtonFace;
color: ButtonText;
border: 1px solid ButtonText;
box-shadow: none;
text-shadow: none;
}
.tb-48__light,
.tb-48__rivet,
.tb-48__pin {
display: none;
}
.tb-48__hasp,
.tb-48__keeper {
background: ButtonFace;
border: 1px solid ButtonText;
box-shadow: none;
}
}<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=Archivo:wght@600;700;800&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=Archivo:wght@600;700;800&display=swap">What changed in the port
Which parts became utilities, which stayed CSS, and why.
- Utilities: the plate, the pressed shadow via
aria-pressed:, the hasp's hinge and swing viagroup-aria-pressed:, the rivets' positions, every state. CSS: the brushed grain, the spotlight gradient with its blend mode, and the small hardware gradients (keeper, hasp, pin, rivet) plus their two slot pseudo-elements. aria-pressed:andgroup-aria-pressed:are the best thing in this port. The CSS original already keyed off[aria-pressed="true"]— the variant form makes that choice visible on both the plate and the hasp, so "accessible state is the source of truth" stops being a convention and becomes something you can read in the markup.- The pointer defaults are in the stylesheet rather than inline for a reason worth repeating from demo 15: they are the no-JS, no-pointer behaviour, and an inline style would suggest JS owns them.
- A11y fix from the CSS original: none needed on contrast — but the port adds
motion-reduce:transition-noneon the hasp, which the original omitted (it only reset the button's own transition), so a 58° swing no longer plays for users who asked for less motion.
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: Tailwind Toolbox Latch Spotlight Button
Source: https://codefronts.com/components/tailwind-css-buttons/tailwind-toolbox-latch-spotlight-button/
A steel toolbox latch under a work lamp: a spotlight tracks your pointer across the brushed plate, and clicking throws the latch — the hasp swings up over the keeper and the button reports its state through aria-pressed, not just its look.
## HTML
```html
<div class="tb-48 w-full min-h-svh grid place-items-center font-tb48-sans antialiased p-[clamp(1.5rem,6vw,4rem)]">
<button type="button" aria-pressed="false" class="tb-48__plate group relative overflow-hidden cursor-pointer grid place-items-center w-68 min-h-22 pl-22 pr-4 border border-tb48-edge rounded-lg text-base font-extrabold tracking-[-0.01em] text-tb48-ink [text-shadow:0_1px_0_#ffffff73] shadow-[inset_0_1px_0_#ffffff80,inset_0_-2px_5px_#00000059,0_16px_26px_-14px_#000000cc] transition-[translate,box-shadow] duration-200 ease-tb48-out touch-manipulation [-webkit-tap-highlight-color:transparent] hover:-translate-y-0.5 active:translate-y-px active:shadow-[inset_0_2px_6px_#00000073] aria-pressed:shadow-[inset_0_2px_6px_#00000066,0_8px_16px_-12px_#000000cc] focus-visible:outline-2 focus-visible:outline-offset-4 focus-visible:outline-tb48-lamp motion-reduce:transition-none">
<span aria-hidden="true" class="tb-48__light absolute inset-0 z-30 pointer-events-none"></span>
<span aria-hidden="true" class="tb-48__rivet absolute z-20 top-2 right-2.5 size-2 rounded-full"></span>
<span aria-hidden="true" class="tb-48__rivet absolute z-20 bottom-2 right-2.5 size-2 rounded-full"></span>
<span aria-hidden="true" class="tb-48__keeper absolute z-10 left-4 top-1/2 -mt-[1.15rem] size-9.5 rounded-[0.2rem] shadow-[inset_0_1px_0_#ffffff47,0_2px_4px_#00000080]"></span>
<span aria-hidden="true" class="tb-48__hasp absolute z-20 left-[2.4rem] top-1/2 -mt-[0.55rem] w-10 h-[1.1rem] rounded-[0.2rem] shadow-[inset_0_1px_0_#ffffff99,0_2px_5px_#00000080] [transform-origin:calc(100%-0.55rem)_50%] transition-[rotate] duration-500 ease-tb48-out group-aria-pressed:-rotate-[58deg] motion-reduce:transition-none">
<span class="tb-48__pin absolute right-[0.28rem] top-1/2 size-[0.55rem] -mt-[0.275rem] rounded-full shadow-[inset_0_-1px_1px_#00000080]"></span>
</span>
<span class="relative z-40">Latch the case</span>
</button>
</div>
```
## CSS
```css
@import "tailwindcss";
@theme {
--color-tb48-bg: #191b1d;
--color-tb48-bg: oklch(0.21 0.006 240);
--color-tb48-steel: #9ba3aa;
--color-tb48-steel: oklch(0.7 0.008 240);
--color-tb48-steel-hi: #d3d9de;
--color-tb48-steel-hi: oklch(0.87 0.008 240);
--color-tb48-steel-lo: #5f676d;
--color-tb48-steel-lo: oklch(0.48 0.01 240);
--color-tb48-edge: #2a2f33;
--color-tb48-edge: oklch(0.28 0.008 240);
--color-tb48-ink: #1b1f22;
--color-tb48-ink: oklch(0.22 0.008 240);
--color-tb48-lamp: #fff6d6;
--color-tb48-lamp: oklch(0.97 0.05 95);
--font-tb48-sans: "Archivo", ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif;
--ease-tb48-out: cubic-bezier(0.16, 1, 0.3, 1);
}
.tb-48 {
width: 100%;
min-height: 100vh;
min-height: 100svh;
background: radial-gradient(30rem 18rem at 50% 30%, #24282b, transparent 72%), var(--color-tb48-bg);
}
/* Pointer defaults: with no pointer (touch, keyboard) the plate keeps a static
top-light rather than going dead. */
.tb-48__plate {
--mx: 50%;
--my: 0%;
background-color: var(--color-tb48-steel);
background-image: repeating-linear-gradient(90deg, rgba(255,255,255,.1) 0 1px, transparent 1px 2px, rgba(0,0,0,.06) 2px 3px, transparent 3px 6px), linear-gradient(170deg, var(--color-tb48-steel-hi), var(--color-tb48-steel) 45%, var(--color-tb48-steel-lo));
}
.tb-48__light {
mix-blend-mode: screen;
background: radial-gradient(7rem 7rem at var(--mx) var(--my), rgba(255,246,214,.34), rgba(255,246,214,.09) 42%, transparent 68%);
}
.tb-48__keeper {
background: linear-gradient(160deg, var(--color-tb48-steel-lo), #3d4348);
}
.tb-48__keeper::after {
content: '';
position: absolute;
inset: 32% 22%;
border-radius: .1rem;
background: #15181a;
box-shadow: inset 0 1px 2px rgba(0,0,0,.9);
}
.tb-48__hasp {
background: linear-gradient(180deg, var(--color-tb48-steel-hi), var(--color-tb48-steel) 55%, var(--color-tb48-steel-lo));
}
.tb-48__hasp::after {
content: '';
position: absolute;
left: .35rem;
top: .3rem;
width: 1.1rem;
height: .5rem;
border-radius: .1rem;
background: #15181a;
box-shadow: inset 0 1px 2px rgba(0,0,0,.8);
}
.tb-48__pin {
background: radial-gradient(circle at 34% 30%, #eef2f5, #6d757b 62%, #3f4649);
}
.tb-48__rivet {
background: radial-gradient(circle at 34% 30%, #f2f5f7, #7d858b 60%, #4a5155);
box-shadow: inset 0 -1px 1px rgba(0,0,0,.45), 0 1px 0 rgba(255,255,255,.3);
}
@media (prefers-color-scheme: light) {
.tb-48:not([data-theme="dark"]) {
--color-tb48-bg: oklch(0.25 0.006 240);
}
}
.tb-48[data-theme="light"] {
--color-tb48-bg: oklch(0.25 0.006 240);
}
@media (forced-colors: active) {
.tb-48__plate {
background: ButtonFace;
color: ButtonText;
border: 1px solid ButtonText;
box-shadow: none;
text-shadow: none;
}
.tb-48__light,
.tb-48__rivet,
.tb-48__pin {
display: none;
}
.tb-48__hasp,
.tb-48__keeper {
background: ButtonFace;
border: 1px solid ButtonText;
box-shadow: none;
}
}
```
## JavaScript
```js
(() => {
const btn = document.querySelector('.tb-48__plate');
if (!btn) return;
let box = null;
btn.addEventListener('pointerenter', () => { box = btn.getBoundingClientRect(); });
btn.addEventListener('pointermove', (e) => {
box = box || btn.getBoundingClientRect();
btn.style.setProperty('--mx', (((e.clientX - box.left) / box.width) * 100).toFixed(1) + '%');
btn.style.setProperty('--my', (((e.clientY - box.top) / box.height) * 100).toFixed(1) + '%');
});
btn.addEventListener('pointerleave', () => {
box = null;
btn.style.setProperty('--mx', '50%');
btn.style.setProperty('--my', '0%');
});
// The ONLY state the controller owns is the attribute. CSS reads it from there.
btn.addEventListener('click', () => {
const on = btn.getAttribute('aria-pressed') === 'true';
btn.setAttribute('aria-pressed', String(!on));
});
})();
```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: Tailwind Toolbox Latch Spotlight Button
Source: https://codefronts.com/components/tailwind-css-buttons/tailwind-toolbox-latch-spotlight-button/
A steel toolbox latch under a work lamp: a spotlight tracks your pointer across the brushed plate, and clicking throws the latch — the hasp swings up over the keeper and the button reports its state through aria-pressed, not just its look.
## HTML
```html
<div class="tb-48 w-full min-h-svh grid place-items-center font-tb48-sans antialiased p-[clamp(1.5rem,6vw,4rem)]">
<button type="button" aria-pressed="false" class="tb-48__plate group relative overflow-hidden cursor-pointer grid place-items-center w-68 min-h-22 pl-22 pr-4 border border-tb48-edge rounded-lg text-base font-extrabold tracking-[-0.01em] text-tb48-ink [text-shadow:0_1px_0_#ffffff73] shadow-[inset_0_1px_0_#ffffff80,inset_0_-2px_5px_#00000059,0_16px_26px_-14px_#000000cc] transition-[translate,box-shadow] duration-200 ease-tb48-out touch-manipulation [-webkit-tap-highlight-color:transparent] hover:-translate-y-0.5 active:translate-y-px active:shadow-[inset_0_2px_6px_#00000073] aria-pressed:shadow-[inset_0_2px_6px_#00000066,0_8px_16px_-12px_#000000cc] focus-visible:outline-2 focus-visible:outline-offset-4 focus-visible:outline-tb48-lamp motion-reduce:transition-none">
<span aria-hidden="true" class="tb-48__light absolute inset-0 z-30 pointer-events-none"></span>
<span aria-hidden="true" class="tb-48__rivet absolute z-20 top-2 right-2.5 size-2 rounded-full"></span>
<span aria-hidden="true" class="tb-48__rivet absolute z-20 bottom-2 right-2.5 size-2 rounded-full"></span>
<span aria-hidden="true" class="tb-48__keeper absolute z-10 left-4 top-1/2 -mt-[1.15rem] size-9.5 rounded-[0.2rem] shadow-[inset_0_1px_0_#ffffff47,0_2px_4px_#00000080]"></span>
<span aria-hidden="true" class="tb-48__hasp absolute z-20 left-[2.4rem] top-1/2 -mt-[0.55rem] w-10 h-[1.1rem] rounded-[0.2rem] shadow-[inset_0_1px_0_#ffffff99,0_2px_5px_#00000080] [transform-origin:calc(100%-0.55rem)_50%] transition-[rotate] duration-500 ease-tb48-out group-aria-pressed:-rotate-[58deg] motion-reduce:transition-none">
<span class="tb-48__pin absolute right-[0.28rem] top-1/2 size-[0.55rem] -mt-[0.275rem] rounded-full shadow-[inset_0_-1px_1px_#00000080]"></span>
</span>
<span class="relative z-40">Latch the case</span>
</button>
</div>
```
## CSS
```css
@import "tailwindcss";
@theme {
--color-tb48-bg: #191b1d;
--color-tb48-bg: oklch(0.21 0.006 240);
--color-tb48-steel: #9ba3aa;
--color-tb48-steel: oklch(0.7 0.008 240);
--color-tb48-steel-hi: #d3d9de;
--color-tb48-steel-hi: oklch(0.87 0.008 240);
--color-tb48-steel-lo: #5f676d;
--color-tb48-steel-lo: oklch(0.48 0.01 240);
--color-tb48-edge: #2a2f33;
--color-tb48-edge: oklch(0.28 0.008 240);
--color-tb48-ink: #1b1f22;
--color-tb48-ink: oklch(0.22 0.008 240);
--color-tb48-lamp: #fff6d6;
--color-tb48-lamp: oklch(0.97 0.05 95);
--font-tb48-sans: "Archivo", ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif;
--ease-tb48-out: cubic-bezier(0.16, 1, 0.3, 1);
}
.tb-48 {
width: 100%;
min-height: 100vh;
min-height: 100svh;
background: radial-gradient(30rem 18rem at 50% 30%, #24282b, transparent 72%), var(--color-tb48-bg);
}
/* Pointer defaults: with no pointer (touch, keyboard) the plate keeps a static
top-light rather than going dead. */
.tb-48__plate {
--mx: 50%;
--my: 0%;
background-color: var(--color-tb48-steel);
background-image: repeating-linear-gradient(90deg, rgba(255,255,255,.1) 0 1px, transparent 1px 2px, rgba(0,0,0,.06) 2px 3px, transparent 3px 6px), linear-gradient(170deg, var(--color-tb48-steel-hi), var(--color-tb48-steel) 45%, var(--color-tb48-steel-lo));
}
.tb-48__light {
mix-blend-mode: screen;
background: radial-gradient(7rem 7rem at var(--mx) var(--my), rgba(255,246,214,.34), rgba(255,246,214,.09) 42%, transparent 68%);
}
.tb-48__keeper {
background: linear-gradient(160deg, var(--color-tb48-steel-lo), #3d4348);
}
.tb-48__keeper::after {
content: '';
position: absolute;
inset: 32% 22%;
border-radius: .1rem;
background: #15181a;
box-shadow: inset 0 1px 2px rgba(0,0,0,.9);
}
.tb-48__hasp {
background: linear-gradient(180deg, var(--color-tb48-steel-hi), var(--color-tb48-steel) 55%, var(--color-tb48-steel-lo));
}
.tb-48__hasp::after {
content: '';
position: absolute;
left: .35rem;
top: .3rem;
width: 1.1rem;
height: .5rem;
border-radius: .1rem;
background: #15181a;
box-shadow: inset 0 1px 2px rgba(0,0,0,.8);
}
.tb-48__pin {
background: radial-gradient(circle at 34% 30%, #eef2f5, #6d757b 62%, #3f4649);
}
.tb-48__rivet {
background: radial-gradient(circle at 34% 30%, #f2f5f7, #7d858b 60%, #4a5155);
box-shadow: inset 0 -1px 1px rgba(0,0,0,.45), 0 1px 0 rgba(255,255,255,.3);
}
@media (prefers-color-scheme: light) {
.tb-48:not([data-theme="dark"]) {
--color-tb48-bg: oklch(0.25 0.006 240);
}
}
.tb-48[data-theme="light"] {
--color-tb48-bg: oklch(0.25 0.006 240);
}
@media (forced-colors: active) {
.tb-48__plate {
background: ButtonFace;
color: ButtonText;
border: 1px solid ButtonText;
box-shadow: none;
text-shadow: none;
}
.tb-48__light,
.tb-48__rivet,
.tb-48__pin {
display: none;
}
.tb-48__hasp,
.tb-48__keeper {
background: ButtonFace;
border: 1px solid ButtonText;
box-shadow: none;
}
}
```
## JavaScript
```js
(() => {
const btn = document.querySelector('.tb-48__plate');
if (!btn) return;
let box = null;
btn.addEventListener('pointerenter', () => { box = btn.getBoundingClientRect(); });
btn.addEventListener('pointermove', (e) => {
box = box || btn.getBoundingClientRect();
btn.style.setProperty('--mx', (((e.clientX - box.left) / box.width) * 100).toFixed(1) + '%');
btn.style.setProperty('--my', (((e.clientY - box.top) / box.height) * 100).toFixed(1) + '%');
});
btn.addEventListener('pointerleave', () => {
box = null;
btn.style.setProperty('--mx', '50%');
btn.style.setProperty('--my', '0%');
});
// The ONLY state the controller owns is the attribute. CSS reads it from there.
btn.addEventListener('click', () => {
const on = btn.getAttribute('aria-pressed') === 'true';
btn.setAttribute('aria-pressed', String(!on));
});
})();
```How this works
The state lives on the attribute, and CSS reads it there. The controller flips aria-pressed and nothing else:
btn.addEventListener('click', () => {
const on = btn.getAttribute('aria-pressed') === 'true';
btn.setAttribute('aria-pressed', String(!on));
});Then group-aria-pressed:-rotate-[58deg] on the hasp and aria-pressed:shadow-[…] on the plate do the rest. If the attribute is ever wrong the button looks wrong, which is the best kind of bug.
The spotlight degrades by default. --mx: 50%; --my: 0% is declared in the stylesheet, so with no pointer the plate keeps a static top-light. That default is not decoration — it is what makes the control usable on touch.
Make it yours
- Spotlight size and warmth live in the one gradient; smaller and brighter reads as a task lamp.
- Change the hasp's throw angle to match your keeper geometry.
- Steel → painted metal: replace the grain layers with a flat colour and a chipped-edge mask.
- The rivets are two radial gradients — move or add them freely.
- A click sound via a tiny
Audioinstance suits this one, if your product allows audio.
Gotchas — read before shipping
- Always mirror toggle state to
aria-pressedon a<button>. A class alone is invisible to assistive tech. - Don't use
aria-pressedand a changing label — pick one, or the announcement contradicts itself. - Pointer-driven lighting must degrade. The static default is declared, not assumed.
- Reading
getBoundingClientRect()per move is fine for one element; cache it for many. - Rotating metal parts need a visible hinge. Without the rivet the motion looks like a bug.
Browser support
| Chrome | Safari | Firefox | Edge |
|---|---|---|---|
| 111+ | 16.4+ | 128+ | 111+ |
Custom properties in gradients, attribute variants and pointer events are all baseline below Tailwind v4's own floor.