Doorway

No dependencies
Code format
React + Tailwind
import { useEffect, useRef, useState } from 'react';

const css = `
  .btn-doorway { flex: none; --room: light-dark(#42454c, #1a1c1f); --floor: light-dark(#50535b, #232529); --door: light-dark(#5f626a, #2e3035); --jamb: light-dark(#6d7179, #393c41); --bright: light-dark(#fbfcfd, #f1f2f3); position: relative; display: block; width: 256px; height: 212px; padding: 0; overflow: hidden; border: 0; border-radius: 14px; background: var(--room); cursor: pointer; box-shadow: inset 0 0 0 1px color-mix(in oklab, var(--bright) 7%, transparent); }
  .btn-doorway:focus-visible { outline: 2px solid light-dark(#50535b, #c6c8cd); outline-offset: 2px; }
  .btn-doorway__floor { position: absolute; left: 0; right: 0; top: 156px; bottom: 0; background: linear-gradient(to bottom, var(--floor), var(--room)); }
  .btn-doorway__skirting { position: absolute; left: 0; right: 0; top: 150px; height: 6px; background: color-mix(in oklab, var(--jamb) 42%, var(--room)); }
  .btn-doorway__haze { position: absolute; left: 50%; top: 82px; width: 120px; height: 150px; margin: -75px 0 0 -60px; border-radius: 50%; background: radial-gradient(closest-side, color-mix(in oklab, var(--bright) 52%, transparent), transparent); opacity: 0; transition: opacity 0.5s, transform 0.6s; }
  .btn-doorway__frame { position: absolute; left: 88px; top: 26px; width: 80px; height: 130px; box-shadow: 0 0 0 5px var(--jamb), 0 -1px 0 6px color-mix(in oklab, var(--bright) 8%, transparent); background: var(--bright); perspective: 340px; perspective-origin: 50% 66%; }
  .btn-doorway__beyond { position: absolute; inset: 0; background: linear-gradient(to bottom, var(--bright) 62%, color-mix(in oklab, var(--bright) 80%, var(--jamb))); }
  .btn-doorway__guest { position: absolute; left: 50%; bottom: 0; width: 34px; height: 92px; margin-left: -17px; transform: translateY(8px) scale(0.5); transform-origin: 50% 100%; opacity: 0; transition: transform 0.9s cubic-bezier(0.25, 0.8, 0.3, 1) 0.2s, opacity 0.3s 0.2s; }
  .btn-doorway__guest path, .btn-doorway__guest circle { fill: var(--room); }
  .btn-doorway__door { position: absolute; inset: 0; transform-origin: 0 50%; background: var(--door); transition: transform 0.7s cubic-bezier(0.3, 1.2, 0.5, 1); box-shadow: inset 0 0 0 1px color-mix(in oklab, var(--bright) 9%, transparent); }
  .btn-doorway__panel { position: absolute; left: 12px; right: 12px; border-radius: 2px; box-shadow: inset 0 0 0 1.5px color-mix(in oklab, var(--room) 44%, transparent), inset 1px 1px 2px light-dark(rgba(0, 0, 0, 0.08), rgba(0, 0, 0, 0.25)); }
  .btn-doorway__panel--a { top: 12px; height: 44px; }
  .btn-doorway__panel--b { top: 66px; height: 52px; }
  .btn-doorway__knob { position: absolute; right: 7px; top: 62px; width: 7px; height: 7px; border-radius: 50%; background: var(--bright); box-shadow: 0 1px 1px light-dark(rgba(0, 0, 0, 0.15), rgba(0, 0, 0, 0.5)); }
  .btn-doorway__spill { position: absolute; left: 0; right: 0; top: 156px; bottom: 0; background: linear-gradient(to bottom, var(--bright), color-mix(in oklab, var(--bright) 8%, transparent) 94%); clip-path: polygon(92px 0, 164px 0, 168px 7px, 88px 7px); opacity: 0.7; transition: clip-path 0.7s cubic-bezier(0.3, 1.1, 0.5, 1), opacity 0.4s; }
  .btn-doorway__shadow { position: absolute; left: 0; right: 0; top: 156px; bottom: 0; background: var(--room); clip-path: polygon(128px 0, 128px 0, 128px 0, 128px 0); opacity: 0; transition: clip-path 0.9s cubic-bezier(0.25, 0.8, 0.3, 1) 0.2s, opacity 0.3s 0.2s; }
  .btn-doorway__mat { position: absolute; left: 50%; bottom: 8px; display: grid; place-items: center; width: 124px; height: 30px; margin-left: -62px; clip-path: polygon(13% 0, 87% 0, 100% 100%, 0 100%); background: color-mix(in oklab, var(--bright) 12%, var(--room)); color: var(--bright); font-size: 12px; font-weight: 700; letter-spacing: 0.2em; text-transform: uppercase; transition: background-color 0.4s; }
  .btn-doorway:hover .btn-doorway__door, .btn-doorway:focus-visible .btn-doorway__door { transform: rotateY(32deg); }
  .btn-doorway:hover .btn-doorway__spill, .btn-doorway:focus-visible .btn-doorway__spill { clip-path: polygon(136px 0, 166px 0, 256px 56px, 150px 56px); opacity: 0.92; }
  .btn-doorway:hover .btn-doorway__haze, .btn-doorway:focus-visible .btn-doorway__haze { opacity: 0.6; }
  .btn-doorway[data-open='true'] .btn-doorway__door { transform: rotateY(104deg); }
  .btn-doorway[data-open='true'] .btn-doorway__spill { clip-path: polygon(88px 0, 168px 0, 262px 56px, -6px 56px); opacity: 1; }
  .btn-doorway[data-open='true'] .btn-doorway__haze { opacity: 1; transform: scale(1.5, 1.2); }
  .btn-doorway[data-open='true'] .btn-doorway__guest { transform: none; opacity: 1; }
  .btn-doorway[data-open='true'] .btn-doorway__shadow { clip-path: polygon(121px 0, 135px 0, 150px 56px, 106px 56px); opacity: 1; }
  .btn-doorway[data-open='true'] .btn-doorway__mat { background: color-mix(in oklab, var(--room) 86%, transparent); }
  @media (prefers-reduced-motion: reduce) {
    .btn-doorway * { transition: none !important; }
  }
`;

