Physics Stack <mv-physics-stack>
항목들이 상자 안에 쌓인 강체인 목록입니다. 카드 모서리를 잡으면(잡은 지점을 축으로 회전) 던질 수 있고, 더미는 중력에 따라 다시 내려앉으며 DOM 순서는 시각적 순서를 따릅니다.
일시 정지 재생
작동 방식
상자 안에 쌓인 강체로 항목을 구현한 목록: 카드 모서리를 잡아(잡은 지점을 축으로 회전) 던질 수 있고, 더미는 중력에 따라 다시 떨어지며, DOM 순서는 화면에 보이는 순서를 따릅니다. 자체 제작 물리 엔진(회전 상자, 마찰, 반동)이며 의존성이 없습니다.
카테고리 마이크로 인터랙션 유형 Web Component (<mv-physics-stack>) 상태 안정 Keywords physics, rigid-body, todo, list, reorder, drag, toss, gravity, tactile
When to use
A short to-do or idea list should feel tactile, with cards users can grab, toss and let fall into place A playful demo or portfolio section should let visitors reorder items by throwing them around
Avoid when
Items must be reordered precisely across columns or statuses → use Kanban instead Items are records with several fields to compare; a physics pile suits a handful of short cards, not dozens → use Table instead Notifications should pile up physically in a corner tray → use Toast Physics instead
설치
node scripts/add.mjs physics-stack --out ./src/marvelous
Marvelous UI MCP 서버를 사용하는 AI 에이전트: install_components({ slugs: ["physics-stack"], target_dir: "<absolute path>/src/marvelous", framework: "react" }).
복사되는 파일(의존성 포함): tokens/tokens.css, core/base.css, core/dom.js, core/element.js, core/motion.js, core/observe.js, core/physics.js, components/physics-stack/physics-stack.js, components/physics-stack/physics-stack.css.
사용법
기본 마크업입니다. 여기서 시작해 속성, data-*, CSS 변수로 커스터마이즈하세요:
<div style="display:grid;gap:.9rem;width:100%;max-width:420px">
<div style="display:flex;align-items:baseline;justify-content:space-between;gap:1rem">
<strong style="font-size:.95rem">My week</strong>
<span id="mv-physics-stack-demo-count" style="font-size:.8rem;color:var(--mv-fg-muted)">5 tasks</span>
</div>
<mv-physics-stack id="mv-physics-stack-demo" label="This week’s tasks" style="--mv-physics-stack-height:26rem">
<div class="mv-psd-card"><span class="mv-psd-dot" style="--c:var(--mv-danger)"></span><span><strong>Review the Carter contract</strong><small>Today · Legal</small></span><button type="button" class="mv-psd-check" aria-label="Complete: Review the Carter contract"></button></div>
<div class="mv-psd-card"><span class="mv-psd-dot" style="--c:var(--mv-warning)"></span><span><strong>Call the bank</strong><small>Tuesday · Finance</small></span><button type="button" class="mv-psd-check" aria-label="Complete: Call the bank"></button></div>
<div class="mv-psd-card"><span class="mv-psd-dot" style="--c:var(--mv-info)"></span><span><strong>Prep the client demo</strong><small>Wednesday · Product</small></span><button type="button" class="mv-psd-check" aria-label="Complete: Prep the client demo"></button></div>
<div class="mv-psd-card"><span class="mv-psd-dot" style="--c:var(--mv-success)"></span><span><strong>Book the train to Boston</strong><small>Thursday · Travel</small></span><button type="button" class="mv-psd-check" aria-label="Complete: Book the train to Boston"></button></div>
<div class="mv-psd-card"><span class="mv-psd-dot" style="--c:var(--mv-accent)"></span><span><strong>Water the plants</strong><small>Friday · Home</small></span><button type="button" class="mv-psd-check" aria-label="Complete: Water the plants"></button></div>
</mv-physics-stack>
<div style="display:flex;gap:.5rem;flex-wrap:wrap">
<button class="mv-button" data-size="sm" id="mv-physics-stack-demo-add">Add a task</button>
<button class="mv-button" data-size="sm" data-variant="outline" id="mv-physics-stack-demo-tidy">Tidy up</button>
<button class="mv-button" data-size="sm" data-variant="ghost" id="mv-physics-stack-demo-shake">Shake</button>
</div>
<p style="margin:0;font-size:.78rem;color:var(--mv-fg-subtle)">Grab a card by a corner and toss it. Keyboard: Alt + ↑/↓.</p>
</div>
<style>
.mv-psd-card { display: flex; align-items: center; gap: .7rem; }
.mv-psd-card > span:nth-child(2) { display: grid; flex: 1; min-width: 0; }
.mv-psd-card strong { font-weight: 600; font-size: .86rem; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.mv-psd-card small { color: var(--mv-fg-muted); font-size: .74rem; }
.mv-psd-dot { width: .55rem; height: .55rem; border-radius: 50%; background: var(--c); flex: none; box-shadow: 0 0 0 3px color-mix(in oklch, var(--c) 20%, transparent); }
.mv-psd-check { flex: none; width: 1.35rem; height: 1.35rem; border-radius: 50%; border: 1.5px solid var(--mv-border-strong); background: transparent; cursor: pointer; display: grid; place-items: center; transition: background var(--mv-duration-fast), border-color var(--mv-duration-fast); }
.mv-psd-check::after { content: ""; width: .5rem; height: .28rem; border: solid var(--mv-fg-on-accent); border-width: 0 0 2px 2px; rotate: -45deg; translate: 0 -1px; opacity: 0; }
.mv-psd-check:hover { border-color: var(--mv-success); }
.mv-psd-check:hover::after { opacity: 1; border-color: var(--mv-success); }
.mv-psd-check[aria-pressed="true"] { background: var(--mv-success); border-color: var(--mv-success); }
.mv-psd-check[aria-pressed="true"]::after { opacity: 1; border-color: var(--mv-fg-on-accent); }
.mv-psd-check:focus-visible { outline: none; box-shadow: var(--mv-focus-ring); }
</style>
<script type="module">
const stack = document.getElementById("mv-physics-stack-demo");
const count = document.getElementById("mv-physics-stack-demo-count");
const ideas = [
["Send invoice #2041", "Monday · Finance", "var(--mv-warning)"],
["Update the schedule", "Tuesday · Team", "var(--mv-info)"],
["Call the plumber back", "Wednesday · Home", "var(--mv-accent)"],
["Approve the mockups", "Thursday · Design", "var(--mv-success)"],
];
let n = 0;
const sync = () => { const k = stack.querySelectorAll(".mv-physics-stack-item:not([data-leaving])").length; count.textContent = k + (k === 1 ? " task" : " tasks"); };
stack.addEventListener("click", (e) => {
const btn = e.target.closest(".mv-psd-check");
if (!btn) return;
btn.setAttribute("aria-pressed", "true");
const card = btn.closest(".mv-physics-stack-item");
setTimeout(() => { stack.toss(card); sync(); }, 180);
});
document.getElementById("mv-physics-stack-demo-add").addEventListener("click", () => {
const [title, meta, color] = ideas[n++ % ideas.length];
const card = document.createElement("div");
card.className = "mv-psd-card";
const dot = document.createElement("span"); dot.className = "mv-psd-dot"; dot.style.setProperty("--c", color);
const text = document.createElement("span");
const strong = document.createElement("strong"); strong.textContent = title;
const small = document.createElement("small"); small.textContent = meta;
text.append(strong, small);
const check = document.createElement("button"); check.type = "button"; check.className = "mv-psd-check"; check.setAttribute("aria-label", "Complete: " + title);
card.append(dot, text, check);
stack.prepend(card);
queueMicrotask(sync);
});
document.getElementById("mv-physics-stack-demo-tidy").addEventListener("click", () => stack.arrange());
document.getElementById("mv-physics-stack-demo-shake").addEventListener("click", () => stack.shake());
stack.addEventListener("mv-toss", () => setTimeout(sync, 0));
</script>
API
Attributes
Name 유형 Default Description labelstring Accessible name of the list. gravitynumber 1Gravity multiplier (1 ≈ 2400 px/s²). frictionnumber 0.6Friction between cards (0 = soap, 1 = rubber). bouncenumber 0.12Bounce on impact (0-1). gapnumber 6Space between cards when the pile is tidied (px).
Properties
Name 유형 Description itemsElement[] List items, in DOM order (= visual order once settled).
Methods
Name Description arrange()Tidies the pile into a straight tower, in DOM order (smooth transition). toss(item)Spins the item out of the box, then removes it from the DOM; the cards above fall into the gap. shake(strength = 1)Gives every card a shake.
Events
Name Description mv-reorderdetail: { order, item }. The visual order changed (toss, Alt + arrows). mv-settledetail: { order }. The pile is at rest (the animation loop stops). mv-tossdetail: { item }. An item was tossed out by toss().
Content structure
Name Description (children)Each direct child becomes a card (mv-physics-stack-item class, role listitem). Adding or removing children is picked up: a new child drops in from the top.
CSS classes
Name Description mv-physics-stack-itemSet on each child; default card style, overridable.
CSS variables
Name Default Description --mv-physics-stack-height22remBox height. --mv-physics-stack-item-widthmin(19rem, 88%)Card width. --mv-physics-stack-bgvar(--mv-bg-subtle)Box background.
Accessibility
role=list / listitem, each card is focusable. ↑/↓ move focus, Alt + ↑/↓ move the card (announced in a polite live region placed right after the list, since a list may only own list items; it is removed with the element). The keyboard hint is a hidden aria-describedby text. The DOM is reordered to match the visual order and focus is preserved. Inner controls (buttons, links, fields) stay clickable and don’t start a grab. Reduced motion: the pile settles instantly, without animation. Without JS: a regular vertical list.