Pill Box
import { useId, useRef, useState, type CSSProperties } from 'react';
type Pill = { k: 'cap' | 'tab' | 'oval'; x: number; y: number; r: number };
const D: Pill = { k: 'tab', x: 4, y: 24, r: 0 };
const IRON: Pill = { k: 'oval', x: 14, y: 14, r: 30 };
const MAG: Pill = { k: 'cap', x: 9, y: 30, r: -20 };
const FISH: Pill = { k: 'cap', x: 6, y: 10, r: 60 };
const DAYS = [
{ label: 'Mon', name: 'Monday', pills: [D, IRON], body: 'Vitamin D with breakfast and iron after lunch.' },
{ label: 'Tue', name: 'Tuesday', pills: [D, MAG], body: 'Vitamin D with breakfast, magnesium at night.' },
{ label: 'Wed', name: 'Wednesday', pills: [D, IRON, FISH], body: 'Vitamin D, iron, and a fish oil with dinner.' },
{ label: 'Thu', name: 'Thursday', pills: [D, MAG], body: 'Vitamin D with breakfast, magnesium at night.' },
{ label: 'Fri', name: 'Friday', pills: [D, IRON], body: 'Vitamin D and iron, both with lunch.' },
{ label: 'Sat', name: 'Saturday', pills: [D], body: 'Just the vitamin D, any time before noon.' },
{ label: 'Sun', name: 'Sunday', pills: [], body: 'A day off: nothing in the box but air.' },
];
const css = `
.tabs-pill-box { --shade: light-dark(#373a40, #121417); --frost: light-dark(#dbdde1, #7d8087); --white: light-dark(#fbfcfd, #e5e6e9); width: 230px; font-family: system-ui, sans-serif; }
.tabs-pill-box__box { position: relative; display: flex; gap: 4px; margin-top: 48px; padding: 5px 4px 6px; border-radius: 10px; background: 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 10px 16px -10px light-dark(rgba(0, 0, 0, 0.3), rgba(0, 0, 0, 0.7)); }
/* Each day is a well under a frosted lid hinged along its back edge. */
.tabs-pill-box__day { position: relative; flex: 1; height: 50px; padding: 0; border: 0; background: none; perspective: 170px; perspective-origin: 50% 0; cursor: pointer; }
.tabs-pill-box__day:focus-visible { outline: 2px solid light-dark(#50535b, #c6c8cd); outline-offset: 3px; border-radius: 6px; }
.tabs-pill-box__well { position: absolute; inset: 0; overflow: hidden; border-radius: 5px 5px 7px 7px; background: radial-gradient(ellipse at 50% 30%, light-dark(#ebedf0, #232529), light-dark(#f3f4f7, #1a1c1f)); box-shadow: inset 0 3px 6px light-dark(rgba(0, 0, 0, 0.25), rgba(0, 0, 0, 0.6)); }
.tabs-pill-box__pills { position: absolute; inset: 0; filter: blur(1.6px); opacity: 0.45; transition: filter 0.3s, opacity 0.3s; }
.tabs-pill-box__day[aria-selected='true'] .tabs-pill-box__pills { filter: none; opacity: 1; transition-delay: 0.12s; }
.tabs-pill-box__pill { position: absolute; left: var(--x); top: var(--y); rotate: var(--r); }
.tabs-pill-box__pill[data-k='tab'] { width: 9px; height: 9px; border-radius: 50%; background: radial-gradient(circle at 35% 30%, var(--white), color-mix(in oklab, var(--white) 75%, light-dark(#6d7179, #7d8087))); box-shadow: inset 0 0 0 1px color-mix(in oklab, var(--white) 80%, light-dark(#6d7179, #7d8087)); }
.tabs-pill-box__pill[data-k='tab']::after { content: ''; position: absolute; left: 4px; top: 1.5px; bottom: 1.5px; width: 1px; background: color-mix(in oklab, var(--white) 70%, light-dark(#6d7179, #7d8087)); }
.tabs-pill-box__pill[data-k='cap'] { width: 14px; height: 6px; border-radius: 3px; background: linear-gradient(90deg, light-dark(#50535b, #c6c8cd) 50%, var(--white) 50%); box-shadow: inset 0 -1px 0 color-mix(in oklab, var(--shade) 25%, transparent); }
.tabs-pill-box__pill[data-k='oval'] { width: 11px; height: 7px; border-radius: 50%; background: radial-gradient(ellipse at 40% 30%, light-dark(#6d7179, #7d8087), light-dark(#dbdde1, #2e3035)); }
.tabs-pill-box__day[aria-selected='true'] .tabs-pill-box__pill { animation: tabs-pill-box-rattle 0.5s cubic-bezier(0.3, 1.5, 0.5, 1) var(--t) both; }
.tabs-pill-box__lid { position: absolute; inset: -1px; transform-origin: 50% 0; transform-style: preserve-3d; transition: transform 0.3s cubic-bezier(0.55, 0, 0.85, 0.35); }
.tabs-pill-box__day[aria-selected='true'] .tabs-pill-box__lid { transform: rotateX(116deg); transition: transform 0.6s cubic-bezier(0.25, 1.4, 0.45, 1); }
.tabs-pill-box__face, .tabs-pill-box__under { position: absolute; inset: 0; border-radius: 6px 6px 8px 8px; backface-visibility: hidden; }
.tabs-pill-box__face { display: flex; align-items: flex-end; justify-content: center; padding-bottom: 6px; background: linear-gradient(170deg, color-mix(in oklab, var(--frost) 40%, transparent), color-mix(in oklab, var(--frost) 62%, transparent)); box-shadow: inset 0 1px 0 color-mix(in oklab, light-dark(#373a40, #f1f2f3) 30%, transparent), inset 0 0 0 1px color-mix(in oklab, light-dark(#373a40, #f1f2f3) 10%, transparent); color: light-dark(#373a40, #f1f2f3); font-size: 8px; font-weight: 800; letter-spacing: 0.08em; text-transform: uppercase; }
.tabs-pill-box__face::before { content: ''; position: absolute; left: 4px; right: 4px; top: 3px; height: 8px; border-radius: 4px; background: linear-gradient(color-mix(in oklab, light-dark(#373a40, #f1f2f3) 22%, transparent), transparent); }
.tabs-pill-box__under { transform: rotateX(180deg); background: linear-gradient(color-mix(in oklab, var(--frost) 75%, var(--shade)), color-mix(in oklab, var(--frost) 55%, transparent)); box-shadow: inset 0 0 0 1px color-mix(in oklab, light-dark(#373a40, #f1f2f3) 12%, transparent); }
.tabs-pill-box__day:hover:not([aria-selected='true']) .tabs-pill-box__lid { transform: rotateX(14deg); transition-duration: 0.2s; }
.tabs-pill-box__panel { min-height: 3em; margin-top: 12px; color: light-dark(#50535b, #c6c8cd); font-size: 12.5px; line-height: 1.5; }
.tabs-pill-box__panel b { display: block; color: light-dark(#373a40, #f1f2f3); font-size: 11px; font-weight: 700; letter-spacing: 0.08em; text-transform: uppercase; }
@keyframes tabs-pill-box-rattle { 0% { translate: 0 0; } 35% { translate: 1px -4px; } 70% { translate: -1px 0; } 100% { translate: 0 0; } }
@media (prefers-reduced-motion: reduce) {
.tabs-pill-box__lid, .tabs-pill-box__day[aria-selected='true'] .tabs-pill-box__lid, .tabs-pill-box__pills { transition: none; }
.tabs-pill-box__day[aria-selected='true'] .tabs-pill-box__pill { animation: none; }
}
`;
export function Preview() {
const id = useId();
const [sel, setSel] = useState(2);
const tabs = useRef<(HTMLButtonElement | null)[]>([]);
const n = DAYS.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();
setSel(next);
tabs.current[next]?.focus();
};
return (
<>
<style href="kit-tabs-pill-box" precedence="kit">
{css}
</style>
<div className="tabs-pill-box">
<div className="tabs-pill-box__box" role="tablist" aria-label="Days" onKeyDown={onKeyDown}>
{DAYS.map((d, i) => (
<button
key={d.name}
ref={(el) => {
tabs.current[i] = el;
}}
className="tabs-pill-box__day"
type="button"
role="tab"
id={`${id}-tab-${i}`}
aria-label={d.name}
aria-selected={sel === i}
aria-controls={`${id}-panel-${i}`}
tabIndex={sel === i ? 0 : -1}
onClick={() => setSel(i)}
>
<span className="tabs-pill-box__well" aria-hidden="true">
<span className="tabs-pill-box__pills">
{d.pills.map((p, k) => (
<span key={k} className="tabs-pill-box__pill" data-k={p.k} style={{ '--x': `${p.x}px`, '--y': `${p.y}px`, '--r': `${p.r}deg`, '--t': `${0.28 + k * 0.06}s` } as CSSProperties} />
))}
</span>
</span>
<span className="tabs-pill-box__lid" aria-hidden="true">
<span className="tabs-pill-box__face">{d.label}</span>
<span className="tabs-pill-box__under" />
</span>
</button>
))}
</div>
{DAYS.map((d, i) => (
<div key={d.name} className="tabs-pill-box__panel" role="tabpanel" id={`${id}-panel-${i}`} aria-labelledby={`${id}-tab-${i}`} hidden={sel !== i}>
<b>{d.name}</b>
{d.body}
</div>
))}
</div>
</>
);
}Markup where the component goes, CSS in your stylesheet, script before </body>. In Vue, Svelte or Angular, call initTabsPillBox(el) on the mounted element instead.
<div class="tabs-pill-box">
<div class="tabs-pill-box__box" role="tablist" aria-label="Days">
<button class="tabs-pill-box__day" type="button" role="tab" id="tabs-pill-box-tab-0" aria-label="Monday" aria-selected="false" aria-controls="tabs-pill-box-panel-0" tabindex="-1">
<span class="tabs-pill-box__well" aria-hidden="true">
<span class="tabs-pill-box__pills">
<span class="tabs-pill-box__pill" data-k="tab" style="--x:4px;--y:24px;--r:0deg;--t:0.28s"></span>
<span class="tabs-pill-box__pill" data-k="oval" style="--x:14px;--y:14px;--r:30deg;--t:0.34s"></span>
</span>
</span>
<span class="tabs-pill-box__lid" aria-hidden="true">
<span class="tabs-pill-box__face">Mon</span>
<span class="tabs-pill-box__under"></span>
</span>
</button>
<button class="tabs-pill-box__day" type="button" role="tab" id="tabs-pill-box-tab-1" aria-label="Tuesday" aria-selected="false" aria-controls="tabs-pill-box-panel-1" tabindex="-1">
<span class="tabs-pill-box__well" aria-hidden="true">
<span class="tabs-pill-box__pills">
<span class="tabs-pill-box__pill" data-k="tab" style="--x:4px;--y:24px;--r:0deg;--t:0.28s"></span>
<span class="tabs-pill-box__pill" data-k="cap" style="--x:9px;--y:30px;--r:-20deg;--t:0.34s"></span>
</span>
</span>
<span class="tabs-pill-box__lid" aria-hidden="true">
<span class="tabs-pill-box__face">Tue</span>
<span class="tabs-pill-box__under"></span>
</span>
</button>
<button class="tabs-pill-box__day" type="button" role="tab" id="tabs-pill-box-tab-2" aria-label="Wednesday" aria-selected="true" aria-controls="tabs-pill-box-panel-2" tabindex="0">
<span class="tabs-pill-box__well" aria-hidden="true">
<span class="tabs-pill-box__pills">
<span class="tabs-pill-box__pill" data-k="tab" style="--x:4px;--y:24px;--r:0deg;--t:0.28s"></span>
<span class="tabs-pill-box__pill" data-k="oval" style="--x:14px;--y:14px;--r:30deg;--t:0.34s"></span>
<span class="tabs-pill-box__pill" data-k="cap" style="--x:6px;--y:10px;--r:60deg;--t:0.4s"></span>
</span>
</span>
<span class="tabs-pill-box__lid" aria-hidden="true">
<span class="tabs-pill-box__face">Wed</span>
<span class="tabs-pill-box__under"></span>
</span>
</button>
<button class="tabs-pill-box__day" type="button" role="tab" id="tabs-pill-box-tab-3" aria-label="Thursday" aria-selected="false" aria-controls="tabs-pill-box-panel-3" tabindex="-1">
<span class="tabs-pill-box__well" aria-hidden="true">
<span class="tabs-pill-box__pills">
<span class="tabs-pill-box__pill" data-k="tab" style="--x:4px;--y:24px;--r:0deg;--t:0.28s"></span>
<span class="tabs-pill-box__pill" data-k="cap" style="--x:9px;--y:30px;--r:-20deg;--t:0.34s"></span>
</span>
</span>
<span class="tabs-pill-box__lid" aria-hidden="true">
<span class="tabs-pill-box__face">Thu</span>
<span class="tabs-pill-box__under"></span>
</span>
</button>
<button class="tabs-pill-box__day" type="button" role="tab" id="tabs-pill-box-tab-4" aria-label="Friday" aria-selected="false" aria-controls="tabs-pill-box-panel-4" tabindex="-1">
<span class="tabs-pill-box__well" aria-hidden="true">
<span class="tabs-pill-box__pills">
<span class="tabs-pill-box__pill" data-k="tab" style="--x:4px;--y:24px;--r:0deg;--t:0.28s"></span>
<span class="tabs-pill-box__pill" data-k="oval" style="--x:14px;--y:14px;--r:30deg;--t:0.34s"></span>
</span>
</span>
<span class="tabs-pill-box__lid" aria-hidden="true">
<span class="tabs-pill-box__face">Fri</span>
<span class="tabs-pill-box__under"></span>
</span>
</button>
<button class="tabs-pill-box__day" type="button" role="tab" id="tabs-pill-box-tab-5" aria-label="Saturday" aria-selected="false" aria-controls="tabs-pill-box-panel-5" tabindex="-1">
<span class="tabs-pill-box__well" aria-hidden="true">
<span class="tabs-pill-box__pills">
<span class="tabs-pill-box__pill" data-k="tab" style="--x:4px;--y:24px;--r:0deg;--t:0.28s"></span>
</span>
</span>
<span class="tabs-pill-box__lid" aria-hidden="true">
<span class="tabs-pill-box__face">Sat</span>
<span class="tabs-pill-box__under"></span>
</span>
</button>
<button class="tabs-pill-box__day" type="button" role="tab" id="tabs-pill-box-tab-6" aria-label="Sunday" aria-selected="false" aria-controls="tabs-pill-box-panel-6" tabindex="-1">
<span class="tabs-pill-box__well" aria-hidden="true">
<span class="tabs-pill-box__pills"></span>
</span>
<span class="tabs-pill-box__lid" aria-hidden="true">
<span class="tabs-pill-box__face">Sun</span>
<span class="tabs-pill-box__under"></span>
</span>
</button>
</div>
<div class="tabs-pill-box__panel" role="tabpanel" id="tabs-pill-box-panel-0" aria-labelledby="tabs-pill-box-tab-0" hidden="">
<b>Monday</b>
Vitamin D with breakfast and iron after lunch.
</div>
<div class="tabs-pill-box__panel" role="tabpanel" id="tabs-pill-box-panel-1" aria-labelledby="tabs-pill-box-tab-1" hidden="">
<b>Tuesday</b>
Vitamin D with breakfast, magnesium at night.
</div>
<div class="tabs-pill-box__panel" role="tabpanel" id="tabs-pill-box-panel-2" aria-labelledby="tabs-pill-box-tab-2">
<b>Wednesday</b>
Vitamin D, iron, and a fish oil with dinner.
</div>
<div class="tabs-pill-box__panel" role="tabpanel" id="tabs-pill-box-panel-3" aria-labelledby="tabs-pill-box-tab-3" hidden="">
<b>Thursday</b>
Vitamin D with breakfast, magnesium at night.
</div>
<div class="tabs-pill-box__panel" role="tabpanel" id="tabs-pill-box-panel-4" aria-labelledby="tabs-pill-box-tab-4" hidden="">
<b>Friday</b>
Vitamin D and iron, both with lunch.
</div>
<div class="tabs-pill-box__panel" role="tabpanel" id="tabs-pill-box-panel-5" aria-labelledby="tabs-pill-box-tab-5" hidden="">
<b>Saturday</b>
Just the vitamin D, any time before noon.
</div>
<div class="tabs-pill-box__panel" role="tabpanel" id="tabs-pill-box-panel-6" aria-labelledby="tabs-pill-box-tab-6" hidden="">
<b>Sunday</b>
A day off: nothing in the box but air.
</div>
</div>.tabs-pill-box { --shade: light-dark(#373a40, #121417); --frost: light-dark(#dbdde1, #7d8087); --white: light-dark(#fbfcfd, #e5e6e9); width: 230px; font-family: system-ui, sans-serif; }
.tabs-pill-box__box { position: relative; display: flex; gap: 4px; margin-top: 48px; padding: 5px 4px 6px; border-radius: 10px; background: 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 10px 16px -10px light-dark(rgba(0, 0, 0, 0.3), rgba(0, 0, 0, 0.7)); }
/* Each day is a well under a frosted lid hinged along its back edge. */
.tabs-pill-box__day { position: relative; flex: 1; height: 50px; padding: 0; border: 0; background: none; perspective: 170px; perspective-origin: 50% 0; cursor: pointer; }
.tabs-pill-box__day:focus-visible { outline: 2px solid light-dark(#50535b, #c6c8cd); outline-offset: 3px; border-radius: 6px; }
.tabs-pill-box__well { position: absolute; inset: 0; overflow: hidden; border-radius: 5px 5px 7px 7px; background: radial-gradient(ellipse at 50% 30%, light-dark(#ebedf0, #232529), light-dark(#f3f4f7, #1a1c1f)); box-shadow: inset 0 3px 6px light-dark(rgba(0, 0, 0, 0.25), rgba(0, 0, 0, 0.6)); }
.tabs-pill-box__pills { position: absolute; inset: 0; filter: blur(1.6px); opacity: 0.45; transition: filter 0.3s, opacity 0.3s; }
.tabs-pill-box__day[aria-selected='true'] .tabs-pill-box__pills { filter: none; opacity: 1; transition-delay: 0.12s; }
.tabs-pill-box__pill { position: absolute; left: var(--x); top: var(--y); rotate: var(--r); }
.tabs-pill-box__pill[data-k='tab'] { width: 9px; height: 9px; border-radius: 50%; background: radial-gradient(circle at 35% 30%, var(--white), color-mix(in oklab, var(--white) 75%, light-dark(#6d7179, #7d8087))); box-shadow: inset 0 0 0 1px color-mix(in oklab, var(--white) 80%, light-dark(#6d7179, #7d8087)); }
.tabs-pill-box__pill[data-k='tab']::after { content: ''; position: absolute; left: 4px; top: 1.5px; bottom: 1.5px; width: 1px; background: color-mix(in oklab, var(--white) 70%, light-dark(#6d7179, #7d8087)); }
.tabs-pill-box__pill[data-k='cap'] { width: 14px; height: 6px; border-radius: 3px; background: linear-gradient(90deg, light-dark(#50535b, #c6c8cd) 50%, var(--white) 50%); box-shadow: inset 0 -1px 0 color-mix(in oklab, var(--shade) 25%, transparent); }
.tabs-pill-box__pill[data-k='oval'] { width: 11px; height: 7px; border-radius: 50%; background: radial-gradient(ellipse at 40% 30%, light-dark(#6d7179, #7d8087), light-dark(#dbdde1, #2e3035)); }
.tabs-pill-box__day[aria-selected='true'] .tabs-pill-box__pill { animation: tabs-pill-box-rattle 0.5s cubic-bezier(0.3, 1.5, 0.5, 1) var(--t) both; }
.tabs-pill-box__lid { position: absolute; inset: -1px; transform-origin: 50% 0; transform-style: preserve-3d; transition: transform 0.3s cubic-bezier(0.55, 0, 0.85, 0.35); }
.tabs-pill-box__day[aria-selected='true'] .tabs-pill-box__lid { transform: rotateX(116deg); transition: transform 0.6s cubic-bezier(0.25, 1.4, 0.45, 1); }
.tabs-pill-box__face, .tabs-pill-box__under { position: absolute; inset: 0; border-radius: 6px 6px 8px 8px; backface-visibility: hidden; }
.tabs-pill-box__face { display: flex; align-items: flex-end; justify-content: center; padding-bottom: 6px; background: linear-gradient(170deg, color-mix(in oklab, var(--frost) 40%, transparent), color-mix(in oklab, var(--frost) 62%, transparent)); box-shadow: inset 0 1px 0 color-mix(in oklab, light-dark(#373a40, #f1f2f3) 30%, transparent), inset 0 0 0 1px color-mix(in oklab, light-dark(#373a40, #f1f2f3) 10%, transparent); color: light-dark(#373a40, #f1f2f3); font-size: 8px; font-weight: 800; letter-spacing: 0.08em; text-transform: uppercase; }
.tabs-pill-box__face::before { content: ''; position: absolute; left: 4px; right: 4px; top: 3px; height: 8px; border-radius: 4px; background: linear-gradient(color-mix(in oklab, light-dark(#373a40, #f1f2f3) 22%, transparent), transparent); }
.tabs-pill-box__under { transform: rotateX(180deg); background: linear-gradient(color-mix(in oklab, var(--frost) 75%, var(--shade)), color-mix(in oklab, var(--frost) 55%, transparent)); box-shadow: inset 0 0 0 1px color-mix(in oklab, light-dark(#373a40, #f1f2f3) 12%, transparent); }
.tabs-pill-box__day:hover:not([aria-selected='true']) .tabs-pill-box__lid { transform: rotateX(14deg); transition-duration: 0.2s; }
.tabs-pill-box__panel { min-height: 3em; margin-top: 12px; color: light-dark(#50535b, #c6c8cd); font-size: 12.5px; line-height: 1.5; }
.tabs-pill-box__panel b { display: block; color: light-dark(#373a40, #f1f2f3); font-size: 11px; font-weight: 700; letter-spacing: 0.08em; text-transform: uppercase; }
@keyframes tabs-pill-box-rattle { 0% { translate: 0 0; } 35% { translate: 1px -4px; } 70% { translate: -1px 0; } 100% { translate: 0 0; } }
@media (prefers-reduced-motion: reduce) {
.tabs-pill-box__lid, .tabs-pill-box__day[aria-selected='true'] .tabs-pill-box__lid, .tabs-pill-box__pills { transition: none; }
.tabs-pill-box__day[aria-selected='true'] .tabs-pill-box__pill { animation: none; }
}function initTabsPillBox(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'; });
// The lid, the pills and the rattle all follow aria-selected in the CSS.
function update() {}
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-pill-box').forEach(initTabsPillBox);Goal: Create a "Pill Box" tab.
Visual behavior: A weekly pill organiser: pick a day and its frosted lid snaps open on the hinge, the pills inside give a little rattle, and the day you left shuts.
Technique:
- A real tablist of buttons with roving tabindex and a labelled panel for each tab; everything drawn follows the selection.
- Each lid is hinged along its back edge and turns past upright, with a front and an underside face using backface-visibility, and a curve that overshoots so it flops back against the hinge.
- Pills under a shut lid are blurred and dim, as if seen through frosted plastic. They sharpen as the lid opens, then hop in turn.
- Nudging a shut lid open a few degrees on hover shows it can be lifted. No script beyond the tabs: all of it follows aria-selected.
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. Each day is named in full. The drawing is aria-hidden. Under prefers-reduced-motion the lids open and close at once and the pills stay still.
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 "Pill Box" tab.
Visual behavior: A weekly pill organiser: pick a day and its frosted lid snaps open on the hinge, the pills inside give a little rattle, and the day you left shuts.
Technique:
- A real tablist of buttons with roving tabindex and a labelled panel for each tab; everything drawn follows the selection.
- Each lid is hinged along its back edge and turns past upright, with a front and an underside face using backface-visibility, and a curve that overshoots so it flops back against the hinge.
- Pills under a shut lid are blurred and dim, as if seen through frosted plastic. They sharpen as the lid opens, then hop in turn.
- Nudging a shut lid open a few degrees on hover shows it can be lifted. No script beyond the tabs: all of it follows aria-selected.
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. Each day is named in full. The drawing is aria-hidden. Under prefers-reduced-motion the lids open and close at once and the pills stay still.
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
A weekly pill organiser: pick a day and its frosted lid snaps open on the hinge, the pills inside give a little rattle, and the day you left shuts.
- Dependencies
- None
- Difficulty
- Intermediate
- Triggers
- click, state change, 3d, spring
- Techniques
- 3d transform, perspective, preserve 3d, css transition, keyframes
- 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. Each day is named in full. The drawing is aria-hidden. Under prefers-reduced-motion the lids open and close at once and the pills stay still.