ExklusivBeta

Drag Scroll <mv-drag-scroll>

Umschließt jede seitlich überlaufende Tabelle, Kartenleiste, Timeline oder jedes Board, damit Mausnutzer sie sehen und bedienen können: Randverläufe und ein Hinweis „More“ nur an verborgenen Seiten, Greifen und Ziehen mit Schwung, das normale Mausrad seitwärts umgelenkt, optionale Blätter-Buttons. Anders als typische Drag-Scroller bricht ein Klick, eine Textauswahl (kurz stillhalten, dann ziehen) oder ein natives Ziehen von Links und Bildern darin nie, Trackpads und Touch bleiben nativ, und das Mausrad wird der Seite nie gestohlen.

KategorieScrollen
TypWeb Component (<mv-drag-scroll>)
StatusBeta
Keywordsexclusive, light, horizontal-scroll, drag, grab, overflow, table, rail, kanban, edge-fade, momentum

When to use

  • A wide data table overflows on laptops and desktop mouse users must find and reach the hidden columns
  • A row of product, trip or article cards scrolls sideways and should be draggable with optional arrow buttons
  • A kanban board or horizontal timeline is wider than the screen and should pan by dragging its background

Avoid when

  • Items should be browsed one or a few at a time with dots, looping or autoplay → use Carousel instead
  • The region scrolls vertically and mainly needs a thin styled scrollbar → use Scroll Area instead
  • Logos or quotes should move on their own in an endless loop → use Marquee instead

Installation

node scripts/add.mjs drag-scroll --out ./src/marvelous

