HextaUI

Scroll area

Natives Scrollen mit minimaler Scrollbar, Kanten, die nur dort ausblenden, wo es mehr zu sehen gibt, und einem optionalen Anschnitt, der das letzte Element halbiert.

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

const names = [
  "Olivia Martin",
  "Jackson Lee",
  "Isabella Nguyen",
  "William Kim",
  "Sofia Davis",
  "Liam Patel",
  "Emma Garcia",
  "Noah Wilson",
]
const roles = ["Design", "Engineering", "Product", "Support"]

const people = Array.from({ length: 40 }, (_, index) => ({
  id: index + 1,
  name: names[index % names.length],
  role: roles[index % roles.length],
}))

export function ScrollAreaDemo() {
  return (
    <ScrollArea peek className="h-80 w-full max-w-sm rounded-lg border">
      <ul className="flex flex-col gap-1 p-2">
        {people.map((person) => (
          <li
            key={person.id}
            className="flex items-center justify-between gap-3 rounded-md px-3 py-2.5 text-sm hover:bg-muted"
          >
            <span className="truncate font-medium">
              {person.id}. {person.name}
            </span>
            <span className="text-muted-foreground">{person.role}</span>
          </li>
        ))}
      </ul>
    </ScrollArea>
  )
}
pnpm dlx shadcn@latest add https://hextaui.com/r/scroll-area.json

Fügt die Komponente, die HextaUI-Theme-Tokens und alle HextaUI-Komponenten hinzu, von denen sie abhängt.

Gib dem Scrollbereich eine feste Höhe oder Breite. Inhalt darüber hinaus scrollt mit dem nativen Scrollverhalten, und eine dünne Scrollleiste erscheint beim Hovern oder Scrollen.

import { ScrollArea } from "@/components/ui/scroll-area"
<ScrollArea className="h-72 rounded-lg border">
  <div className="p-4">{/* long content */}</div>
</ScrollArea>

Peek

Beide Listen haben dieselbe Höhe. Mit peek, rechts, kürzt sich die Box selbst, sodass das letzte sichtbare Item halb abgeschnitten ist, was zeigt, dass es mehr zu scrollen gibt, bevor es jemand versucht. Sie kürzt die Box nie unter die halbe Höhe.

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

const names = [
  "Olivia Martin",
  "Jackson Lee",
  "Isabella Nguyen",
  "William Kim",
  "Sofia Davis",
  "Liam Patel",
  "Emma Garcia",
  "Noah Wilson",
]
const roles = ["Design", "Engineering", "Product", "Support"]

const people = Array.from({ length: 40 }, (_, index) => ({
  id: index + 1,
  name: names[index % names.length],
  role: roles[index % roles.length],
}))

function PeopleList() {
  return (
    <ul className="flex flex-col gap-1 p-2">
      {people.map((person) => (
        <li
          key={person.id}
          className="flex items-center justify-between gap-3 rounded-md px-3 py-2.5 text-sm hover:bg-muted"
        >
          <span className="truncate font-medium">
            {person.id}. {person.name}
          </span>
          <span className="text-muted-foreground">{person.role}</span>
        </li>
      ))}
    </ul>
  )
}

export function ScrollAreaPeek() {
  return (
    <div className="grid w-full max-w-xl grid-cols-2 gap-4">
      <ScrollArea className="h-80 rounded-lg border">
        <PeopleList />
      </ScrollArea>
      <ScrollArea peek className="h-80 rounded-lg border">
        <PeopleList />
      </ScrollArea>
    </div>
  )
}

Text

Ränder blenden nur dort aus, wo es mehr zu scrollen gibt. Oben blendet nur unten aus, und der Fade wächst ein, wenn du dich von einem Rand wegscrollst.

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

export function ScrollAreaText() {
  return (
    <ScrollArea className="h-48 w-full max-w-md rounded-lg border">
      <div className="flex flex-col gap-3 p-4 text-sm leading-6">
        {Array.from({ length: 6 }, (_, index) => (
          <p key={index}>
            Vernacular architecture is building done outside any academic
            tradition, and without professional guidance. It reflects local
            traditions, materials and climate, and makes up most of the world’s
            built environment.
          </p>
        ))}
      </div>
    </ScrollArea>
  )
}