export function DoorwayButton() {
  const [open, setOpen] = useState(false);
  const timer = useRef(0);
  useEffect(() => () => window.clearTimeout(timer.current), []);
  const press = () => {
    window.clearTimeout(timer.current);
    setOpen(true);
    timer.current = window.setTimeout(() => setOpen(false), 2800);
  };
  return (
    <>
      <style>
        {css}
      </style>
      <button type="button" className="btn-doorway" aria-label="Enter" data-open={open} onClick={press}>
        <span className="btn-doorway__floor" aria-hidden="true" />
        <span className="btn-doorway__skirting" aria-hidden="true" />
        <span className="btn-doorway__haze" aria-hidden="true" />
        <span className="btn-doorway__frame" aria-hidden="true">
          <span className="btn-doorway__beyond" />
          <svg className="btn-doorway__guest" viewBox="0 0 34 92">
            <circle cx="17" cy="11" r="9" />
            <path d="M5 28 C5 22 11 20 17 20 C23 20 29 22 29 28 L31 58 L26 58 L25 92 L19 92 L18 62 L16 62 L15 92 L9 92 L8 58 L3 58 Z" />
          </svg>
          <span className="btn-doorway__door">
            <span className="btn-doorway__panel btn-doorway__panel--a" />
            <span className="btn-doorway__panel btn-doorway__panel--b" />
            <span className="btn-doorway__knob" />
          </span>
        </span>
        <span className="btn-doorway__spill" aria-hidden="true" />
        <span className="btn-doorway__shadow" aria-hidden="true" />
        <span className="btn-doorway__mat" aria-hidden="true">
          {open ? 'Welcome' : 'Enter'}
        </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-doorway
Source on GitHub

About this component

A dark room with light under the door. Hover and the door falls ajar, spilling a wedge of light across the floor; press and it swings wide, with someone standing in the doorway.

Dependencies
None
Difficulty
Intermediate
Triggers
hover, click, 3d, state change
Techniques
3d transform, perspective, clip path, css transition
Reduced motion
Respected
Theme
Light and dark (follows your page's color-scheme)

A real button named Enter, with the scene aria-hidden. Keyboard focus sets the door ajar just as hover does. Under prefers-reduced-motion everything changes at once.

More buttons

See all
Footlight
Gumball
Honk
Ransom note