Floppy
import { useId, useRef, useState } from 'react';
const DISKS = [
{ label: 'Photos', dir: 'PHOTOS', files: [['BEACH.JPG', '412K'], ['SNOW.JPG', '506K']] },
{ label: 'Letters', dir: 'LETTERS', files: [['MUM.TXT', '3K'], ['LANDLORD.TXT', '2K']] },
{ label: 'Games', dir: 'GAMES', files: [['TETRA.EXE', '188K'], ['CHESS.EXE', '240K']] },
];
const REST_LEFT = [20, 89, 158];
const SLOT_LEFT = 89;
const css = `
.tabs-floppy { --shade: light-dark(#373a40, #121417); --plastic: light-dark(#cbced3, #2e3035); --paper: light-dark(#fbfcfd, #e5e6e9); --ink: light-dark(#373a40, #121417); --steel: light-dark(#6d7179, #a7abb2); width: 230px; }
.tabs-floppy__desk { position: relative; height: 154px; }
/* The drive seen from above: vents, a badge, and the slot along its front edge. */
.tabs-floppy__drive { position: absolute; left: 30px; top: 0; z-index: 2; width: 170px; height: 58px; border-radius: 6px 6px 3px 3px; background: repeating-linear-gradient(90deg, color-mix(in oklab, var(--shade) 40%, transparent) 0 2px, transparent 2px 6px) 12px 10px / 60px 18px no-repeat, linear-gradient(light-dark(#dbdde1, #2e3035), light-dark(#ebedf0, #232529)); box-shadow: inset 0 1px 0 color-mix(in oklab, light-dark(#373a40, #f1f2f3) 14%, transparent), 0 6px 12px -6px light-dark(rgba(0, 0, 0, 0.3), rgba(0, 0, 0, 0.7)); }
.tabs-floppy__badge { position: absolute; right: 12px; top: 10px; color: light-dark(#5f626a, #a7abb2); font: 600 7.5px/1.3 ui-monospace, SFMono-Regular, Menlo, monospace; letter-spacing: 0.08em; text-align: right; }
.tabs-floppy__bezel { position: absolute; left: 0; right: 0; bottom: 0; height: 12px; border-radius: 0 0 3px 3px; background: linear-gradient(light-dark(#cbced3, #393c41), light-dark(#dbdde1, #2e3035)); }
.tabs-floppy__slot { position: absolute; left: 54px; top: 4px; width: 62px; height: 3px; border-radius: 2px; background: var(--shade); }
.tabs-floppy__led { position: absolute; left: 126px; top: 4px; width: 7px; height: 3px; border-radius: 1px; background: light-dark(#ebedf0, #232529); }
.tabs-floppy[data-run='a'] .tabs-floppy__led { animation: tabs-floppy-read-a 1.1s steps(1) 1.05s; }
.tabs-floppy[data-run='b'] .tabs-floppy__led { animation: tabs-floppy-read-b 1.1s steps(1) 1.05s; }
.tabs-floppy__eject { position: absolute; left: 140px; top: 3px; width: 18px; height: 5px; border-radius: 2px; background: light-dark(#6d7179, #7d8087); box-shadow: 0 1px 0 light-dark(#ebedf0, #232529); }
.tabs-floppy[data-run='a'] .tabs-floppy__eject { animation: tabs-floppy-press-a 0.3s; }
.tabs-floppy[data-run='b'] .tabs-floppy__eject { animation: tabs-floppy-press-b 0.3s; }
.tabs-floppy__spot { position: absolute; top: 96px; width: 52px; height: 56px; box-sizing: border-box; border: 1px dashed light-dark(#dbdde1, #2e3035); border-radius: 4px; }
/* Each disk moves in two steps: picked up and carried under the slot, then pushed in;
or popped out, then carried home and set down. */
.tabs-floppy__track { position: absolute; left: 0; top: 96px; transition: transform 0.45s cubic-bezier(0.45, 0, 0.3, 1) 0.24s; }
.tabs-floppy__track[data-in] { z-index: 1; transition: transform 0.42s cubic-bezier(0.45, 0, 0.3, 1) 0.3s; }
.tabs-floppy__lift { display: block; }
.tabs-floppy__track[data-in][data-carry] .tabs-floppy__lift { animation: tabs-floppy-carry-in 0.5s ease-in-out 0.3s both; }
.tabs-floppy__track[data-out] .tabs-floppy__lift { animation: tabs-floppy-carry-out 0.75s ease-in-out both; }
.tabs-floppy__disk { position: relative; display: block; width: 52px; height: 56px; padding: 0; border: 0; border-radius: 3px; background: linear-gradient(160deg, color-mix(in oklab, var(--plastic) 85%, light-dark(#373a40, #f1f2f3)), var(--plastic) 45%, color-mix(in oklab, var(--plastic) 80%, var(--shade))); clip-path: polygon(0 0, 46px 0, 52px 6px, 52px 56px, 0 56px); cursor: pointer; transition: transform 0.3s cubic-bezier(0.3, 1.5, 0.5, 1); }
.tabs-floppy__track[data-in] .tabs-floppy__disk { transform: translateY(-32px); transition: transform 0.32s cubic-bezier(0.5, 0, 0.35, 1.3) 0.76s; }
.tabs-floppy__disk:hover { filter: brightness(1.08); }
.tabs-floppy__track:not([data-in]) .tabs-floppy__disk:hover { transform: translateY(-3px); }
.tabs-floppy__shutter { position: absolute; left: 13px; top: 0; width: 26px; height: 19px; border-radius: 0 0 2px 2px; background: linear-gradient(90deg, var(--steel), color-mix(in oklab, var(--steel) 60%, light-dark(#373a40, #f1f2f3)) 40%, var(--steel)); }
.tabs-floppy__shutter::after { content: ''; position: absolute; right: 5px; top: 3px; width: 6px; height: 12px; border-radius: 1px; background: color-mix(in oklab, var(--plastic) 70%, var(--shade)); }
.tabs-floppy__arrow { position: absolute; left: 4px; top: 4px; width: 0; height: 0; border: 3px solid transparent; border-bottom: 4px solid color-mix(in oklab, var(--plastic) 60%, light-dark(#373a40, #f1f2f3)); border-top: 0; }
.tabs-floppy__label { position: absolute; left: 5px; right: 5px; bottom: 4px; height: 24px; border-radius: 1px; background: repeating-linear-gradient(transparent 0 7px, color-mix(in oklab, var(--ink) 14%, transparent) 7px 8px) 0 6px / 100% 16px no-repeat, var(--paper); color: var(--ink); font: 13px/24px 'Bradley Hand', 'Segoe Print', 'Chalkboard SE', 'Comic Sans MS', cursive; text-align: center; }
.tabs-floppy__disk:focus-visible { outline: none; }
.tabs-floppy__track:has(.tabs-floppy__disk:focus-visible) { outline: 2px solid light-dark(#50535b, #c6c8cd); outline-offset: 3px; border-radius: 4px; }
.tabs-floppy__track[data-in]:has(.tabs-floppy__disk:focus-visible) { outline: none; }
.tabs-floppy__track[data-in] .tabs-floppy__disk:focus-visible .tabs-floppy__label { outline: 2px solid light-dark(#50535b, #c6c8cd); outline-offset: 2px; }
/* The listing, line by line once the drive has read the disk. */
.tabs-floppy__panel { min-height: 44px; margin-top: 8px; padding: 0 2px; color: light-dark(#50535b, #c6c8cd); font: 10.5px/15px ui-monospace, SFMono-Regular, Menlo, monospace; }
.tabs-floppy__panel span { display: grid; grid-template-columns: 92px 34px; animation: tabs-floppy-line 0.2s steps(1) both; }
.tabs-floppy__panel span:first-child { display: block; color: light-dark(#373a40, #f1f2f3); }
.tabs-floppy__panel b, .tabs-floppy__panel i { font: inherit; }
.tabs-floppy__panel i { text-align: right; }
@keyframes tabs-floppy-read-a { 0%, 30%, 60% { background: light-dark(#373a40, #f1f2f3); box-shadow: 0 0 6px light-dark(#373a40, #f1f2f3); } 15%, 45%, 80% { background: light-dark(#ebedf0, #232529); box-shadow: none; } }
@keyframes tabs-floppy-read-b { 0%, 30%, 60% { background: light-dark(#373a40, #f1f2f3); box-shadow: 0 0 6px light-dark(#373a40, #f1f2f3); } 15%, 45%, 80% { background: light-dark(#ebedf0, #232529); box-shadow: none; } }
@keyframes tabs-floppy-press-a { 40% { transform: translateY(1px); box-shadow: none; } }
@keyframes tabs-floppy-press-b { 40% { transform: translateY(1px); box-shadow: none; } }
@keyframes tabs-floppy-carry-in { 0%, 100% { transform: none; filter: none; } 35%, 80% { transform: scale(1.06); filter: drop-shadow(0 6px 6px light-dark(rgba(0, 0, 0, 0.11), rgba(0, 0, 0, 0.35))); } }
@keyframes tabs-floppy-carry-out { 0%, 100% { transform: none; filter: none; } 30%, 85% { transform: scale(1.06); filter: drop-shadow(0 6px 6px light-dark(rgba(0, 0, 0, 0.11), rgba(0, 0, 0, 0.35))); } }
@keyframes tabs-floppy-line { from { opacity: 0; } to { opacity: 1; } }
@media (prefers-reduced-motion: reduce) {
.tabs-floppy__track, .tabs-floppy__track[data-in], .tabs-floppy__disk, .tabs-floppy__track[data-in] .tabs-floppy__disk { transition: none; }
.tabs-floppy__led, .tabs-floppy__eject, .tabs-floppy__panel span, .tabs-floppy__lift { animation: none !important; }
}
`;
export function Preview() {
const id = useId();
const [{ sel, from, run }, setState] = useState({ sel: 0, from: -1, run: 'a' });
const tabs = useRef<(HTMLButtonElement | null)[]>([]);
const pick = (i: number) => setState((s) => (s.sel === i ? s : { sel: i, from: s.sel, run: s.run === 'a' ? 'b' : 'a' }));
const n = DISKS.length;
const onKeyDown = (e: React.KeyboardEvent) => {
const next = { ArrowRight: (sel + 1) % n, ArrowLeft: (sel + n - 1) % n, Home: 0, End: n - 1 }[e.key];
if (next === undefined) return;
e.preventDefault();
pick(next);
tabs.current[next]?.focus();
};
return (
<>
<style href="kit-tabs-floppy" precedence="kit">
{css}
</style>
<div className="tabs-floppy" data-run={run}>
<div className="tabs-floppy__desk">
<div className="tabs-floppy__drive" aria-hidden="true">
<span className="tabs-floppy__badge">
3.5 IN
<br />
1.44 MB
</span>
<span className="tabs-floppy__bezel">
<span className="tabs-floppy__slot" />
<span className="tabs-floppy__led" />
<span className="tabs-floppy__eject" />
</span>
</div>
{REST_LEFT.map((x) => (
<span key={x} className="tabs-floppy__spot" style={{ left: x }} aria-hidden="true" />
))}
<div role="tablist" aria-label="Disks" onKeyDown={onKeyDown}>
{DISKS.map((d, i) => (
<span
key={d.label}
className="tabs-floppy__track"
data-in={sel === i ? '' : undefined}
data-carry={sel === i && from >= 0 ? '' : undefined}
data-out={from === i && sel !== i ? '' : undefined}
style={{ transform: sel === i ? `translate(${SLOT_LEFT}px, -36px)` : `translateX(${REST_LEFT[i]}px)` }}
>
<span className="tabs-floppy__lift">
<button
ref={(el) => {
tabs.current[i] = el;
}}
className="tabs-floppy__disk"
type="button"
role="tab"
id={`${id}-tab-${i}`}
aria-selected={sel === i}
aria-controls={`${id}-panel-${i}`}
tabIndex={sel === i ? 0 : -1}
onClick={() => pick(i)}
>
<span className="tabs-floppy__shutter" aria-hidden="true" />
<span className="tabs-floppy__arrow" aria-hidden="true" />
<span className="tabs-floppy__label">{d.label}</span>
</button>
</span>
</span>
))}
</div>
</div>
{DISKS.map((d, i) => (
<div key={d.label} className="tabs-floppy__panel" role="tabpanel" id={`${id}-panel-${i}`} aria-labelledby={`${id}-tab-${i}`} hidden={sel !== i}>
<span style={{ animationDelay: '1.15s' }}>{`A:\\${d.dir}`}</span>
{d.files.map(([name, size], k) => (
<span key={name} style={{ animationDelay: `${1.35 + k * 0.18}s` }}>
<b>{name}</b>
<i>{size}</i>
</span>
))}
</div>
))}
</div>
</>
);
}Markup where the component goes, CSS in your stylesheet, script before </body>. In Vue, Svelte or Angular, call initTabsFloppy(el) on the mounted element instead.
<div class="tabs-floppy" data-run="a">
<div class="tabs-floppy__desk">
<div class="tabs-floppy__drive" aria-hidden="true">
<span class="tabs-floppy__badge">
3.5 IN
<br/>
1.44 MB
</span>
<span class="tabs-floppy__bezel">
<span class="tabs-floppy__slot"></span>
<span class="tabs-floppy__led"></span>
<span class="tabs-floppy__eject"></span>
</span>
</div>
<span class="tabs-floppy__spot" style="left:20px" aria-hidden="true"></span>
<span class="tabs-floppy__spot" style="left:89px" aria-hidden="true"></span>
<span class="tabs-floppy__spot" style="left:158px" aria-hidden="true"></span>
<div role="tablist" aria-label="Disks">
<span class="tabs-floppy__track" data-in="" style="transform:translate(89px, -36px)">
<span class="tabs-floppy__lift">
<button class="tabs-floppy__disk" type="button" role="tab" id="tabs-floppy-tab-0" aria-selected="true" aria-controls="tabs-floppy-panel-0" tabindex="0">
<span class="tabs-floppy__shutter" aria-hidden="true"></span>
<span class="tabs-floppy__arrow" aria-hidden="true"></span>
<span class="tabs-floppy__label">Photos</span>
</button>
</span>
</span>
<span class="tabs-floppy__track" style="transform:translateX(89px)">
<span class="tabs-floppy__lift">
<button class="tabs-floppy__disk" type="button" role="tab" id="tabs-floppy-tab-1" aria-selected="false" aria-controls="tabs-floppy-panel-1" tabindex="-1">
<span class="tabs-floppy__shutter" aria-hidden="true"></span>
<span class="tabs-floppy__arrow" aria-hidden="true"></span>
<span class="tabs-floppy__label">Letters</span>
</button>
</span>
</span>
<span class="tabs-floppy__track" style="transform:translateX(158px)">
<span class="tabs-floppy__lift">
<button class="tabs-floppy__disk" type="button" role="tab" id="tabs-floppy-tab-2" aria-selected="false" aria-controls="tabs-floppy-panel-2" tabindex="-1">
<span class="tabs-floppy__shutter" aria-hidden="true"></span>
<span class="tabs-floppy__arrow" aria-hidden="true"></span>
<span class="tabs-floppy__label">Games</span>
</button>
</span>
</span>
</div>
</div>
<div class="tabs-floppy__panel" role="tabpanel" id="tabs-floppy-panel-0" aria-labelledby="tabs-floppy-tab-0">
<span style="animation-delay:1.15s">A:\PHOTOS</span>
<span style="animation-delay:1.35s">
<b>BEACH.JPG</b>
<i>412K</i>
</span>
<span style="animation-delay:1.53s">
<b>SNOW.JPG</b>
<i>506K</i>
</span>
</div>
<div class="tabs-floppy__panel" role="tabpanel" id="tabs-floppy-panel-1" aria-labelledby="tabs-floppy-tab-1" hidden="">
<span style="animation-delay:1.15s">A:\LETTERS</span>
<span style="animation-delay:1.35s">
<b>MUM.TXT</b>
<i>3K</i>
</span>
<span style="animation-delay:1.53s">
<b>LANDLORD.TXT</b>
<i>2K</i>
</span>
</div>
<div class="tabs-floppy__panel" role="tabpanel" id="tabs-floppy-panel-2" aria-labelledby="tabs-floppy-tab-2" hidden="">
<span style="animation-delay:1.15s">A:\GAMES</span>
<span style="animation-delay:1.35s">
<b>TETRA.EXE</b>
<i>188K</i>
</span>
<span style="animation-delay:1.53s">
<b>CHESS.EXE</b>
<i>240K</i>
</span>
</div>
</div>.tabs-floppy { --shade: light-dark(#373a40, #121417); --plastic: light-dark(#cbced3, #2e3035); --paper: light-dark(#fbfcfd, #e5e6e9); --ink: light-dark(#373a40, #121417); --steel: light-dark(#6d7179, #a7abb2); width: 230px; }
.tabs-floppy__desk { position: relative; height: 154px; }
/* The drive seen from above: vents, a badge, and the slot along its front edge. */
.tabs-floppy__drive { position: absolute; left: 30px; top: 0; z-index: 2; width: 170px; height: 58px; border-radius: 6px 6px 3px 3px; background: repeating-linear-gradient(90deg, color-mix(in oklab, var(--shade) 40%, transparent) 0 2px, transparent 2px 6px) 12px 10px / 60px 18px no-repeat, linear-gradient(light-dark(#dbdde1, #2e3035), light-dark(#ebedf0, #232529)); box-shadow: inset 0 1px 0 color-mix(in oklab, light-dark(#373a40, #f1f2f3) 14%, transparent), 0 6px 12px -6px light-dark(rgba(0, 0, 0, 0.3), rgba(0, 0, 0, 0.7)); }
.tabs-floppy__badge { position: absolute; right: 12px; top: 10px; color: light-dark(#5f626a, #a7abb2); font: 600 7.5px/1.3 ui-monospace, SFMono-Regular, Menlo, monospace; letter-spacing: 0.08em; text-align: right; }
.tabs-floppy__bezel { position: absolute; left: 0; right: 0; bottom: 0; height: 12px; border-radius: 0 0 3px 3px; background: linear-gradient(light-dark(#cbced3, #393c41), light-dark(#dbdde1, #2e3035)); }
.tabs-floppy__slot { position: absolute; left: 54px; top: 4px; width: 62px; height: 3px; border-radius: 2px; background: var(--shade); }
.tabs-floppy__led { position: absolute; left: 126px; top: 4px; width: 7px; height: 3px; border-radius: 1px; background: light-dark(#ebedf0, #232529); }
.tabs-floppy[data-run='a'] .tabs-floppy__led { animation: tabs-floppy-read-a 1.1s steps(1) 1.05s; }
.tabs-floppy[data-run='b'] .tabs-floppy__led { animation: tabs-floppy-read-b 1.1s steps(1) 1.05s; }
.tabs-floppy__eject { position: absolute; left: 140px; top: 3px; width: 18px; height: 5px; border-radius: 2px; background: light-dark(#6d7179, #7d8087); box-shadow: 0 1px 0 light-dark(#ebedf0, #232529); }
.tabs-floppy[data-run='a'] .tabs-floppy__eject { animation: tabs-floppy-press-a 0.3s; }
.tabs-floppy[data-run='b'] .tabs-floppy__eject { animation: tabs-floppy-press-b 0.3s; }
.tabs-floppy__spot { position: absolute; top: 96px; width: 52px; height: 56px; box-sizing: border-box; border: 1px dashed light-dark(#dbdde1, #2e3035); border-radius: 4px; }
/* Each disk moves in two steps: picked up and carried under the slot, then pushed in;
or popped out, then carried home and set down. */
.tabs-floppy__track { position: absolute; left: 0; top: 96px; transition: transform 0.45s cubic-bezier(0.45, 0, 0.3, 1) 0.24s; }
.tabs-floppy__track[data-in] { z-index: 1; transition: transform 0.42s cubic-bezier(0.45, 0, 0.3, 1) 0.3s; }
.tabs-floppy__lift { display: block; }
.tabs-floppy__track[data-in][data-carry] .tabs-floppy__lift { animation: tabs-floppy-carry-in 0.5s ease-in-out 0.3s both; }
.tabs-floppy__track[data-out] .tabs-floppy__lift { animation: tabs-floppy-carry-out 0.75s ease-in-out both; }
.tabs-floppy__disk { position: relative; display: block; width: 52px; height: 56px; padding: 0; border: 0; border-radius: 3px; background: linear-gradient(160deg, color-mix(in oklab, var(--plastic) 85%, light-dark(#373a40, #f1f2f3)), var(--plastic) 45%, color-mix(in oklab, var(--plastic) 80%, var(--shade))); clip-path: polygon(0 0, 46px 0, 52px 6px, 52px 56px, 0 56px); cursor: pointer; transition: transform 0.3s cubic-bezier(0.3, 1.5, 0.5, 1); }
.tabs-floppy__track[data-in] .tabs-floppy__disk { transform: translateY(-32px); transition: transform 0.32s cubic-bezier(0.5, 0, 0.35, 1.3) 0.76s; }
.tabs-floppy__disk:hover { filter: brightness(1.08); }
.tabs-floppy__track:not([data-in]) .tabs-floppy__disk:hover { transform: translateY(-3px); }
.tabs-floppy__shutter { position: absolute; left: 13px; top: 0; width: 26px; height: 19px; border-radius: 0 0 2px 2px; background: linear-gradient(90deg, var(--steel), color-mix(in oklab, var(--steel) 60%, light-dark(#373a40, #f1f2f3)) 40%, var(--steel)); }
.tabs-floppy__shutter::after { content: ''; position: absolute; right: 5px; top: 3px; width: 6px; height: 12px; border-radius: 1px; background: color-mix(in oklab, var(--plastic) 70%, var(--shade)); }
.tabs-floppy__arrow { position: absolute; left: 4px; top: 4px; width: 0; height: 0; border: 3px solid transparent; border-bottom: 4px solid color-mix(in oklab, var(--plastic) 60%, light-dark(#373a40, #f1f2f3)); border-top: 0; }
.tabs-floppy__label { position: absolute; left: 5px; right: 5px; bottom: 4px; height: 24px; border-radius: 1px; background: repeating-linear-gradient(transparent 0 7px, color-mix(in oklab, var(--ink) 14%, transparent) 7px 8px) 0 6px / 100% 16px no-repeat, var(--paper); color: var(--ink); font: 13px/24px 'Bradley Hand', 'Segoe Print', 'Chalkboard SE', 'Comic Sans MS', cursive; text-align: center; }
.tabs-floppy__disk:focus-visible { outline: none; }
.tabs-floppy__track:has(.tabs-floppy__disk:focus-visible) { outline: 2px solid light-dark(#50535b, #c6c8cd); outline-offset: 3px; border-radius: 4px; }
.tabs-floppy__track[data-in]:has(.tabs-floppy__disk:focus-visible) { outline: none; }
.tabs-floppy__track[data-in] .tabs-floppy__disk:focus-visible .tabs-floppy__label { outline: 2px solid light-dark(#50535b, #c6c8cd); outline-offset: 2px; }
/* The listing, line by line once the drive has read the disk. */
.tabs-floppy__panel { min-height: 44px; margin-top: 8px; padding: 0 2px; color: light-dark(#50535b, #c6c8cd); font: 10.5px/15px ui-monospace, SFMono-Regular, Menlo, monospace; }
.tabs-floppy__panel span { display: grid; grid-template-columns: 92px 34px; animation: tabs-floppy-line 0.2s steps(1) both; }
.tabs-floppy__panel span:first-child { display: block; color: light-dark(#373a40, #f1f2f3); }
.tabs-floppy__panel b, .tabs-floppy__panel i { font: inherit; }
.tabs-floppy__panel i { text-align: right; }
@keyframes tabs-floppy-read-a { 0%, 30%, 60% { background: light-dark(#373a40, #f1f2f3); box-shadow: 0 0 6px light-dark(#373a40, #f1f2f3); } 15%, 45%, 80% { background: light-dark(#ebedf0, #232529); box-shadow: none; } }
@keyframes tabs-floppy-read-b { 0%, 30%, 60% { background: light-dark(#373a40, #f1f2f3); box-shadow: 0 0 6px light-dark(#373a40, #f1f2f3); } 15%, 45%, 80% { background: light-dark(#ebedf0, #232529); box-shadow: none; } }
@keyframes tabs-floppy-press-a { 40% { transform: translateY(1px); box-shadow: none; } }
@keyframes tabs-floppy-press-b { 40% { transform: translateY(1px); box-shadow: none; } }
@keyframes tabs-floppy-carry-in { 0%, 100% { transform: none; filter: none; } 35%, 80% { transform: scale(1.06); filter: drop-shadow(0 6px 6px light-dark(rgba(0, 0, 0, 0.11), rgba(0, 0, 0, 0.35))); } }
@keyframes tabs-floppy-carry-out { 0%, 100% { transform: none; filter: none; } 30%, 85% { transform: scale(1.06); filter: drop-shadow(0 6px 6px light-dark(rgba(0, 0, 0, 0.11), rgba(0, 0, 0, 0.35))); } }
@keyframes tabs-floppy-line { from { opacity: 0; } to { opacity: 1; } }
@media (prefers-reduced-motion: reduce) {
.tabs-floppy__track, .tabs-floppy__track[data-in], .tabs-floppy__disk, .tabs-floppy__track[data-in] .tabs-floppy__disk { transition: none; }
.tabs-floppy__led, .tabs-floppy__eject, .tabs-floppy__panel span, .tabs-floppy__lift { animation: none !important; }
}function initTabsFloppy(root) {
var list = root.querySelector('[role="tablist"]');
var tabs = Array.prototype.slice.call(list.querySelectorAll('[role="tab"]'));
var panels = Array.prototype.slice.call(root.querySelectorAll('[role="tabpanel"]'));
var n = tabs.length;
var sel = tabs.findIndex(function (tab) { return tab.getAttribute('aria-selected') === 'true'; });
var tracks = root.querySelectorAll('.tabs-floppy__track');
var REST_LEFT = [20, 89, 158];
var SLOT_LEFT = 89;
var run = 'a';
function update(from) {
run = run === 'a' ? 'b' : 'a';
root.setAttribute('data-run', run); // replays the eject press and the read light
tracks.forEach(function (track, k) {
track.toggleAttribute('data-in', k === sel);
track.toggleAttribute('data-carry', k === sel);
track.toggleAttribute('data-out', k === from);
track.style.transform = k === sel ? 'translate(' + SLOT_LEFT + 'px, -36px)' : 'translateX(' + REST_LEFT[k] + 'px)';
});
}
function pick(i) {
if (i === sel) return;
var from = sel;
sel = i;
tabs.forEach(function (tab, k) {
tab.setAttribute('aria-selected', String(k === sel));
tab.tabIndex = k === sel ? 0 : -1;
});
panels.forEach(function (panel, k) { panel.hidden = k !== sel; });
update(from);
}
function onClick(e) {
var i = tabs.indexOf(e.currentTarget);
if (i >= 0) pick(i);
}
function onKey(e) {
var next = { ArrowRight: (sel + 1) % n, ArrowLeft: (sel + n - 1) % n, Home: 0, End: n - 1 }[e.key];
if (next === undefined) return;
e.preventDefault();
pick(next);
tabs[next].focus();
}
tabs.forEach(function (tab) { tab.addEventListener('click', onClick); });
list.addEventListener('keydown', onKey);
return function () {
tabs.forEach(function (tab) { tab.removeEventListener('click', onClick); });
list.removeEventListener('keydown', onKey);
};
}
document.querySelectorAll('.tabs-floppy').forEach(initTabsFloppy);Goal: Create a "Floppy" tab.
Visual behavior: Disks as tabs: the disk in the drive ejects and goes back to its place, the one you pick slides into the slot, the light blinks and its files list out.
Technique:
- A real tablist of buttons with roving tabindex and a labelled panel for each tab; everything drawn follows the selection.
- Each disk moves in two steps on two nested elements: the outer carries it across and the inner pushes it in, with delays swapped so it goes across then in, or out then across.
- A disk is lifted while it is carried (scale and a drop shadow), so it passes over the others rather than through them. The drive sits above the disks, so the disk goes under its cover and leaves the label showing.
- The read light and the eject press replay by switching between two identical animations, and the listing appears line by line once the disk is in.
Accessibility: Real ARIA tabs: arrow keys, Home and End move between tabs, only the selected tab is in the Tab order, and each panel is labelled by its tab. The drawing is aria-hidden. Under prefers-reduced-motion the disks swap places at once and the light and the listing do not animate.
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 "Floppy" tab.
Visual behavior: Disks as tabs: the disk in the drive ejects and goes back to its place, the one you pick slides into the slot, the light blinks and its files list out.
Technique:
- A real tablist of buttons with roving tabindex and a labelled panel for each tab; everything drawn follows the selection.
- Each disk moves in two steps on two nested elements: the outer carries it across and the inner pushes it in, with delays swapped so it goes across then in, or out then across.
- A disk is lifted while it is carried (scale and a drop shadow), so it passes over the others rather than through them. The drive sits above the disks, so the disk goes under its cover and leaves the label showing.
- The read light and the eject press replay by switching between two identical animations, and the listing appears line by line once the disk is in.
Accessibility: Real ARIA tabs: arrow keys, Home and End move between tabs, only the selected tab is in the Tab order, and each panel is labelled by its tab. The drawing is aria-hidden. Under prefers-reduced-motion the disks swap places at once and the light and the listing do not animate.
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
Disks as tabs: the disk in the drive ejects and goes back to its place, the one you pick slides into the slot, the light blinks and its files list out.
- Dependencies
- None
- Difficulty
- Intermediate
- Triggers
- click, state change, delay stagger
- Techniques
- css transition, keyframes, clip path, stagger
- Reduced motion
- Respected
- Theme
- Light and dark (follows your page's color-scheme)
Real ARIA tabs: arrow keys, Home and End move between tabs, only the selected tab is in the Tab order, and each panel is labelled by its tab. The drawing is aria-hidden. Under prefers-reduced-motion the disks swap places at once and the light and the listing do not animate.