# Clear Acrylic ID Passes in a Slotted Block That Lift out and Turn, with Neon Edges and a Floating Print

> ID badges cut from clear acrylic stand in a slotted block and the one you pick lifts out and turns to face you. Free React and Tailwind code, or HTML.

A shadcn/ui block from the [@weinc](https://we.inc/components?utm_source=shadcn.exchange) registry, listed on [shadcn.exchange](https://shadcn.exchange/r/weinc/clear-acrylic-id-passes-in-a-slotted-block-that-lift-out-and-turn-with-neon-edges-and-a-floating-print).

- Page, with a live preview: https://shadcn.exchange/r/weinc/clear-acrylic-id-passes-in-a-slotted-block-that-lift-out-and-turn-with-neon-edges-and-a-floating-print
- Screenshot: https://shadcn.exchange/shot/weinc/clear-acrylic-id-passes-in-a-slotted-block-that-lift-out-and-turn-with-neon-edges-and-a-floating-print/light.png
- Registry item: https://we.inc/r/clear-acrylic-id-passes-in-a-slotted-block-that-lift-out-and-turn-with-neon-edges-and-a-floating-print.json
- Listed: 2026-10-28, source last changed 2026-10-28

## Install

```bash
npx shadcn@latest add https://shadcn.exchange/r/weinc/clear-acrylic-id-passes-in-a-slotted-block-that-lift-out-and-turn-with-neon-edges-and-a-floating-print.json
```

Run it in a project that has shadcn/ui set up. It adds the files below and installs what they depend on.

## Dependencies

- npm packages: `framer-motion`

## Files

### `components/we-inc/clear-acrylic-id-passes-in-a-slotted-block-that-lift-out-and-turn-with-neon-edges-and-a-floating-print.tsx`

```tsx
import { useEffect, useRef, useState } from "react";
import { motion, useMotionValue, useReducedMotion, useSpring } from "framer-motion";

// One entry per pass. `edge` is the colour of the fluorescent sheet it is cut from.
const passes = [
  {
    name: "Tobi Adeyemi",
    role: "SPEAKER",
    no: "014",
    edge: "#c4ff3d",
    tone: "#8a2bd6",
    photo: "https://images.pexels.com/photos/9363613/pexels-photo-9363613.jpeg?auto=compress&cs=tinysrgb&w=600",
    alt: "A man with a round afro in a white shirt, lit violet against a bright purple wall",
    at: "50% 18%",
  },
  {
    name: "Mara Vidal",
    role: "CREW",
    no: "102",
    edge: "#4fd8ff",
    tone: "#c40f3c",
    photo: "https://images.pexels.com/photos/21352926/pexels-photo-21352926.jpeg?auto=compress&cs=tinysrgb&w=600",
    alt: "A woman in a green top and black jacket with one arm raised behind her head, against a red backdrop",
    at: "50% 12%",
  },
  {
    name: "Sam Okoro",
    role: "ARTIST",
    no: "057",
    edge: "#ff8a2a",
    tone: "#1f4a37",
    photo: "https://images.pexels.com/photos/7301578/pexels-photo-7301578.jpeg?auto=compress&cs=tinysrgb&w=600",
    alt: "A man seen from behind his shoulder in a blue knitted beanie and jumper, against a dark green wall",
    at: "60% 20%",
  },
];
const event = { name: "OFFCUT", year: "26" };
const BW = 188; // pass width
const BH = 268; // pass height
const THICK = 16; // sheet thickness
const RIM = 12; // clear margin round the print
const TOP = 36; // clear margin above the print, where the strap slot is cut
const GAP = 208; // distance between pass centres
const REST = -22; // angle a pass sits at in its slot, in degrees
const LIFT = 58; // how far the chosen pass rises
const HOLD = 3400; // milliseconds each pass is held up while it plays by itself
const BLOCK = { w: 644, h: 38, d: 96 };
const SCENE_H = BH + LIFT + BLOCK.h + 8;
const PW = BW - RIM * 2, PH = BH - TOP - RIM; // size of the printed photograph

export default function ClearAcrylicIdPasses() {
  const root = useRef<HTMLDivElement>(null);
  const reduce = useReducedMotion();
  const [picked, setPicked] = useState(1);
  const [scale, setScale] = useState(1);
  const touched = useRef(0);
  const mx = useMotionValue(0), my = useMotionValue(0);
  const turn = useSpring(mx, { stiffness: 60, damping: 14 });
  const tip = useSpring(my, { stiffness: 60, damping: 14 });

  useEffect(() => {
    const el = root.current;
    if (!el) return;
    const fit = () => setScale(Math.min(1.36, (el.clientWidth - 32) / BLOCK.w));
    fit();
    const ro = new ResizeObserver(fit); ro.observe(el);
    return () => ro.disconnect();
  }, []);

  // Plays by itself until someone picks a pass, and again a while after they stop.
  useEffect(() => {
    if (reduce) return;
    const id = window.setInterval(() => {
      if (document.hidden || performance.now() - touched.current < 9000) return;
      setPicked((p) => (p + 1) % passes.length);
    }, HOLD);
    return () => window.clearInterval(id);
  }, [reduce]);

  const pick = (i: number) => { touched.current = performance.now(); setPicked(i); };
  const spring = reduce ? { duration: 0 } : { type: "spring" as const, stiffness: 120, damping: 13, mass: 1.1 };
  const layers = Array.from({ length: THICK / 2 - 1 }, (_, i) => -THICK / 2 + 2 + i * 2);

  return (
    <div
      ref={root}
      onPointerMove={(e) => {
        if (reduce) return;
        const r = e.currentTarget.getBoundingClientRect();
        mx.set(((e.clientX - r.left) / r.width - 0.5) * 16); my.set(((e.clientY - r.top) / r.height - 0.5) * -6);
      }}
      onPointerLeave={() => { mx.set(0); my.set(0); }}
      className="relative flex min-h-[680px] w-full items-center justify-center overflow-hidden bg-[#161924] pb-24 pt-16"
    >
      <style>{`
        @keyframes clear-acrylic-id-passes-sway { 0%, 100% { transform: rotateY(-7deg); } 50% { transform: rotateY(7deg); } }
        .clear-acrylic-id-passes-sway { animation: clear-acrylic-id-passes-sway 5.2s ease-in-out infinite; }
        @media (prefers-reduced-motion: reduce) { .clear-acrylic-id-passes-sway { animation: none; } }
      `}</style>
      <div aria-hidden="true" className="pointer-events-none absolute inset-0" style={{ background: "radial-gradient(70% 60% at 50% 40%, #343a50 0%, #1f2331 48%, #161924 100%)" }} />

      {/* Sized with zoom, not a scale transform, so the lettering is drawn sharp at the final size. */}
      <div className="relative" style={{ zoom: scale }}>
        <div className="relative" style={{ width: BLOCK.w, height: SCENE_H, perspective: 1700, perspectiveOrigin: "50% 30%" }}>
          <motion.div className="absolute inset-0" style={{ transformStyle: "preserve-3d", rotateX: -11, rotateY: turn, x: 0, y: tip }}>
            {/* Shadow of the block on the floor. */}
            <div aria-hidden="true" className="absolute rounded-[50%]" style={{ left: -30, top: SCENE_H - 70, width: BLOCK.w + 60, height: 140, background: "radial-gradient(closest-side, rgba(0,0,0,0.7), rgba(0,0,0,0.3) 60%, rgba(0,0,0,0))", transform: "rotateX(90deg)", filter: "blur(6px)" }} />

            {/* The anodised block: a top with three diagonal slots and a brushed front. */}
            <div aria-hidden="true" className="absolute left-0 rounded-[3px]" style={{ top: SCENE_H - BLOCK.h - BLOCK.d / 2, width: BLOCK.w, height: BLOCK.d, transform: "rotateX(90deg)", background: "linear-gradient(180deg, #a9acb4 0%, #c6c8ce 55%, #dcdde2 100%)", boxShadow: "inset 0 0 0 1px rgba(255,255,255,0.3)" }}>
              {passes.map((p, i) => (
                <div key={p.no} className="absolute" style={{ left: BLOCK.w / 2 + (i - 1) * GAP - BW / 2 - 3, top: BLOCK.d / 2 - (THICK + 4) / 2, width: BW + 6, height: THICK + 4, transform: `rotate(${-REST}deg)` }}>
                  <div className="absolute rounded-[50%] bg-black transition-opacity duration-500" style={{ left: 6, right: 6, top: -8, height: THICK + 30, filter: "blur(9px)", opacity: picked === i ? 0.35 : 0 }} />
                  <div className="absolute inset-0 rounded-[3px] bg-[#0b0b0d]" style={{ boxShadow: `inset 0 2px 3px rgba(0,0,0,0.9), 0 1px 0 rgba(255,255,255,0.6), 0 0 ${picked === i ? 10 : 22}px ${picked === i ? 1 : 3}px ${p.edge}${picked === i ? "30" : "66"}` }} />
                </div>
              ))}
            </div>
            <div aria-hidden="true" className="absolute left-0 overflow-hidden rounded-b-[3px]" style={{ top: SCENE_H - BLOCK.h, width: BLOCK.w, height: BLOCK.h, transform: `translateZ(${BLOCK.d / 2}px)`, background: "linear-gradient(180deg, #f4f5f7 0%, #b9bcc3 7%, #8f929a 58%, #5f626a 100%)", boxShadow: "inset 0 1px 0 rgba(255,255,255,0.9)" }}>
              <div className="absolute inset-0 opacity-60" style={{ backgroundImage: "repeating-linear-gradient(90deg, rgba(255,255,255,0.1) 0 1px, rgba(0,0,0,0.06) 1px 3px)" }} />
              {/* Each sheet throws a little of its colour onto the metal below it. */}
              {passes.map((p, i) => (
                <div key={p.no} className="absolute top-0 h-full transition-opacity duration-500" style={{ left: BLOCK.w / 2 + (i - 1) * GAP - BW / 2 + 6, width: BW - 12, background: `linear-gradient(180deg, ${p.edge}aa 0%, ${p.edge}33 45%, ${p.edge}00 100%)`, filter: "blur(7px)", opacity: picked === i ? 0.3 : 0.75 }} />
              ))}
              <p className="absolute right-4 top-1/2 -translate-y-1/2 font-mono text-[8px] font-semibold tracking-[0.34em] text-[#2a2c33]/70">{event.name} / {event.year}</p>
            </div>

            {passes.map((p, i) => {
              const up = picked === i;
              return (
                <motion.div
                  key={p.no}
                  className="absolute"
                  style={{ left: BLOCK.w / 2 + (i - 1) * GAP - BW / 2, top: SCENE_H - BLOCK.h - BH, width: BW, height: BH, transformStyle: "preserve-3d" }}
                  initial={false}
                  animate={{ y: up ? -LIFT : 0, rotateY: up ? 6 : REST }}
                  transition={spring}
                >
                  <div className={`absolute inset-0 ${up ? "clear-acrylic-id-passes-sway" : ""}`} style={{ transformStyle: "preserve-3d" }}>
                    {/* Back of the sheet: the photograph is printed here and seen through the plastic. */}
                    <div className="absolute inset-0 rounded-[16px]" style={{ transform: `translateZ(${-THICK / 2}px)`, border: `2px solid ${p.edge}`, background: "rgba(255,255,255,0.04)" }}>
                      <div className="absolute overflow-hidden rounded-[5px]" style={{ left: RIM - 2, top: TOP - 2, width: PW, height: PH, backgroundColor: p.tone }}>
                        <img src={p.photo} alt={p.alt} width={PW} height={PH} loading="eager" draggable={false} className="h-full w-full select-none object-cover" style={{ objectPosition: p.at }} />
                        <div aria-hidden="true" className="absolute inset-0" style={{ background: "linear-gradient(180deg, rgba(0,0,0,0.3) 0%, rgba(0,0,0,0) 26%, rgba(0,0,0,0) 56%, rgba(0,0,0,0.66) 100%)" }} />
                      </div>
                    </div>
                    {/* Shadow that the white ink on the front throws back onto the print. */}
                    <div aria-hidden="true" className="absolute inset-0 text-black/50" style={{ transform: `translateZ(${-THICK / 2 + 1}px)`, filter: "blur(1.4px)" }}>
                      <Ink p={p} />
                    </div>
                    {/* The body of the sheet: stacked outlines that read as one glowing edge from the side. */}
                    {layers.map((z) => (
                      <div key={z} aria-hidden="true" className="absolute inset-0 rounded-[16px]" style={{ transform: `translateZ(${z}px)`, border: `2.5px solid ${p.edge}`, opacity: 0.62, boxShadow: z === 0 ? `0 0 22px 0 ${p.edge}77` : undefined }} />
                    ))}
                    {/* Light bent at the cut edges: the print mirrored into the clear margin. */}
                    <div aria-hidden="true" className="absolute inset-0 overflow-hidden rounded-[16px]" style={{ transform: "translateZ(2px)", opacity: 0.36 }}>
                      {[
                        { box: { left: 3, top: TOP, width: RIM - 3, height: PH }, inner: { left: RIM - 3 - PW, top: 0 }, flip: "scaleX(-1)" },
                        { box: { left: BW - RIM, top: TOP, width: RIM - 3, height: PH }, inner: { left: 0, top: 0 }, flip: "scaleX(-1)" },
                        { box: { left: RIM, top: BH - RIM, width: PW, height: RIM - 3 }, inner: { left: 0, top: 0 }, flip: "scaleY(-1)" },
                      ].map((r, k) => (
                        <div key={k} className="absolute overflow-hidden" style={{ ...r.box, filter: "blur(0.6px) saturate(1.3) brightness(1.15)" }}>
                          <div className="absolute" style={{ ...r.inner, width: PW, height: PH, backgroundImage: `url(${p.photo})`, backgroundSize: "cover", backgroundPosition: p.at, backgroundColor: p.tone, transform: r.flip }} />
                        </div>
                      ))}
                    </div>
                    {/* Front of the sheet: white ink, the strap slot, bevel and gloss. */}
                    <div className="absolute inset-0 overflow-hidden rounded-[16px] text-white" style={{ transform: `translateZ(${THICK / 2}px)`, border: `1.5px solid ${p.edge}`, boxShadow: `inset 0 0 0 1px rgba(255,255,255,0.55), inset 2px 2px 3px rgba(255,255,255,0.6), inset -3px -4px 7px ${p.edge}aa, inset 0 0 18px ${p.edge}2e`, background: "linear-gradient(180deg, rgba(255,255,255,0.1), rgba(255,255,255,0.02) 30%, rgba(255,255,255,0.05))" }}>
                      <div aria-hidden="true" className="absolute left-1/2 top-[13px] h-[10px] w-[50px] -translate-x-1/2 rounded-full bg-[#1b1e2b]" style={{ boxShadow: `inset 0 2px 2px rgba(0,0,0,0.5), inset 0 -1.5px 0 ${p.edge}, 0 0 0 1.5px ${p.edge}cc, 0 1px 0 2px rgba(255,255,255,0.35)` }} />
                      <Ink p={p} />
                      <div aria-hidden="true" className="absolute inset-0" style={{ background: "linear-gradient(118deg, rgba(255,255,255,0.5) 0%, rgba(255,255,255,0.12) 16%, rgba(255,255,255,0) 30%, rgba(255,255,255,0) 54%, rgba(255,255,255,0.2) 60%, rgba(255,255,255,0.04) 68%, rgba(255,255,255,0) 76%)" }} />
                    </div>
                    <button
                      type="button"
                      onClick={() => pick(i)}
                      aria-pressed={up}
                      aria-label={`${p.name}, ${p.role.toLowerCase()}, pass number ${p.no}. ${up ? "Lifted out." : "Press to lift it out of the block."}`}
                      className="absolute inset-0 cursor-pointer rounded-[16px] outline-none focus-visible:ring-2 focus-visible:ring-white focus-visible:ring-offset-4 focus-visible:ring-offset-[#161924]"
                      style={{ transform: `translateZ(${THICK / 2 + 1}px)` }}
                    />
                  </div>
                </motion.div>
              );
            })}
          </motion.div>
        </div>
      </div>

      <p className="pointer-events-none absolute bottom-7 left-7 font-mono text-[10px] font-semibold uppercase leading-5 tracking-[0.22em] text-zinc-100">
        {event.name} {event.year} passes
        <span className="block font-medium text-zinc-400">Pick a pass to lift it out of the block</span>
      </p>
    </div>
  );
}

// Everything printed in white on the front. Drawn twice: once as ink and once, blurred, as its shadow.
function Ink({ p }: { p: (typeof passes)[number] }) {
  return (
    <div className="absolute" style={{ left: RIM, top: TOP, width: PW, height: PH }}>
      <p className="absolute left-[11px] top-[10px] text-[11px] font-black tracking-[0.26em]">{event.name}</p>
      <p className="absolute right-[11px] top-[10px] font-mono text-[10px] font-bold tracking-[0.1em]">{event.year}</p>
      <p className="absolute bottom-[40px] left-[11px] text-[7.5px] font-bold tracking-[0.3em]">{p.role}</p>
      <p className="absolute bottom-[14px] left-[10px] right-[10px] text-[21px] font-bold leading-[0.95] tracking-[-0.02em]">{p.name}</p>
      <p className="absolute bottom-[42px] right-[11px] font-mono text-[7.5px] font-bold tracking-[0.16em]">No. {p.no}</p>
    </div>
  );
}
```
