Scroll Strip
A horizontal strip that says so when there is more of it off-screen.
Real use · a tab strip in a pane you can drag narrow
Drag it narrow: the pair of controls appears the moment a tab falls off the edge, and each one disables at its end rather than disappearing — so the strip never jumps sideways.
Edge fade — a second, quieter signal
The controls say the strip can scroll; the fade says the content continues right there. It clears itself at each end, so a strip that fits is never dimmed.
Control sizes
Content that fits — no controls at all
Right-to-left
Using Scroll Strip
Silica ships one design system through three paths. They share a class vocabulary but are not interchangeable — pick the one that matches what you are building.
Plain HTML — CSS classes
The Tailwind v4 plugin. Works in any framework or none — Rails, Django, PHP, Go, a static file. This path ships no JavaScript, so Scroll Strip is styled but inert: its behavior comes from scroll-strip. For that behavior with no framework, use the node-tree path below.
/* in your CSS */
@import "tailwindcss";
@plugin "@wizeworks/silicaui";Root class scroll-strip, with 8 modifiers:
.scroll-strip.scroll-strip-control.scroll-strip-faded.scroll-strip-lg.scroll-strip-md.scroll-strip-sm.scroll-strip-track.scroll-strip-xl.scroll-strip-xs
React
Typed components over those same classes, with behavior and accessibility from Base UI.
npm i @wizeworks/silicaui-reactimport { ScrollStrip } from "@wizeworks/silicaui-react";Props
| Prop | Type | Required | Description |
|---|---|---|---|
| label | string | yes | What the strip holds, as a plural noun — it names the controls ("Scroll **tabs** forward") and the scroll region itself. Required: a bare chevron with no accessible name is the most common way this pattern ships broken. |
| size | SilicaSize | no | Control size on the xs–xl scale. Default md. |
| fade | boolean | no | Also fade the clipped edge, as a second and quieter signal — the controls say the strip CAN scroll, the fade says the content continues right there. Off by default: a strip that fits must not look dimmed at the edges. |
| step | number | no | Fraction of the visible width moved per press, 0–1. Default 0.8 — deliberately not a full screenful, because the sliver of overlap is what makes it read as the strip moving rather than jumping somewhere new. |
| controls | boolean | no | Render the prev/next controls. Default true. Setting false keeps the scroller (and its keyboard reachability) but drops the buttons — for a strip whose overflow is already advertised some other way. |
| trackClassName | string | no | Extra classes for the scroller itself, e.g. gap-2 py-1. |
Source of the demo above
The exact file that renders the examples above, so it can never drift from them. It imports a few of this site’s own layout helpers (../lib/…), so read it for the component calls rather than copying it whole.
Show source
import { useState } from "react";
import { Badge, Button, ScrollStrip } from "@wizeworks/silicaui-react";
import { Section, Row } from "../lib/Section";
// Six, not more: the pane has to be draggable to a width where they all FIT,
// or the demo only ever shows one of the two states it exists to contrast.
const TABS = ["Overview", "Timeline", "Messages", "Activity", "Documents", "Details"];
const CHIPS = ["All", "Open", "Pending review", "Blocked", "Scheduled", "Archived", "Draft"];
const SIZES = ["xs", "sm", "md", "lg", "xl"] as const;
export function ScrollStripDemo() {
// The demo has to be narrow-able, because the whole component only exists
// for the width at which the content stops fitting — a fixed-width showcase
// would never once show the controls.
const [width, setWidth] = useState(28);
const [active, setActive] = useState("Overview");
return (
<>
<Section title="Real use · a tab strip in a pane you can drag narrow">
<div className="flex flex-col gap-3">
<label className="flex items-center gap-3 text-sm">
<span className="w-24 shrink-0">Pane width</span>
<input
type="range"
min={14}
max={48}
value={width}
onChange={(e) => setWidth(Number(e.target.value))}
className="range range-sm max-w-xs"
/>
<span className="tabular-nums">{width}rem</span>
</label>
<div
className="rounded-box border border-base-300"
style={{ width: `${width}rem` }}
>
<ScrollStrip data-demo="pane" label="tabs" trackClassName="gap-1 p-1">
{TABS.map((tab) => (
<Button
key={tab}
variant={tab === active ? "soft" : "ghost"}
color="neutral"
size="sm"
onClick={() => setActive(tab)}
>
{tab}
</Button>
))}
</ScrollStrip>
<div className="border-t border-base-300 p-4 text-sm">{active}</div>
</div>
<p className="max-w-md text-sm">
Drag it narrow: the pair of controls appears the moment a tab falls
off the edge, and each one disables at its end rather than
disappearing — so the strip never jumps sideways.
</p>
</div>
</Section>
<Section title="Edge fade — a second, quieter signal">
<div className="flex w-80 flex-col gap-4">
<ScrollStrip data-demo="fade" label="filters" fade size="sm" trackClassName="gap-2">
{CHIPS.map((chip) => (
<Badge key={chip} variant="outline" color="neutral">
{chip}
</Badge>
))}
</ScrollStrip>
<p className="text-sm">
The controls say the strip <em>can</em> scroll; the fade says the
content continues right there. It clears itself at each end, so a
strip that fits is never dimmed.
</p>
</div>
</Section>
<Section title="Control sizes">
<div className="flex flex-col gap-3">
{SIZES.map((size) => (
<div key={size} className="flex items-center gap-3">
<span className="w-8 shrink-0 text-sm">{size}</span>
<div className="w-72">
<ScrollStrip data-demo={`strip-size-${size}`} label="cards" size={size} trackClassName="gap-2">
{CHIPS.map((chip) => (
<Badge key={chip} color="neutral" variant="outline">
{chip}
</Badge>
))}
</ScrollStrip>
</div>
</div>
))}
</div>
</Section>
<Section title="Content that fits — no controls at all">
<Row>
<div className="w-96 rounded-box border border-base-300 p-1">
<ScrollStrip data-demo="fits" label="tags" trackClassName="gap-2">
<Badge color="neutral">One</Badge>
<Badge color="neutral">Two</Badge>
<Badge color="neutral">Three</Badge>
</ScrollStrip>
</div>
</Row>
</Section>
<Section title="Right-to-left">
<div dir="rtl" className="w-72 rounded-box border border-base-300 p-1">
<ScrollStrip data-demo="rtl" label="عناصر" trackClassName="gap-2">
{CHIPS.map((chip) => (
<Badge key={chip} variant="outline" color="neutral">
{chip}
</Badge>
))}
</ScrollStrip>
</div>
</Section>
</>
);
}Vanilla — node tree to HTML
scroll-strip — a horizontal strip that SAYS SO when part of it is off-screen. Build a node tree, project it with toHtml(), and let the zero-dependency runtime hydrate it. This is the path for generated or user-authored documents — a site builder, CMS output, a static export.
npm i @wizeworks/silicaui-html @wizeworks/silicaui-behaviorsimport { atom, toHtml } from "@wizeworks/silicaui-html";
const node = atom("ScrollStrip", "scroll-strip", { /* props */ }, [/* children */]);
const markup = toHtml(node);Styling on this path is the class string in the second argument — the same classes as the Plain HTML section above. There is no color prop here; write "scroll-strip scroll-strip-primary" rather than passing a colour as a prop.
- Takes children
- yes
- Hydrated by
scroll-strip
Load @wizeworks/silicaui-behaviors on the page or this markup renders correctly and does nothing.
The full API for Scroll Strip, on every path, is available through the Silica MCP server.