Exclusivo

Drag Scroll <mv-drag-scroll>

Envolve qualquer tabela, trilho de cards, linha do tempo ou quadro que transborde na horizontal para que quem usa mouse o veja e o use: esmaecimento nas bordas e uma dica “More” só nos lados ocultos, arrastar para pegar com inércia, a roda comum convertida em rolagem lateral, botões de paginação opcionais.

Como funciona

Envolve qualquer tabela, trilho de cards, linha do tempo ou quadro que transborde na horizontal para que quem usa mouse o veja e o use: fades nas bordas e uma dica “More” só nos lados ocultos, arrastar com inércia, a roda comum convertida em rolagem lateral e botões de paginação opcionais. Ao contrário dos drag-scrollers comuns, um clique, uma seleção de texto (segure parado e depois arraste) ou o arraste nativo de um link ou imagem dentro dele nunca quebram, trackpads e toque continuam nativos, e a roda nunca é roubada da página.

CategoriaRolagem
TipoWeb Component (<mv-drag-scroll>)
Statusestável
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

Instalação

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

Agente de IA com o servidor MCP do Marvelous UI: install_components({ slugs: ["drag-scroll"], target_dir: "<absolute path>/src/marvelous", framework: "react" }).

Arquivos copiados (dependências incluídas): tokens/tokens.css, core/base.css, core/dom.js, core/element.js, core/i18n.js, core/motion.js, components/drag-scroll/drag-scroll.js, components/drag-scroll/drag-scroll.css.

Uso

Início rápido, a menor marcação que funciona:

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

Marcação de referência, para usar como ponto de partida e personalizar com atributos, data-* e variáveis 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

NameTipoPadrãoDescrição
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

NameTipoDescrição
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

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

Events

NameDescrição
mv-overflowWhen the hidden sides change. detail = { start, end }.

Content structure

NameDescrição
(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

NameDescrição
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

NamePadrãoDescrição
--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. Known limits: horizontal only (vertical overflow stays native); keyboard scrolling is the browser's own (arrows scroll by a step, Home and End follow the browser, not a page of items); the trackpad and mouse wheel are told apart by heuristics on the wheel deltas, so an unusual mouse driver can be taken for a trackpad and simply scroll natively.

Esta página foi traduzida com IA. Informar um problema de tradução