Exclusivobeta
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: 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.
| Categoria | Rolagem |
|---|---|
| Tipo | Web Component (<mv-drag-scroll>) |
| Status | beta |
| Keywords | exclusive, 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/marvelousAgente 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/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
| Name | Tipo | Default | Description |
|---|---|---|---|
buttons | boolean | Round previous / next buttons on the hidden sides, each scrolling one visible width (minus the fades). | |
label | string | table caption, else “Scrollable content” | Accessible name of the scroll region (a <caption> inside is used automatically). |
no-hint | boolean | Keeps the fades but removes the “More” hint chips. | |
no-wheel | boolean | Leaves the vertical mouse wheel alone (never turned sideways). | |
no-drag | boolean | Disables 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-scrolled | boolean | Set once the reader has scrolled (the hint drops its word). | |
data-dragging | boolean | Set during a grab-drag. |
Properties
| Name | Tipo | Description |
|---|---|---|
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. |
scroller | HTMLElement | The scrolling element (read-only). |
overflow | { start, end } | Which sides currently hide content (read-only). |
Methods
| Name | Description |
|---|---|
page(direction) | Scrolls one visible width toward the end (1) or the start (-1), smoothly unless motion is reduced. |
Events
| Name | Description |
|---|---|
mv-overflow | When the hidden sides change. detail = { start, end }. |
Content structure
| Name | Description |
|---|---|
(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
| Name | Description |
|---|---|
mv-drag-scroll-viewport | The scrolling box (generated, detected or yours). |
mv-drag-scroll-hint / -button | Generated hint chips (decorative) or paging buttons, with data-side="start|end". |
CSS variables
| Name | Default | Description |
|---|---|---|
--mv-drag-scroll-fade | 3rem | Width of the edge fades. |
--mv-drag-scroll-hint-top | 50% | Vertical position of the hint chips and buttons. |
--mv-drag-scroll-radius | var(--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.