Przejdź do treści

Scroll Area

Przewijany obszar z dyskretnymi, nakładanymi paskami przewijania dopasowanymi do motywu — pod spodem natywne przewijanie, na wierzchu spójna oprawa.

Kod źródłowyBase UIRejestr
import { Fragment } from "react"

import { ScrollArea } from "@/components/ui/scroll-area"
import { Separator } from "@/components/ui/separator"

const releases = Array.from({ length: 40 }, (_, i) => `v1.${40 - i}.0`)

export default function ScrollAreaDemo() {
  return (
    <ScrollArea className="h-64 w-48 rounded-xl border bg-card shadow-raised">
      <div className="p-4">
        <p className="mb-3 text-sm font-semibold text-foreground">Wydania</p>
        {releases.map((release, index) => (
          <Fragment key={release}>
            {index > 0 && <Separator className="my-2" />}
            <div className="font-mono text-code text-muted-foreground">
              {release}
            </div>
          </Fragment>
        ))}
      </div>
    </ScrollArea>
  )
}

Instalacja

$ pnpm dlx shadcn@latest add https://www.prfct.dev/r/scroll-area.json

Użycie

import { ScrollArea, ScrollBar } from "@/components/ui/scroll-area"
<ScrollArea className="h-72 w-48 rounded-xl border">
  {/* long content */}
</ScrollArea>

Nadaj obszarowi stałą wysokość (lub szerokość, przy treści poziomej) — przewija się wszystko, co wykracza poza tę ramkę. Samo przewijanie jest natywne: kółko myszy, bezwładność gładzika, dotyk, klawiatura i zakotwiczanie przewijania działają dokładnie tak jak na zwykłym elemencie z overflow: auto.

Przykłady

Przewijanie poziome

Treść przewijana w bok musi być szersza niż obszar (w-max na wierszu flex) i potrzebuje poziomego paska przewijania. Dodaj <ScrollBar orientation="horizontal" /> jako element potomny — pionowy pasek jest dołączony zawsze i pojawia się tylko wtedy, gdy treść wykracza poza obszar w pionie.

import { ScrollArea, ScrollBar } from "@/components/ui/scroll-area"

const works = [
  { title: "Południk", artist: "Ola Nowak" },
  { title: "Odpływ", artist: "Jonas Berg" },
  { title: "Żar", artist: "Maya Chen" },
  { title: "Notatki z terenu", artist: "Iris Vale" },
  { title: "Nokturn", artist: "Theo Park" },
  { title: "Rozkwit", artist: "Lena Roth" },
]

export default function ScrollAreaHorizontal() {
  return (
    <ScrollArea className="w-full max-w-lg rounded-xl border bg-card">
      <div className="flex w-max gap-4 p-4">
        {works.map((work) => (
          <figure
            key={work.title}
            className="flex w-40 shrink-0 flex-col gap-2"
          >
            <div className="aspect-[3/4] rounded-lg bg-muted" />
            <figcaption className="flex flex-col">
              <span className="text-sm font-medium text-foreground">
                {work.title}
              </span>
              <span className="text-xs text-muted-foreground">
                {work.artist}
              </span>
            </figcaption>
          </figure>
        ))}
      </div>
      <ScrollBar orientation="horizontal" />
    </ScrollArea>
  )
}

Obie osie

Szerokie dane — arkusze kalkulacyjne, osie czasu, duże tabele — mogą przewijać się w obu kierunkach. Przyklejone nagłówki i pierwsze kolumny pomagają zachować orientację; narożnik, w którym spotykają się paski przewijania, pozostaje pusty.

import { ScrollArea, ScrollBar } from "@/components/ui/scroll-area"

const months = [
  "Sty",
  "Lut",
  "Mar",
  "Kwi",
  "Maj",
  "Cze",
  "Lip",
  "Sie",
  "Wrz",
  "Paź",
  "Lis",
  "Gru",
]
const regions = [
  "Ameryka Północna",
  "Ameryka Południowa",
  "Europa Zachodnia",
  "Europa Środkowa",
  "Kraje nordyckie",
  "Bliski Wschód",
  "Afryka",
  "Azja Południowa",
  "Azja Wschodnia",
  "Oceania",
]

function value(row: number, col: number) {
  return (((row + 3) * (col + 7) * 37) % 900) + 100
}