Unten beginnen

viewportRef gibt dir das scrollende Element. Setze sein scrollTop, um einen Chat bei der neuesten Nachricht zu beginnen, sodass nur der obere Rand ausblendet.

"use client"

import * as React from "react"

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

export function ScrollAreaChat() {
  const viewportRef = React.useRef<HTMLDivElement>(null)

  React.useLayoutEffect(() => {
    const viewport = viewportRef.current
    if (viewport) {
      viewport.scrollTop = viewport.scrollHeight
    }
  }, [])

  return (
    <ScrollArea
      viewportRef={viewportRef}
      aria-label="Messages"
      className="h-56 w-full max-w-sm rounded-lg border"
    >
      <div className="flex flex-col gap-2 p-3 text-sm">
        {Array.from({ length: 24 }, (_, index) => (
          <p
            key={index}
            className={
              index % 3 === 0
                ? "self-end rounded-lg bg-primary px-3 py-2 text-primary-foreground"
                : "self-start rounded-lg bg-muted px-3 py-2"
            }
          >
            Message {index + 1}
          </p>
        ))}
      </div>
    </ScrollArea>
  )
}

Horizontal

Setze scrollbars="horizontal" und gib dem Inhalt w-max, damit er über die Box hinauswachsen kann.

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

const tags = [
  "Design systems",
  "Motion",
  "Accessibility",
  "Typography",
  "Color",
  "Layout",
  "Forms",
  "Data tables",
  "Charts",
  "Navigation",
  "Overlays",
  "Feedback",
]

export function ScrollAreaHorizontal() {
  return (
    <ScrollArea
      scrollbars="horizontal"
      className="w-full max-w-md rounded-lg border"
    >
      <div className="flex w-max gap-2 p-3">
        {tags.map((tag) => (
          <span
            key={tag}
            className="rounded-full border px-3 py-1 text-sm whitespace-nowrap"
          >
            {tag}
          </span>
        ))}
      </div>
    </ScrollArea>
  )
}

Beide Achsen

scrollbars="both" zeigt beide Scrollleisten und eine Ecke, wo sie sich treffen, und blendet alle vier Ränder aus.

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

export function ScrollAreaBoth() {
  return (
    <ScrollArea
      scrollbars="both"
      className="h-64 w-full max-w-md rounded-lg border"
    >
      <div className="grid w-max grid-cols-[repeat(10,6rem)] gap-2 p-3">
        {Array.from({ length: 100 }, (_, index) => (
          <div
            key={index}
            className="grid aspect-square place-items-center rounded-md bg-muted text-sm"
          >
            {index + 1}
          </div>
        ))}
      </div>
    </ScrollArea>
  )
}

Dynamischer Inhalt

Peek und die Fades aktualisieren sich, wenn Items hinzukommen oder entfallen. Bei zu wenigen Items zum Scrollen behält die Box ihre Höhe, und nichts blendet aus.

"use client"

import * as React from "react"

import { Button } from "@/components/ui/button"
import { ScrollArea } from "@/components/ui/scroll-area"

export function ScrollAreaDynamic() {
  const [count, setCount] = React.useState(4)

  return (
    <div className="flex w-full max-w-sm flex-col gap-3">
      <ScrollArea peek className="h-48 rounded-lg border">
        <ul className="flex flex-col gap-1 p-2">
          {Array.from({ length: count }, (_, index) => (
            <li key={index} className="rounded-md bg-muted px-3 py-3 text-sm">
              Item {index + 1}
            </li>
          ))}
        </ul>
      </ScrollArea>
      <div className="flex gap-2">
        <Button size="sm" onClick={() => setCount(count + 1)}>
          Add item
        </Button>
        <Button
          size="sm"
          variant="ghost"
          onClick={() => setCount(Math.max(0, count - 1))}
        >
          Remove item
        </Button>
      </div>
    </div>
  )
}

Auf einer farbigen Fläche

Der Fade ist eine Maske auf dem Inhalt und keine Überlagerungsfarbe, sodass er auf jedem Hintergrund funktioniert.

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

