Qure UI
Components

Scroll Area

A scrollable region with a scrollbar that looks the same on every platform.

'use client'import { Badge } from '@/registry/qure/ui/badge'import { ScrollArea } from '@/registry/qure/ui/scroll-area'import { Separator } from '@/registry/qure/ui/separator'const studies = [  { accession: 'ACC-100482', name: 'Rajan, M.', modality: 'CXR', tone: 'urgent', flag: 'Abnormal' },  { accession: 'ACC-100481', name: 'Fernandes, P.', modality: 'CT Thorax', tone: 'neutral', flag: 'Normal' },  { accession: 'ACC-100479', name: 'Kaur, H.', modality: 'CXR', tone: 'attention', flag: 'Review' },  { accession: 'ACC-100477', name: 'Osei, K.', modality: 'CXR', tone: 'neutral', flag: 'Normal' },  { accession: 'ACC-100474', name: 'Bianchi, L.', modality: 'CT Head', tone: 'urgent', flag: 'Abnormal' },  { accession: 'ACC-100470', name: 'Ahmed, N.', modality: 'CXR', tone: 'neutral', flag: 'Normal' },  { accession: 'ACC-100468', name: 'Novak, J.', modality: 'CT Thorax', tone: 'attention', flag: 'Review' },  { accession: 'ACC-100465', name: 'Tan, W.', modality: 'CXR', tone: 'neutral', flag: 'Normal' },] as constexport default function ScrollAreaDemo() {  return (    <ScrollArea className="h-64 w-full max-w-sm rounded-lg border">      <div className="p-3 text-sm">        {studies.map((study, i) => (          <div key={study.accession}>            {i > 0 ? <Separator className="my-2" /> : null}            <div className="flex items-center justify-between gap-3">              <div className="min-w-0">                <div className="truncate font-medium">{study.name}</div>                <div className="text-muted-foreground truncate text-xs">                  {study.accession} · {study.modality}                </div>              </div>              <Badge tone={study.tone} size="sm">{study.flag}</Badge>            </div>          </div>        ))}      </div>    </ScrollArea>  )}

Installation

npx shadcn@latest add https://qure-ui.qure.ai/r/scroll-area.json

Usage

import { ScrollArea } from '@/components/ui/scroll-area'
<ScrollArea className="h-64">
  {/* … */}
</ScrollArea>

The height belongs to the caller. ScrollArea is overflow: hidden with a viewport filling it, so a ScrollArea with no height on it, or on an ancestor, scrolls nothing and shows nothing wrong — it simply grows.

This is a cosmetic scrollbar over a real one: the viewport still scrolls natively, so wheel, trackpad, touch, keyboard and find-in-page all behave. It is the wrong choice for the page itself — hijacking the document scrollbar breaks the browser's own scroll position restore — and the right choice for a panel inside a layout that has to look identical on Windows and macOS.

Composition

<ScrollArea>          {/* clips, and positions the scrollbars */}
  <Viewport>          {/* the element that actually scrolls */}
    <Content>         {/* measured, so the thumb knows its size */}
  <ScrollBar />       {/* one per axis, rendered by orientation */}
  <Corner />          {/* only when both axes are present */}
</ScrollArea>

Our ScrollArea renders all of that for you and takes an orientation of vertical, horizontal or both. ScrollBar is exported separately for the rare case where a scrollbar needs to be placed by hand.

Vertical

'use client'import { Badge } from '@/registry/qure/ui/badge'import { ScrollArea } from '@/registry/qure/ui/scroll-area'import { Separator } from '@/registry/qure/ui/separator'const studies = [  { accession: 'ACC-100482', name: 'Rajan, M.', modality: 'CXR', tone: 'urgent', flag: 'Abnormal' },  { accession: 'ACC-100481', name: 'Fernandes, P.', modality: 'CT Thorax', tone: 'neutral', flag: 'Normal' },  { accession: 'ACC-100479', name: 'Kaur, H.', modality: 'CXR', tone: 'attention', flag: 'Review' },  { accession: 'ACC-100477', name: 'Osei, K.', modality: 'CXR', tone: 'neutral', flag: 'Normal' },  { accession: 'ACC-100474', name: 'Bianchi, L.', modality: 'CT Head', tone: 'urgent', flag: 'Abnormal' },  { accession: 'ACC-100470', name: 'Ahmed, N.', modality: 'CXR', tone: 'neutral', flag: 'Normal' },  { accession: 'ACC-100468', name: 'Novak, J.', modality: 'CT Thorax', tone: 'attention', flag: 'Review' },  { accession: 'ACC-100465', name: 'Tan, W.', modality: 'CXR', tone: 'neutral', flag: 'Normal' },] as constexport default function ScrollAreaDemo() {  return (    <ScrollArea className="h-64 w-full max-w-sm rounded-lg border">      <div className="p-3 text-sm">        {studies.map((study, i) => (          <div key={study.accession}>            {i > 0 ? <Separator className="my-2" /> : null}            <div className="flex items-center justify-between gap-3">              <div className="min-w-0">                <div className="truncate font-medium">{study.name}</div>                <div className="text-muted-foreground truncate text-xs">                  {study.accession} · {study.modality}                </div>              </div>              <Badge tone={study.tone} size="sm">{study.flag}</Badge>            </div>          </div>        ))}      </div>    </ScrollArea>  )}