export default function ScrollAreaBothAxes() {
  return (
    <ScrollArea className="h-72 w-full max-w-lg rounded-xl border bg-card shadow-raised">
      <table className="w-max border-collapse text-sm numeric">
        <thead className="sticky top-0 z-raised bg-surface">
          <tr className="border-b">
            <th
              scope="col"
              className="sticky left-0 z-raised bg-surface px-4 py-2.5 text-left text-xs font-medium text-muted-foreground"
            >
              Region
            </th>
            {months.map((month) => (
              <th
                key={month}
                scope="col"
                className="px-4 py-2.5 text-right text-xs font-medium text-muted-foreground"
              >
                {month}
              </th>
            ))}
          </tr>
        </thead>
        <tbody>
          {regions.map((region, row) => (
            <tr key={region} className="border-b last:border-0">
              <td className="sticky left-0 bg-card px-4 py-2.5 whitespace-nowrap text-foreground">
                {region}
              </td>
              {months.map((month, col) => (
                <td
                  key={month}
                  className="px-4 py-2.5 text-right text-muted-foreground"
                >
                  {value(row, col).toLocaleString("pl-PL")}
                </td>
              ))}
            </tr>
          ))}
        </tbody>
      </table>
      <ScrollBar orientation="horizontal" />
    </ScrollArea>
  )
}

Zanikające krawędzie

Element główny informuje, gdzie treść wykracza poza obszar, przez atrybuty data-overflow-y-start i data-overflow-y-end (oraz ich odpowiedniki -x-). Wygaszaj krawędź tylko wtedy, gdy w danym kierunku jest więcej treści — to wskazówka, że obszar się przewija, która znika po dotarciu do końca.

import { Avatar, AvatarFallback } from "@/components/ui/avatar"
import { ScrollArea } from "@/components/ui/scroll-area"
import { cn } from "@/lib/utils"

const activity = [
  {
    who: "AK",
    name: "Ana Kowalska",
    what: "scaliła #482 z gałęzią main",
    when: "2 min",
  },
  {
    who: "JB",
    name: "Jonas Berg",
    what: "skomentował Tokeny v2",
    when: "14 min",
  },
  {
    who: "MC",
    name: "Maya Chen",
    what: "zgłosiła błąd pułapki fokusu w oknie dialogowym",
    when: "1 godz.",
  },
  { who: "TP", name: "Theo Park", what: "wydał v1.4.0", when: "3 godz." },
  {
    who: "IV",
    name: "Iris Vale",
    what: "zaprosiła 3 osoby do zespołu projektowego",
    when: "5 godz.",
  },
  { who: "LR", name: "Lena Roth", what: "zarchiwizowała stare ikony", when: "1 dzień" },
  {
    who: "OS",
    name: "Omar Said",
    what: "zaktualizował testy kontrastu",
    when: "1 dzień",
  },
  { who: "AK", name: "Ana Kowalska", what: "zatwierdziła #479", when: "2 dni" },
]

const fade =
  "before:pointer-events-none before:absolute before:inset-x-0 before:right-2.5 before:top-0 before:z-raised before:h-10 before:rounded-t-xl before:bg-linear-to-b before:from-card before:to-transparent before:opacity-0 before:transition-opacity before:duration-base data-overflow-y-start:before:opacity-100 " +
  "after:pointer-events-none after:absolute after:inset-x-0 after:right-2.5 after:bottom-0 after:z-raised after:h-10 after:rounded-b-xl after:bg-linear-to-t after:from-card after:to-transparent after:opacity-0 after:transition-opacity after:duration-base data-overflow-y-end:after:opacity-100"

export default function ScrollAreaFadeEdges() {
  return (
    <ScrollArea
      className={cn(
        "h-72 w-full max-w-sm rounded-xl border bg-card shadow-raised",
        fade
      )}
    >
      <ul className="flex flex-col p-2">
        {activity.map((item, index) => (
          <li
            key={index}
            className="flex items-center gap-3 rounded-lg px-2 py-2.5"
          >
            <Avatar size="sm">
              <AvatarFallback>{item.who}</AvatarFallback>
            </Avatar>
            <p className="min-w-0 flex-1 truncate text-sm text-muted-foreground">
              <span className="font-medium text-foreground">{item.name}</span>{" "}
              {item.what}
            </p>
            <span className="text-xs text-muted-foreground numeric">
              {item.when}
            </span>
          </li>
        ))}
      </ul>
    </ScrollArea>
  )
}
<ScrollArea
  className={cn(
    "relative h-72",
    "after:absolute after:inset-x-0 after:bottom-0 after:h-10 after:bg-linear-to-t after:from-card",
    "after:opacity-0 after:transition-opacity data-overflow-y-end:after:opacity-100"
  )}
>

Paski przewijania

Paski przewijania nakładają się na treść, zamiast zabierać jej miejsce, więc układ nie zmienia się w zależności od platformy. Pozostają ukryte, dopóki nie najedziesz na obszar lub go nie przewiniesz, a potem znikają; suwak ciemnieje po najechaniu i można go przeciągać.

Wytyczne