const names = [
  "Olivia Martin",
  "Jackson Lee",
  "Isabella Nguyen",
  "William Kim",
  "Sofia Davis",
  "Liam Patel",
  "Emma Garcia",
  "Noah Wilson",
]
const roles = ["Design", "Engineering", "Product", "Support"]

const people = Array.from({ length: 40 }, (_, index) => ({
  id: index + 1,
  name: names[index % names.length],
  role: roles[index % roles.length],
}))

export function ScrollAreaColoredSurface() {
  return (
    <div className="w-full max-w-sm rounded-xl bg-muted p-2">
      <ScrollArea className="h-56">
        <ul className="flex flex-col gap-1 p-2">
          {people.map((person) => (
            <li
              key={person.id}
              className="flex items-center justify-between gap-3 rounded-md px-3 py-2.5 text-sm hover:bg-background"
            >
              <span className="truncate font-medium">
                {person.id}. {person.name}
              </span>
              <span className="text-muted-foreground">{person.role}</span>
            </li>
          ))}
        </ul>
      </ScrollArea>
    </div>
  )
}

In einem Sheet

Umschließe ihn in einer Flex-Spalte mit einem Element mit min-h-0 flex-1 und gib dem Scrollbereich h-full, damit er den restlichen Platz füllt.

import { Button } from "@/components/ui/button"
import { ScrollArea } from "@/components/ui/scroll-area"
import {
  Sheet,
  SheetContent,
  SheetHeader,
  SheetTitle,
  SheetTrigger,
} from "@/components/ui/sheet"

const names = [
  "Olivia Martin",
  "Jackson Lee",
  "Isabella Nguyen",
  "William Kim",
  "Sofia Davis",
  "Liam Patel",
  "Emma Garcia",
  "Noah Wilson",
]
const roles = ["Design", "Engineering", "Product", "Support"]

const people = Array.from({ length: 40 }, (_, index) => ({
  id: index + 1,
  name: names[index % names.length],
  role: roles[index % roles.length],
}))

export function ScrollAreaSheet() {
  return (
    <Sheet>
      <SheetTrigger render={<Button variant="outline" />}>
        Open members
      </SheetTrigger>
      <SheetContent>
        <SheetHeader>
          <SheetTitle>Members</SheetTitle>
        </SheetHeader>
        <div className="min-h-0 flex-1 px-4 pb-4">
          <ScrollArea peek className="h-full">
            <ul className="flex flex-col gap-1 p-2">
              {people.map((person) => (
                <li
                  key={person.id}
                  className="flex items-center justify-between gap-3 rounded-md px-3 py-2.5 text-sm hover:bg-muted"
                >
                  <span className="truncate font-medium">
                    {person.id}. {person.name}
                  </span>
                  <span className="text-muted-foreground">{person.role}</span>
                </li>
              ))}
            </ul>
          </ScrollArea>
        </div>
      </SheetContent>
    </Sheet>
  )
}

Ohne Fade

Schalte die Rand-Fades mit fade={false} ab.

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

const names = [
  "Olivia Martin",
  "Jackson Lee",
  "Isabella Nguyen",
  "William Kim",
  "Sofia Davis",
  "Liam Patel",
  "Emma Garcia",
  "Noah Wilson",
]
const roles = ["Design", "Engineering", "Product", "Support"]

const people = Array.from({ length: 40 }, (_, index) => ({
  id: index + 1,
  name: names[index % names.length],
  role: roles[index % roles.length],
}))

export function ScrollAreaWithoutFade() {
  return (
    <ScrollArea fade={false} className="h-40 w-full max-w-sm rounded-lg border">
      <ul className="flex flex-col gap-1 p-2">
        {people.map((person) => (
          <li
            key={person.id}
            className="flex items-center justify-between gap-3 rounded-md px-3 py-2.5 text-sm hover:bg-muted"
          >
            <span className="truncate font-medium">
              {person.id}. {person.name}
            </span>
            <span className="text-muted-foreground">{person.role}</span>
          </li>
        ))}
      </ul>
    </ScrollArea>
  )
}

Rechts nach links

Der Inhalt beginnt am rechten Rand, Scrollleiste und Fades folgen der Leserichtung, und die Richtung wird von der Seite übernommen.

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

