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

Overview

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

AllOpenPending reviewBlockedScheduledArchivedDraft

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

xs
AllOpenPending reviewBlockedScheduledArchivedDraft
sm
AllOpenPending reviewBlockedScheduledArchivedDraft
md
AllOpenPending reviewBlockedScheduledArchivedDraft
lg
AllOpenPending reviewBlockedScheduledArchivedDraft
xl
AllOpenPending reviewBlockedScheduledArchivedDraft

Content that fits — no controls at all

OneTwoThree

Right-to-left

AllOpenPending reviewBlockedScheduledArchivedDraft

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-react
import { ScrollStrip } from "@wizeworks/silicaui-react";

Props

PropTypeRequiredDescription
labelstringyesWhat 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.
sizeSilicaSizenoControl size on the xs–xl scale. Default md.
fadebooleannoAlso 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.
stepnumbernoFraction 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.
controlsbooleannoRender 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.
trackClassNamestringnoExtra 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-behaviors
import { 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.