HextaUI

Scroll area

Un défilement natif avec une barre de défilement minimale, des bords qui s’estompent seulement là où il y a plus à voir, et un aperçu facultatif qui coupe le dernier élément en deux.

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

Ajoute le composant, les tokens de thème HextaUI et les composants HextaUI dont il dépend.

Donnez à la zone de défilement une hauteur ou une largeur fixe. Le contenu au-delà défile avec le comportement natif, et une fine barre de défilement apparaît au survol ou pendant le défilement.

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

Peek

Les deux listes ont la même hauteur. Avec peek, à droite, la boîte se raccourcit pour que le dernier élément visible soit coupé à mi-hauteur, ce qui montre qu'il reste à faire défiler avant même que quelqu'un essaie. Elle ne rogne jamais la boîte en dessous de la moitié de sa hauteur.

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>
  )
}

Texte

Les bords ne s'estompent que là où il reste du contenu à faire défiler. En haut, seul le bas s'estompe, et le fondu s'installe progressivement quand vous vous éloignez d'un bord.

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>
  )
}

Démarrer en bas

viewportRef vous donne l'élément qui défile. Définissez son scrollTop pour démarrer un chat au dernier message, de sorte que seul le bord supérieur s'estompe.

"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

Définissez scrollbars="horizontal" et donnez w-max au contenu pour qu'il puisse dépasser la boîte.

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>
  )
}

Les deux axes

scrollbars="both" affiche les deux barres de défilement et un coin à leur jonction, et estompe les quatre bords.

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>
  )
}

Contenu dynamique

Peek et les fondus se mettent à jour quand des éléments sont ajoutés ou retirés. Avec trop peu d'éléments pour défiler, la boîte garde sa hauteur et rien ne s'estompe.

"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>
  )
}

Sur une surface colorée

Le fondu est un masque sur le contenu, pas une couleur en surcouche : il fonctionne sur n'importe quel arrière-plan.

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>
  )
}

Dans une sheet

Dans une colonne flex, entourez-la d'un élément min-h-0 flex-1 et donnez h-full à la zone de défilement pour qu'elle remplisse l'espace restant.

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>
  )
}

Sans fondu

Désactivez les fondus de bord avec fade={false}.

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>
  )
}

De droite à gauche

Le contenu commence au bord droit, la barre de défilement et les fondus suivent le sens de lecture, et la direction est reprise de la page.

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>
  )
}

Le viewport n'entre dans l'ordre de tabulation que s'il y a quelque chose à faire défiler. Une fois focalisé, il défile avec les touches natives du navigateur.

ToucheAction
TabDonne le focus au viewport quand son contenu déborde.
↑↓Défile verticalement.
←→Défile horizontalement.
Page UpPage DownSpaceDéfile d'une page.
HomeEndSaute au début ou à la fin.
  • Donnez un aria-label à la zone de défilement quand son rôle n'est pas clair d'après le contenu environnant, comme le label « Messages » dans l'exemple de chat.
  • L'anneau de focus est dessiné sur la zone de défilement elle-même, et reste donc visible au-dessus des bords estompés.

Construit sur le scroll area de Base UI. ScrollArea rend ensemble la racine, le viewport, le contenu et les barres de défilement.

PropTypePar défaut
scrollbars
"vertical" | "horizontal" | "both""vertical"
fadeEstompe les bords qui ont plus de contenu.
booleantrue
peekRogne la hauteur pour que le dernier élément visible soit coupé à mi-hauteur. Marquez les éléments avec data-scroll-area-item pour choisir lesquels comptent.
booleanfalse
viewportRefL'élément qui défile.
Ref<HTMLDivElement>–
overflowEdgeThresholdPixels à parcourir avant qu'un bord compte comme débordant.
number | { xStart, xEnd, yStart, yEnd }0
className
string | (state) => string–
render
ReactElement | (props, state) => ReactElement<div>
AttributDescription
data-slot="scroll-area"La racine.
data-slot="scroll-area-viewport"L'élément qui défile.
data-slot="scroll-area-content"Enveloppe votre contenu dans le viewport.
data-peekPrésent quand peek est activé.
data-peekingPrésent tant que peek rogne la hauteur.
data-scrollingPrésent pendant que l'utilisateur fait défiler.
data-has-overflow-xPrésent quand le contenu est plus large que le viewport.
data-has-overflow-yPrésent quand le contenu est plus haut que le viewport.
data-overflow-y-startPrésent quand il y a plus de contenu au-dessus. Des attributs équivalents existent pour -y-end, -x-start et -x-end pour les autres bords.
--scroll-area-fadeTaille du fondu de bord. 2.5rem par défaut.
--scroll-area-overflow-y-startDistance depuis le bord supérieur en pixels. Des variables équivalentes existent pour -y-end, -x-start et -x-end.

Rendu pour vous par ScrollArea. N'exportez-le que si vous composez vous-même les parties de Base UI.

PropTypePar défaut
orientation
"vertical" | "horizontal""vertical"
render
ReactElement | (props, state) => ReactElement<div>
AttributDescription
data-slot="scroll-area-scrollbar"La piste de la barre de défilement.
data-slot="scroll-area-thumb"Le curseur déplaçable.
data-orientationvertical ou horizontal.
data-hoveringPrésent tant que le pointeur est sur la zone.
data-scrollingPrésent pendant que l'utilisateur fait défiler.
--scroll-area-thumb-heightLa hauteur du curseur.
--scroll-area-thumb-widthLa largeur du curseur.