Exclusifbêta

Drag Scroll <mv-drag-scroll>

Enveloppe n’importe quel tableau, rail de cartes, frise ou kanban qui déborde latéralement pour que les utilisateurs de souris le voient et s’en servent : fondus sur les bords et indice « Plus » uniquement sur les côtés masqués, saisie et glissement avec inertie, molette classique convertie en défilement horizontal, boutons de pagination facultatifs. Contrairement aux drag-scrollers habituels, un clic, une sélection de texte (restez immobile, puis faites glisser) ou un glissement natif de lien ou d’image à l’intérieur ne casse jamais, les trackpads et le tactile restent natifs, et la molette n’est jamais volée à la page.

CatégorieDéfilement
TypeWeb Component (<mv-drag-scroll>)
Statutbêta
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

Agent IA avec le serveur MCP de Marvelous UI : install_components({ slugs: ["drag-scroll"], target_dir: "<absolute path>/src/marvelous", framework: "react" }).

Fichiers copiés (dépendances comprises) : 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.

Utilisation

Démarrage rapide, le balisage minimal qui fonctionne :

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

Balisage de référence : partez de celui-ci et personnalisez-le avec les attributs, data-* et les variables CSS :

<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

NameTypeDefaultDescription
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

NameTypeDescription
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.

Cette page a été traduite par IA. Signaler un problème de traduction