Kiedy używać

  • Do ograniczonych obszarów w układzie, które przewijają się niezależnie: menu, paneli bocznych, wątków czatu, list w kartach, kodu.
  • Gdy domyślny pasek przewijania platformy nie pasowałby do gęstego interfejsu z własnym motywem — zwłaszcza grube, zawsze widoczne paski w Windowsie.

Kiedy nie używać

  • Do przewijania całej strony. Pozwól dokumentowi przewijać się natywnie; zagnieżdżone przewijanie głównej treści zmusza do walki z dwiema pozycjami przewijania naraz.
  • Do ukrywania treści, która się nie mieści. Jeśli trzeba przewijać małe pole, żeby znaleźć coś ważnego, daj mu więcej miejsca.
  • Do poziomego przewijania głównej treści na komputerach. Użytkownicy przeoczają treść wykraczającą w bok; lepiej ją zawijaj lub podziel na strony.

Pokaż, że obszar się przewija

Nakładane paski przewijania są w spoczynku ukryte, więc to sam obszar musi sugerować, że kryje więcej treści. Przetnij treść przy krawędzi w połowie elementu, wygaś krawędź, za którą jest więcej treści, albo pokaż licznik.

Dziennik zmian

v1.4.0 — Tokeny v2

v1.3.2 — Poprawki

v1.3.1 — Fokus w oknie dialogowym

DobrzeOstatni element jest ucięty przy krawędzi — widać, że lista ciągnie się dalej.

Dziennik zmian

v1.4.0 — Tokeny v2

v1.3.2 — Poprawki

ŹlePole kończy się dokładnie między elementami — nic nie sugeruje, że jest ich więcej.

Jeden kierunek naraz

Trzymaj się jednej osi przewijania w każdym obszarze. Przewijanie w dwóch osiach sprawdza się w siatkach danych, gdzie użytkownicy się go spodziewają; gdzie indziej łatwo się w treści zgubić.

Dostępność

  • Przewijanie klawiaturą. Viewport trafia do kolejności tabulacji tylko wtedy, gdy jego treść wykracza poza obszar. Gdy ma fokus, ↑ ↓ ← →, Page Up, Page Down, Home, End i Spacja przewijają go natywnie, a wewnętrzny pierścień fokusu o grubości 2 px pokazuje, gdzie jesteś.
  • Nazwij obszar. Przewijany obszar, który może otrzymać fokus, ale nie ma nazwy, zostanie odczytany jako nic konkretnego. Gdy treść ma znaczenie sama w sobie, umieść ją w punkcie orientacyjnym z etykietą (<section aria-label="Release history">) albo nadaj jej widoczny nagłówek.
  • Paski przewijania to udogodnienie dla wskaźnika. Użytkownicy czytników ekranu i klawiatury przewijają viewport bezpośrednio; własne paski przewijania są — tak jak natywne — ukryte przed technologiami wspomagającymi.
  • Ograniczony ruch. Przy prefers-reduced-motion paski przewijania pojawiają się i znikają natychmiast.

Dokumentacja API

ScrollArea

Kontener przewijania: element główny, viewport, element opakowujący treść, pionowy ScrollBar i narożnik. className trafia do elementu głównego. Elementy potomne renderują się w elemencie opakowującym treść (data-slot="scroll-area-content"), który Base UI mierzy, dzięki czemu suwak nadąża za treścią, gdy ta rośnie lub maleje; elementy potomne ScrollBar renderują się obok viewportu. Przyjmuje wszystkie propsy ScrollArea.Root z Base UI.

PropTypDomyślnie
overflowEdgeThreshold

Liczba pikseli przepełnienia, po której przekroczeniu ustawiane są atrybuty krawędzi data-overflow-*.

number | { xStart?, xEnd?, yStart?, yEnd? }0
className

Stylizuje element główny. Tutaj ustaw stałą wysokość lub szerokość.

stringBrak wartości domyślnej

ScrollBar

Pasek przewijania z suwakiem. Do przewijania w bok dodaj pasek z orientation="horizontal". Przyjmuje wszystkie propsy ScrollArea.Scrollbar z Base UI.

PropTypDomyślnie
orientation

Oś, którą steruje pasek przewijania.

"vertical" | "horizontal""vertical"
keepMounted

Utrzymuje pasek w DOM, nawet gdy treść nie wykracza poza obszar w danej osi.

booleanfalse

Atrybuty danych

Ustawiane na elemencie głównym; używaj ich do stylizowania krawędzi i stanów.

AtrybutObecny, gdy
data-has-overflow-x / data-has-overflow-yTreść wykracza poza obszar w danej osi.
data-overflow-x-start / data-overflow-x-endPo lewej / po prawej jest więcej treści.
data-overflow-y-start / data-overflow-y-endPowyżej / poniżej jest więcej treści.
data-scrollingObszar jest właśnie przewijany.