Airlock

No dependencies
Code format
React + Tailwind
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>
    </>
  );
}

Free to use in any project, commercial included. MIT-0, no attribution required. Licence

Install with shadcn

npx shadcn@latest add @movin/button-airlock
Source on GitHub

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.

More buttons

See all
Black Hole
Blueprint
Circuit
Constellation