KI-Agent mit dem MCP-Server von Marvelous UI: install_components({ slugs: ["drag-scroll"], 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, components/drag-scroll/drag-scroll.js, components/drag-scroll/drag-scroll.css.

Verwendung

Schnellstart mit dem kleinsten funktionierenden Markup:

<mv-drag-scroll>
  <table>…wide table…</table>
</mv-drag-scroll>

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

<div id="mv-drag-scroll-demo" style="display:grid;gap:1.5rem;width:100%;max-width:880px;margin-inline:auto">
  <section style="border:1px solid var(--mv-border);border-radius:var(--mv-radius-xl);background:var(--mv-surface);overflow:hidden">
    <h3 style="margin:0;padding:.85rem 1.1rem .6rem;font-size:.9rem">Revenue by region, fiscal 2026</h3>
    <mv-drag-scroll label="Revenue by region, fiscal 2026" style="border-radius:0">
      <table style="width:max-content;min-width:100%;border-collapse:collapse;font-size:.85rem;font-variant-numeric:tabular-nums;white-space:nowrap">
        <thead>
          <tr style="color:var(--mv-fg-muted);text-align:end;border-block:1px solid var(--mv-border);background:var(--mv-bg-subtle)">
            <th scope="col" style="text-align:start;padding:.55rem 1.1rem;font-weight:500">Region</th>
            <th scope="col" style="padding:.55rem 1.1rem;font-weight:500">Jan</th><th scope="col" style="padding:.55rem 1.1rem;font-weight:500">Feb</th><th scope="col" style="padding:.55rem 1.1rem;font-weight:500">Mar</th><th scope="col" style="padding:.55rem 1.1rem;font-weight:500">Apr</th><th scope="col" style="padding:.55rem 1.1rem;font-weight:500">May</th><th scope="col" style="padding:.55rem 1.1rem;font-weight:500">Jun</th><th scope="col" style="padding:.55rem 1.1rem;font-weight:500">Jul</th><th scope="col" style="padding:.55rem 1.1rem;font-weight:500">Aug</th><th scope="col" style="padding:.55rem 1.1rem;font-weight:500">Sep</th><th scope="col" style="padding:.55rem 1.1rem;font-weight:500">Total</th>
          </tr>
        </thead>
        <tbody style="text-align:end">
          <tr style="border-bottom:1px solid var(--mv-border)"><th scope="row" style="text-align:start;padding:.55rem 1.1rem;font-weight:500"><a href="#na" style="color:var(--mv-accent)">North America</a></th><td style="padding:.55rem 1.1rem">$412,300</td><td style="padding:.55rem 1.1rem">$398,750</td><td style="padding:.55rem 1.1rem">$451,020</td><td style="padding:.55rem 1.1rem">$467,480</td><td style="padding:.55rem 1.1rem">$489,110</td><td style="padding:.55rem 1.1rem">$502,640</td><td style="padding:.55rem 1.1rem">$478,900</td><td style="padding:.55rem 1.1rem">$495,315</td><td style="padding:.55rem 1.1rem">$521,860</td><td style="padding:.55rem 1.1rem;font-weight:600">$4,217,375</td></tr>
          <tr style="border-bottom:1px solid var(--mv-border)"><th scope="row" style="text-align:start;padding:.55rem 1.1rem;font-weight:500"><a href="#eu" style="color:var(--mv-accent)">Europe</a></th><td style="padding:.55rem 1.1rem">$286,940</td><td style="padding:.55rem 1.1rem">$274,300</td><td style="padding:.55rem 1.1rem">$301,780</td><td style="padding:.55rem 1.1rem">$318,450</td><td style="padding:.55rem 1.1rem">$322,090</td><td style="padding:.55rem 1.1rem">$309,660</td><td style="padding:.55rem 1.1rem">$265,210</td><td style="padding:.55rem 1.1rem">$251,480</td><td style="padding:.55rem 1.1rem">$334,920</td><td style="padding:.55rem 1.1rem;font-weight:600">$2,664,830</td></tr>
          <tr style="border-bottom:1px solid var(--mv-border)"><th scope="row" style="text-align:start;padding:.55rem 1.1rem;font-weight:500"><a href="#apac" style="color:var(--mv-accent)">Asia Pacific</a></th><td style="padding:.55rem 1.1rem">$198,420</td><td style="padding:.55rem 1.1rem">$231,060</td><td style="padding:.55rem 1.1rem">$244,890</td><td style="padding:.55rem 1.1rem">$239,170</td><td style="padding:.55rem 1.1rem">$257,330</td><td style="padding:.55rem 1.1rem">$268,540</td><td style="padding:.55rem 1.1rem">$281,700</td><td style="padding:.55rem 1.1rem">$290,215</td><td style="padding:.55rem 1.1rem">$302,480</td><td style="padding:.55rem 1.1rem;font-weight:600">$2,313,805</td></tr>
          <tr><th scope="row" style="text-align:start;padding:.55rem 1.1rem;font-weight:500"><a href="#latam" style="color:var(--mv-accent)">Latin America</a></th><td style="padding:.55rem 1.1rem">$74,810</td><td style="padding:.55rem 1.1rem">$79,230</td><td style="padding:.55rem 1.1rem">$81,650</td><td style="padding:.55rem 1.1rem">$88,940</td><td style="padding:.55rem 1.1rem">$92,370</td><td style="padding:.55rem 1.1rem">$95,020</td><td style="padding:.55rem 1.1rem">$97,860</td><td style="padding:.55rem 1.1rem">$101,445</td><td style="padding:.55rem 1.1rem">$106,190</td><td style="padding:.55rem 1.1rem;font-weight:600">$817,515</td></tr>
        </tbody>
      </table>
    </mv-drag-scroll>
  </section>

  <section>
    <div style="display:flex;justify-content:space-between;align-items:baseline;gap:1rem;margin-bottom:.6rem">
      <h3 style="margin:0;font-size:.95rem">Trips departing in October</h3>
      <output data-demo-status style="font-size:.8rem;color:var(--mv-fg-muted)">Drag the row, or use the arrows</output>
    </div>
    <mv-drag-scroll buttons label="Trips departing in October">
      <ul style="display:flex;gap:.85rem;margin:0;padding:.15rem .15rem .6rem;list-style:none">
        <li><a href="#lisbon" data-trip="Lisbon" style="display:grid;gap:.45rem;width:13rem;color:inherit;text-decoration:none"><span style="display:block;aspect-ratio:4/3;border-radius:var(--mv-radius-lg);background:linear-gradient(135deg,oklch(0.78 0.12 60),oklch(0.62 0.15 25))"></span><span style="font-weight:600;font-size:.9rem">Lisbon, Portugal</span><span style="font-size:.8rem;color:var(--mv-fg-muted)">Oct 4 to Oct 9 · from $1,240</span></a></li>
        <li><a href="#kyoto" data-trip="Kyoto" style="display:grid;gap:.45rem;width:13rem;color:inherit;text-decoration:none"><span style="display:block;aspect-ratio:4/3;border-radius:var(--mv-radius-lg);background:linear-gradient(135deg,oklch(0.8 0.1 350),oklch(0.6 0.14 10))"></span><span style="font-weight:600;font-size:.9rem">Kyoto, Japan</span><span style="font-size:.8rem;color:var(--mv-fg-muted)">Oct 7 to Oct 15 · from $2,890</span></a></li>
        <li><a href="#cape-town" data-trip="Cape Town" style="display:grid;gap:.45rem;width:13rem;color:inherit;text-decoration:none"><span style="display:block;aspect-ratio:4/3;border-radius:var(--mv-radius-lg);background:linear-gradient(135deg,oklch(0.8 0.1 200),oklch(0.55 0.12 240))"></span><span style="font-weight:600;font-size:.9rem">Cape Town, South Africa</span><span style="font-size:.8rem;color:var(--mv-fg-muted)">Oct 10 to Oct 18 · from $2,150</span></a></li>
        <li><a href="#oaxaca" data-trip="Oaxaca" style="display:grid;gap:.45rem;width:13rem;color:inherit;text-decoration:none"><span style="display:block;aspect-ratio:4/3;border-radius:var(--mv-radius-lg);background:linear-gradient(135deg,oklch(0.82 0.13 90),oklch(0.62 0.16 45))"></span><span style="font-weight:600;font-size:.9rem">Oaxaca, Mexico</span><span style="font-size:.8rem;color:var(--mv-fg-muted)">Oct 12 to Oct 17 · from $980</span></a></li>
        <li><a href="#reykjavik" data-trip="Reykjavík" style="display:grid;gap:.45rem;width:13rem;color:inherit;text-decoration:none"><span style="display:block;aspect-ratio:4/3;border-radius:var(--mv-radius-lg);background:linear-gradient(135deg,oklch(0.8 0.08 180),oklch(0.5 0.1 270))"></span><span style="font-weight:600;font-size:.9rem">Reykjavík, Iceland</span><span style="font-size:.8rem;color:var(--mv-fg-muted)">Oct 19 to Oct 24 · from $1,760</span></a></li>
        <li><a href="#marrakesh" data-trip="Marrakesh" style="display:grid;gap:.45rem;width:13rem;color:inherit;text-decoration:none"><span style="display:block;aspect-ratio:4/3;border-radius:var(--mv-radius-lg);background:linear-gradient(135deg,oklch(0.78 0.12 40),oklch(0.55 0.13 15))"></span><span style="font-weight:600;font-size:.9rem">Marrakesh, Morocco</span><span style="font-size:.8rem;color:var(--mv-fg-muted)">Oct 23 to Oct 28 · from $1,090</span></a></li>
        <li><a href="#queenstown" data-trip="Queenstown" style="display:grid;gap:.45rem;width:13rem;color:inherit;text-decoration:none"><span style="display:block;aspect-ratio:4/3;border-radius:var(--mv-radius-lg);background:linear-gradient(135deg,oklch(0.8 0.1 150),oklch(0.52 0.11 190))"></span><span style="font-weight:600;font-size:.9rem">Queenstown, New Zealand</span><span style="font-size:.8rem;color:var(--mv-fg-muted)">Oct 26 to Nov 5 · from $3,420</span></a></li>
      </ul>
    </mv-drag-scroll>
  </section>
</div>
<script type="module">
  const root = document.getElementById("mv-drag-scroll-demo");
  const status = root.querySelector("[data-demo-status]");
  root.addEventListener("click", (e) => {
    const trip = e.target.closest("[data-trip], a[href^='#']");
    if (!trip) return;
    e.preventDefault();
    status.textContent = `Opened ${trip.dataset.trip ?? trip.textContent}`;
  });
</script>

API

Attributes

NameTypDefaultDescription
buttonsbooleanRound previous / next buttons on the hidden sides, each scrolling one visible width (minus the fades).
labelstringtable caption, else “Scrollable content”Accessible name of the scroll region (a <caption> inside is used automatically).
no-hintbooleanKeeps the fades but removes the “More” hint chips.
no-wheelbooleanLeaves the vertical mouse wheel alone (never turned sideways).
no-dragbooleanDisables grab-drag (e.g. when every item is itself draggable).
data-overflow"start" | "end" | "start end" | ""Set by the component while the content overflows: the sides that currently hide something (absent when nothing overflows).
data-scrolledbooleanSet once the reader has scrolled (the hint drops its word).
data-draggingbooleanSet during a grab-drag.

Properties

NameTypDescription
strings{ label, more, previous, next }Default texts, for translation: region name, hint word, button names (“Scroll back”, “Scroll forward”). Assignable before the element is defined.
scrollerHTMLElementThe scrolling element (read-only).
overflow{ start, end }Which sides currently hide content (read-only).

Methods

NameDescription
page(direction)Scrolls one visible width toward the end (1) or the start (-1), smoothly unless motion is reduced.

Events

NameDescription
mv-overflowWhen the hidden sides change. detail = { start, end }.

Content structure

NameDescription
(default)The wide content. It is wrapped in .mv-drag-scroll-viewport; a single child that already scrolls sideways (overflow-x: auto) is used as is, or provide .mv-drag-scroll-viewport yourself (frameworks that own their children).

CSS classes

NameDescription
mv-drag-scroll-viewportThe scrolling box (generated, detected or yours).
mv-drag-scroll-hint / -buttonGenerated hint chips (decorative) or paging buttons, with data-side="start|end".

CSS variables

NameDefaultDescription
--mv-drag-scroll-fade3remWidth of the edge fades.
--mv-drag-scroll-hint-top50%Vertical position of the hint chips and buttons.
--mv-drag-scroll-radiusvar(--mv-radius-md)Corner radius of the region and its focus ring.

Accessibility

While the content overflows, the scrolling box becomes a focusable region (tabindex=0, role=region) named by the label attribute, a table <caption> or “Scrollable content”, so keyboard users scroll it with the arrow keys and screen readers announce what it is; when nothing overflows it is a plain box with no extra tab stop. The fades and hint chips are decorative (aria-hidden, pointer-events: none). The optional buttons are real named <button>s with aria-controls, kept out of the Tab order because the region itself scrolls with the keyboard; if one hides while focused, focus moves to the region. Grab-drag is mouse-only, starts after 6 px, and only the click that ends a real drag is cancelled; a press held still for a moment keeps native text selection and link or image drag; form fields, contenteditable, draggable="true" and [data-no-drag] never start a drag, and double-click or Shift selection is untouched. Touch, pen and trackpad scrolling are never intercepted. The wheel is turned sideways only for a real mouse wheel, after the pointer moved over the region (not when the page scrolls it under a still cursor), when nothing under the pointer scrolls vertically, and it goes back to the page at either end. Standard grab / grabbing cursors, only under (hover: hover) and (pointer: fine). Reduced motion (system or data-motion="reduce"): no momentum, no glide, no nudge. Forced colors: no mask, bordered chips, Highlight focus. Right-to-left layouts supported. Without JavaScript the content still scrolls.

Diese Seite wurde mit KI übersetzt. Übersetzungsfehler melden