Airlock
import { useEffect, useRef, useState } from 'react';
const css = `
.btn-airlock {
position: relative;
width: 190px;
height: 54px;
padding: 0;
border: 1px solid light-dark(#cbced3, #393c41);
border-radius: 12px;
background: light-dark(#fbfcfd, #121417);
font-family: inherit;
cursor: pointer;
overflow: hidden;
}
.btn-airlock:focus-visible { outline: 2px solid light-dark(#42454c, #e5e6e9); outline-offset: 3px; }
.btn-airlock__inside {
position: absolute;
inset: 0;
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
color: light-dark(#373a40, #f1f2f3);
font-size: 13px;
font-weight: 600;
letter-spacing: 0.08em;
text-transform: uppercase;
background: radial-gradient(ellipse at center, color-mix(in oklab, light-dark(#373a40, #f1f2f3) 22%, transparent), transparent 70%);
}
.btn-airlock__led {
width: 7px;
height: 7px;
border-radius: 50%;
background: light-dark(#373a40, #f1f2f3);
box-shadow: 0 0 10px light-dark(#373a40, #f1f2f3);
}
.btn-airlock__door {
position: absolute;
inset: 0;
display: flex;
align-items: center;
background: light-dark(#ebedf0, #232529);
color: light-dark(#42454c, #e5e6e9);
font-size: 12px;
font-weight: 600;
letter-spacing: 0.22em;
text-transform: uppercase;
transition: transform 0.55s cubic-bezier(0.65, 0, 0.35, 1);
}
.btn-airlock__door--l { clip-path: polygon(0 0, 60% 0, 46% 100%, 0 100%); padding-left: 18px; }
.btn-airlock__door--r { clip-path: polygon(60% 0, 100% 0, 100% 100%, 46% 100%); justify-content: flex-end; padding-right: 16px; }
/* A hazard band runs down each door's leading edge. */
.btn-airlock__door::before {
content: '';
position: absolute;
inset: 0;
background: repeating-linear-gradient(-45deg, light-dark(#6d7179, #7d8087) 0 3px, transparent 3px 7px);
}
.btn-airlock__door--l::before { clip-path: polygon(53% 0, 60% 0, 46% 100%, 39% 100%); }
.btn-airlock__door--r::before { clip-path: polygon(60% 0, 67% 0, 53% 100%, 46% 100%); }
.btn-airlock__lock { width: 14px; height: 14px; color: light-dark(#5f626a, #a7abb2); }
.btn-airlock:hover:not([data-open]) .btn-airlock__door--l,
.btn-airlock:focus-visible:not([data-open]) .btn-airlock__door--l { transform: translateX(-5px); transition-duration: 0.3s; }
.btn-airlock:hover:not([data-open]) .btn-airlock__door--r,
.btn-airlock:focus-visible:not([data-open]) .btn-airlock__door--r { transform: translateX(5px); transition-duration: 0.3s; }
.btn-airlock[data-open] .btn-airlock__door--l { transform: translateX(-62%); }
.btn-airlock[data-open] .btn-airlock__door--r { transform: translateX(62%); }
@media (prefers-reduced-motion: reduce) {
.btn-airlock__door { transition: opacity 0.2s ease; }
.btn-airlock[data-open] .btn-airlock__door { transform: none; opacity: 0; }
}
`;
export function AirlockButton() {
const [open, setOpen] = useState(false);
const timer = useRef(0);
useEffect(() => () => clearTimeout(timer.current), []);
return (
<>
<style>
{css}
</style>
<button
type="button"
className="btn-airlock"
data-open={open || undefined}
aria-label={open ? 'Access granted' : 'Request access'}
onClick={() => {
setOpen(true);
clearTimeout(timer.current);
timer.current = window.setTimeout(() => setOpen(false), 2200);
}}
>
<span className="btn-airlock__inside" aria-hidden="true">
<span className="btn-airlock__led" />
Granted
</span>
<span className="btn-airlock__door btn-airlock__door--l" aria-hidden="true">
Access
</span>
<span className="btn-airlock__door btn-airlock__door--r" aria-hidden="true">
<svg className="btn-airlock__lock" viewBox="0 0 24 24" fill="none">
<rect x="5" y="11" width="14" height="9" rx="2" stroke="currentColor" strokeWidth="1.8" />
<path d="M8 11V8a4 4 0 0 1 8 0v3" stroke="currentColor" strokeWidth="1.8" />
</svg>
</span>
</button>
</>
);
}Markup where the component goes, CSS in your stylesheet, script before </body>. In Vue, Svelte or Angular, call initBtnAirlock(el) on the mounted element instead.
<button type="button" class="btn-airlock" aria-label="Request access">
<span class="btn-airlock__inside" aria-hidden="true"><span class="btn-airlock__led"></span>Granted</span>
<span class="btn-airlock__door btn-airlock__door--l" aria-hidden="true">Access</span>
<span class="btn-airlock__door btn-airlock__door--r" aria-hidden="true">
<svg class="btn-airlock__lock" viewBox="0 0 24 24" fill="none">
<rect x="5" y="11" width="14" height="9" rx="2" stroke="currentColor" stroke-width="1.8"></rect>
<path d="M8 11V8a4 4 0 0 1 8 0v3" stroke="currentColor" stroke-width="1.8"></path>
</svg>
</span>
</button>.btn-airlock {
position: relative;
width: 190px;
height: 54px;
padding: 0;
border: 1px solid light-dark(#cbced3, #393c41);
border-radius: 12px;
background: light-dark(#fbfcfd, #121417);
font-family: inherit;
cursor: pointer;
overflow: hidden;
}
.btn-airlock:focus-visible { outline: 2px solid light-dark(#42454c, #e5e6e9); outline-offset: 3px; }
.btn-airlock__inside {
position: absolute;
inset: 0;
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
color: light-dark(#373a40, #f1f2f3);
font-size: 13px;
font-weight: 600;
letter-spacing: 0.08em;
text-transform: uppercase;
background: radial-gradient(ellipse at center, color-mix(in oklab, light-dark(#373a40, #f1f2f3) 22%, transparent), transparent 70%);
}
.btn-airlock__led {
width: 7px;
height: 7px;
border-radius: 50%;
background: light-dark(#373a40, #f1f2f3);
box-shadow: 0 0 10px light-dark(#373a40, #f1f2f3);
}
.btn-airlock__door {
position: absolute;
inset: 0;
display: flex;
align-items: center;
background: light-dark(#ebedf0, #232529);
color: light-dark(#42454c, #e5e6e9);
font-size: 12px;
font-weight: 600;
letter-spacing: 0.22em;
text-transform: uppercase;
transition: transform 0.55s cubic-bezier(0.65, 0, 0.35, 1);
}
.btn-airlock__door--l { clip-path: polygon(0 0, 60% 0, 46% 100%, 0 100%); padding-left: 18px; }
.btn-airlock__door--r { clip-path: polygon(60% 0, 100% 0, 100% 100%, 46% 100%); justify-content: flex-end; padding-right: 16px; }
/* A hazard band runs down each door's leading edge. */
.btn-airlock__door::before {
content: '';
position: absolute;
inset: 0;
background: repeating-linear-gradient(-45deg, light-dark(#6d7179, #7d8087) 0 3px, transparent 3px 7px);
}
.btn-airlock__door--l::before { clip-path: polygon(53% 0, 60% 0, 46% 100%, 39% 100%); }
.btn-airlock__door--r::before { clip-path: polygon(60% 0, 67% 0, 53% 100%, 46% 100%); }
.btn-airlock__lock { width: 14px; height: 14px; color: light-dark(#5f626a, #a7abb2); }
.btn-airlock:hover:not([data-open]) .btn-airlock__door--l,
.btn-airlock:focus-visible:not([data-open]) .btn-airlock__door--l { transform: translateX(-5px); transition-duration: 0.3s; }
.btn-airlock:hover:not([data-open]) .btn-airlock__door--r,
.btn-airlock:focus-visible:not([data-open]) .btn-airlock__door--r { transform: translateX(5px); transition-duration: 0.3s; }
.btn-airlock[data-open] .btn-airlock__door--l { transform: translateX(-62%); }
.btn-airlock[data-open] .btn-airlock__door--r { transform: translateX(62%); }
@media (prefers-reduced-motion: reduce) {
.btn-airlock__door { transition: opacity 0.2s ease; }
.btn-airlock[data-open] .btn-airlock__door { transform: none; opacity: 0; }
}function initBtnAirlock(root) {
var timer = 0;
function set(open) {
root.toggleAttribute('data-open', open);
root.setAttribute('aria-label', open ? 'Access granted' : 'Request access');
}
function click() {
set(true);
clearTimeout(timer);
timer = setTimeout(function () { set(false); }, 2200);
}
root.addEventListener('click', click);
return function () {
clearTimeout(timer);
root.removeEventListener('click', click);
};
}
document.querySelectorAll('.btn-airlock').forEach(initBtnAirlock);Goal: Create a "Airlock" button.
Visual behavior: Two hazard-striped doors seal the button. Hover cracks them apart a few pixels; a click slides them open on an angled seam to show a lit ACCESS GRANTED inside, then they close again.
Technique:
- Both doors cover the whole button and are cut by clip-path polygons along one diagonal seam, so they meet without a gap.
- A ::before on each door draws a hazard band (a repeating -45deg stripe) clipped to a thin strip along its leading edge.
- Hover nudges the doors 5px apart; a click sets data-open, which translates them 62% outward on an ease-in-out curve, revealing a radial glow and the granted label.
- A timeout clears data-open after 2.2s, and the aria-label follows the state.
Accessibility: A real <button> whose aria-label follows the state (Request access / Access granted); the doors are aria-hidden. Focus outline. Under prefers-reduced-motion the doors fade instead of sliding.
My stack: {{USER_STACK}}
My styling: {{USER_STYLING}}
My constraints: {{USER_CONSTRAINTS}}
My colors: neutral only. Black, white and grays, no hues. Use the color values above as written, or my project's closest neutral tokens.
Return a single self-contained component. No dependencies.Goal: Create a "Airlock" button.
Visual behavior: Two hazard-striped doors seal the button. Hover cracks them apart a few pixels; a click slides them open on an angled seam to show a lit ACCESS GRANTED inside, then they close again.
Technique:
- Both doors cover the whole button and are cut by clip-path polygons along one diagonal seam, so they meet without a gap.
- A ::before on each door draws a hazard band (a repeating -45deg stripe) clipped to a thin strip along its leading edge.
- Hover nudges the doors 5px apart; a click sets data-open, which translates them 62% outward on an ease-in-out curve, revealing a radial glow and the granted label.
- A timeout clears data-open after 2.2s, and the aria-label follows the state.
Accessibility: A real <button> whose aria-label follows the state (Request access / Access granted); the doors are aria-hidden. Focus outline. Under prefers-reduced-motion the doors fade instead of sliding.
My stack: {{USER_STACK}}
My styling: {{USER_STYLING}}
My constraints: {{USER_CONSTRAINTS}}
My colors: use my project's existing color tokens. The color values above are grayscale placeholders. Map each one by its role (surface, text, border, accent, glow) to my theme, and keep their relative lightness so contrast and the effect still read.
Return a single self-contained component. No dependencies.Paste into Claude, ChatGPT or Cursor after filling in {{USER_STACK}}, {{USER_STYLING}} and {{USER_CONSTRAINTS}}. Neutral keeps the greys you see here; Match my project asks the model to map them onto your own colour tokens.
Free to use in any project, commercial included. MIT-0, no attribution required. Licence
About this component
Two hazard-striped doors seal the button. Hover cracks them apart a few pixels; a click slides them open on an angled seam to show a lit ACCESS GRANTED inside, then they close again.
- Dependencies
- None
- Difficulty
- Intermediate
- Triggers
- click, hover, state change
- Techniques
- clip path, css transition, pseudo elements, radial gradient
- Reduced motion
- Respected
- Theme
- Light and dark (follows your page's color-scheme)
A real <button> whose aria-label follows the state (Request access / Access granted); the doors are aria-hidden. Focus outline. Under prefers-reduced-motion the doors fade instead of sliding.