const tags = [
  "أنظمة التصميم",
  "الحركة",
  "إمكانية الوصول",
  "الطباعة",
  "الألوان",
  "التخطيط",
  "النماذج",
  "جداول البيانات",
  "الرسوم البيانية",
  "التنقل",
]

export function ScrollAreaRtl() {
  return (
    <div dir="rtl" className="w-full max-w-md">
      <ScrollArea scrollbars="horizontal" className="rounded-lg border">
        <div className="flex w-max gap-2 p-3">
          {tags.map((tag) => (
            <span
              key={tag}
              className="rounded-full border px-3 py-1 text-sm whitespace-nowrap"
            >
              {tag}
            </span>
          ))}
        </div>
      </ScrollArea>
    </div>
  )
}

Der Viewport gehört nur zur Tab-Reihenfolge, wenn er etwas zu scrollen hat. Fokussiert, scrollt er mit den nativen Tasten des Browsers.

TasteAktion
TabFokussiert den Viewport, wenn sein Inhalt überläuft.
↑↓Scrollt vertikal.
←→Scrollt horizontal.
Page UpPage DownSpaceScrollt um eine Seite.
HomeEndSpringt zum Anfang oder Ende.
  • Gib dem Scrollbereich ein aria-label, wenn sein Zweck aus dem umgebenden Inhalt nicht klar wird, wie das Label „Messages“ im Chat-Beispiel.
  • Der Fokusring wird auf dem Scrollbereich selbst gezeichnet und bleibt so über den ausgeblendeten Rändern sichtbar.

Gebaut auf der Base UI Scroll Area. ScrollArea rendert Root, Viewport, Inhalt und Scrollleisten zusammen.

PropTypStandard
scrollbars
"vertical" | "horizontal" | "both""vertical"
fadeBlende die Ränder aus, die mehr Inhalt haben.
booleantrue
peekKürze die Höhe, sodass das letzte sichtbare Item halb abgeschnitten ist. Markiere Items mit data-scroll-area-item, um zu wählen, welche Elemente zählen.
booleanfalse
viewportRefDas Element, das scrollt.
Ref<HTMLDivElement>–
overflowEdgeThresholdPixel, die gescrollt werden, bevor ein Rand als überlaufend gilt.
number | { xStart, xEnd, yStart, yEnd }0
className
string | (state) => string–
render
ReactElement | (props, state) => ReactElement<div>
AttributBeschreibung
data-slot="scroll-area"Das Root.
data-slot="scroll-area-viewport"Das Element, das scrollt.
data-slot="scroll-area-content"Umschließt deinen Inhalt im Viewport.
data-peekVorhanden, wenn peek aktiv ist.
data-peekingVorhanden, solange peek die Höhe kürzt.
data-scrollingVorhanden, solange der Nutzer scrollt.
data-has-overflow-xVorhanden, wenn der Inhalt breiter als der Viewport ist.
data-has-overflow-yVorhanden, wenn der Inhalt höher als der Viewport ist.
data-overflow-y-startVorhanden, wenn darüber mehr Inhalt liegt. Entsprechende Attribute -y-end, -x-start und -x-end gibt es für die anderen Ränder.
--scroll-area-fadeGröße des Rand-Fades. Standard ist 2.5rem.
--scroll-area-overflow-y-startAbstand vom oberen Rand in Pixeln. Entsprechende Variablen -y-end, -x-start und -x-end existieren.

Wird von ScrollArea für dich gerendert. Exportiere es nur, wenn du die Base-UI-Teile selbst zusammensetzt.

PropTypStandard
orientation
"vertical" | "horizontal""vertical"
render
ReactElement | (props, state) => ReactElement<div>
AttributBeschreibung
data-slot="scroll-area-scrollbar"Die Spur der Scrollleiste.
data-slot="scroll-area-thumb"Der ziehbare Thumb.
data-orientationvertical oder horizontal.
data-hoveringVorhanden, solange der Zeiger über dem Bereich ist.
data-scrollingVorhanden, solange der Nutzer scrollt.
--scroll-area-thumb-heightDie Höhe des Thumbs.
--scroll-area-thumb-widthDie Breite des Thumbs.