Beta

Physics Stack <mv-physics-stack>

Eine Liste, deren Elemente starre Körper sind, in einer Box gestapelt: Greif eine Karte an einer Ecke (sie dreht sich um den Griffpunkt), wirf sie, der Stapel fällt unter der Schwerkraft zurück, und die DOM-Reihenfolge folgt der sichtbaren Reihenfolge. Eigene Physik-Engine (orientierte Boxen, Reibung, Abprall), keine Abhängigkeiten.

KategorieMikrointeraktionen
TypWeb Component (<mv-physics-stack>)
StatusBeta
Keywordsphysics, 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

Installation

node scripts/add.mjs physics-stack --out ./src/marvelous

KI-Agent mit dem MCP-Server von Marvelous UI: install_components({ slugs: ["physics-stack"], target_dir: "<absolute path>/src/marvelous", framework: "react" }).

Kopierte Dateien (inklusive Abhängigkeiten): 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.

Verwendung

Referenz-Markup zum Starten und Anpassen mit Attributen, data-* und CSS-Variablen:

<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

NameTypDefaultDescription
labelstringAccessible name of the list.
gravitynumber1Gravity multiplier (1 ≈ 2400 px/s²).
frictionnumber0.6Friction between cards (0 = soap, 1 = rubber).
bouncenumber0.12Bounce on impact (0-1).
gapnumber6Space between cards when the pile is tidied (px).

Properties

NameTypDescription
itemsElement[]List items, in DOM order (= visual order once settled).

Methods

NameDescription
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

NameDescription
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

NameDescription
(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

NameDescription
mv-physics-stack-itemSet on each child; default card style, overridable.

CSS variables

NameDefaultDescription
--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 live region). 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.

Diese Seite wurde mit KI übersetzt. Übersetzungsfehler melden