The default. A worklist that has to stay a fixed height inside a column of other panels.

Horizontal

'use client'import { ScrollArea } from '@/registry/qure/ui/scroll-area'const series = [  'Scout', 'Axial 1mm', 'Axial 5mm', 'Coronal', 'Sagittal',  'Lung window', 'Mediastinal', 'Bone', 'MIP 10mm', 'MinIP',]export default function ScrollAreaHorizontal() {  return (    <ScrollArea orientation="horizontal" className="w-full max-w-md rounded-lg border">      {/* w-max is what makes the row overflow rather than wrap — without it          the flex row shrinks to the viewport and nothing ever scrolls. */}      <div className="flex w-max gap-3 p-3 pb-4">        {series.map(name => (          <figure key={name} className="w-28 shrink-0">            <div className="bg-muted aspect-square rounded-md border" />            <figcaption className="text-muted-foreground mt-1.5 truncate text-xs">{name}</figcaption>          </figure>        ))}      </div>    </ScrollArea>  )}

Series thumbnails. Note the w-max on the inner row — without it, the flex row shrinks to fit the viewport and there is nothing to scroll.

Both axes

'use client'import { ScrollArea } from '@/registry/qure/ui/scroll-area'const columns = ['Lesion', 'Series', 'Slice', 'Long axis', 'Short axis', 'Baseline', 'Change', 'Reader']const rows = [  ['RUL nodule', 'Axial 1mm', '68', '8.1 mm', '6.4 mm', '8.0 mm', '+0.1 mm', 'A. Menon'],  ['LLL nodule', 'Axial 1mm', '124', '4.3 mm', '3.9 mm', '4.1 mm', '+0.2 mm', 'A. Menon'],  ['Hilar node', 'Axial 5mm', '41', '12.6 mm', '9.8 mm', '14.2 mm', '−1.6 mm', 'S. Iyer'],  ['Adrenal', 'Axial 5mm', '203', '17.0 mm', '15.1 mm', '16.8 mm', '+0.2 mm', 'S. Iyer'],  ['Pleural plaque', 'Coronal', '77', '22.4 mm', '3.2 mm', '22.1 mm', '+0.3 mm', 'A. Menon'],  ['Liver cyst', 'Axial 5mm', '188', '9.9 mm', '9.1 mm', '9.9 mm', '0.0 mm', 'H. Kaur'],]export default function ScrollAreaBoth() {  return (    <ScrollArea orientation="both" className="h-56 w-full max-w-md rounded-lg border">      <table className="w-max text-sm">        <thead>          <tr className="text-muted-foreground text-left text-xs">            {columns.map(c => (              <th key={c} className="px-3 py-2 font-medium whitespace-nowrap">{c}</th>            ))}          </tr>        </thead>        <tbody>          {rows.map(row => (            <tr key={row[0]} className="border-t">              {row.map((cell, i) => (                <td key={i} className="px-3 py-2 whitespace-nowrap">{cell}</td>              ))}            </tr>          ))}        </tbody>      </table>    </ScrollArea>  )}

A measurements table wider than the panel holding it. orientation="both" adds the corner between the two bars, so they do not overlap at the intersection.

Inside a card

Report — CT Thorax, ACC-100482
'use client'import { Button } from '@/registry/qure/ui/button'import { Card, CardContent, CardFooter, CardHeader, CardTitle } from '@/registry/qure/ui/card'import { ScrollArea } from '@/registry/qure/ui/scroll-area'export default function ScrollAreaCard() {  return (    <Card className="w-full max-w-md">      <CardHeader>        <CardTitle>Report — CT Thorax, ACC-100482</CardTitle>      </CardHeader>      <CardContent className="px-0">        {/* The scroll sits inside the card, not around it, so the header and            the sign-off row stay put while the report moves. */}        <ScrollArea className="h-48">          <div className="space-y-3 px-5 text-sm leading-6">            <p>              <strong>Technique.</strong> Non-contrast volumetric acquisition of the thorax at              1mm, reconstructed axially, coronally and sagittally.            </p>            <p>              <strong>Comparison.</strong> CT Thorax of 14 March 2025.            </p>            <p>              <strong>Findings.</strong> An 8mm solid nodule in the right upper lobe, unchanged              in size and morphology since the prior study. No new pulmonary nodule. No pleural              effusion. Mediastinal nodes are not enlarged by size criteria. The airways are              patent to the segmental level. No acute osseous abnormality.            </p>            <p>              <strong>Impression.</strong> Stable 8mm right upper lobe nodule; routine              surveillance at twelve months is appropriate.            </p>          </div>        </ScrollArea>      </CardContent>      <CardFooter className="justify-end">        <Button variant="tertiary" size="sm">Addendum</Button>        <Button size="sm">Sign</Button>      </CardFooter>    </Card>  )}

Put the scroll around the report body, not around the card: the title and the sign-off row should stay put while the text moves. The padding goes on the content inside the scroll area, so the scrollbar sits against the card's edge rather than floating in the middle of the inset.

Nothing here changes the tab order. A scrollable region with no focusable child is still unreachable by keyboard — give the viewport tabIndex={0} and an aria-label in that case, which is the one situation this component cannot decide for you.

API Reference

Prop

Type

The root also forwards everything Base UI's ScrollArea accepts, including overflowEdgeThreshold for fading the edges of a scrolled region.

On this page