Skip to content
Layout

Scroll Area

A scroll container that looks like it belongs: thin, token-coloured bars that thicken under the pointer, and optional edge fades that say there is more beyond an edge — and only when there is.

Walkthrough

1. Basic

maxHeight is as tall as it grows before it scrolls. The bar is a hairline until the pointer is on it, and the scrolling is the browser's own — momentum and all.

  • Amina Yusuf

    Amina Yusuf

    Design
  • Dilan Karim

    Dilan Karim

    Engineering
  • Hevi Salih

    Hevi Salih

    Support
  • Omar Aziz

    Omar Aziz

    Engineering
  • Rozh Ahmed

    Rozh Ahmed

    Sales
  • Sara Hama

    Sara Hama

    Design
  • Zana Omer

    Zana Omer

    Operations
  • Lana Faraj

    Lana Faraj

    Finance
  • Karwan Ali

    Karwan Ali

    Engineering
  • Shilan Rashid

    Shilan Rashid

    Support
<script lang="ts">	import { Avatar, ScrollArea, Text } from 'omaris';​	const PEOPLE = [		['Amina Yusuf', 'Design'],		['Dilan Karim', 'Engineering'],		['Hevi Salih', 'Support'],		['Omar Aziz', 'Engineering'],		['Rozh Ahmed', 'Sales'],		['Sara Hama', 'Design'],		['Zana Omer', 'Operations'],		['Lana Faraj', 'Finance'],		['Karwan Ali', 'Engineering'],		['Shilan Rashid', 'Support']	] as const;</script>​<ScrollArea	maxHeight={240}	label="Team"	class="w-80 max-w-full rounded-shape-lg border border-border">	<ul class="flex flex-col py-1">		{#each PEOPLE as [name, team] (name)}			<li class="flex items-center gap-3 px-4 py-2">				<Avatar {name} size="sm" colorize />				<Text variant="body-md" class="min-w-0 flex-1" lines={1}>{name}</Text>				<Text variant="label-sm" tone="muted">{team}</Text>			</li>		{/each}	</ul></ScrollArea>

2. Edge fades

fade masks each edge the content has been scrolled away from, and only that edge — scroll down and the top fades in; reach the end and the bottom comes back sharp. A mask, so it works on any surface.

  1. 9:12 AM

    Order #4821 confirmed on WhatsApp

  2. 9:30 AM

    Menu updated — 3 prices changed

  3. 10:02 AM

    Courier picked up #4821

  4. 10:15 AM

    New review: five stars

  5. 10:41 AM

    Order #4822 confirmed on WhatsApp

  6. 11:05 AM

    Kahi with geymar marked sold out

  7. 11:20 AM

    Order #4823 cancelled by the customer

  8. 11:48 AM

    Courier picked up #4822

  9. 12:10 PM

    Lunch menu switched on

  10. 12:31 PM

    Order #4824 confirmed on WhatsApp

<script lang="ts">	import { ScrollArea, Text } from 'omaris';​	const EVENTS = [		['9:12 AM', 'Order #4821 confirmed on WhatsApp'],		['9:30 AM', 'Menu updated — 3 prices changed'],		['10:02 AM', 'Courier picked up #4821'],		['10:15 AM', 'New review: five stars'],		['10:41 AM', 'Order #4822 confirmed on WhatsApp'],		['11:05 AM', 'Kahi with geymar marked sold out'],		['11:20 AM', 'Order #4823 cancelled by the customer'],		['11:48 AM', 'Courier picked up #4822'],		['12:10 PM', 'Lunch menu switched on'],		['12:31 PM', 'Order #4824 confirmed on WhatsApp']	] as const;</script>​<ScrollArea	maxHeight="15rem"	fade	label="Today's activity"	class="w-80 max-w-full rounded-shape-lg bg-surface-container-low">	<ol class="flex flex-col gap-3 p-4">		{#each EVENTS as [time, text] (time)}			<li class="flex gap-3">				<Text variant="label-md" tone="muted" tabular class="w-16 shrink-0">{time}</Text>				<Text variant="body-sm">{text}</Text>			</li>		{/each}	</ol></ScrollArea>

3. Horizontal

orientation="horizontal" for a row that runs off the side, and both for a table bigger than its frame — sticky headers work, because the viewport is the scroller. The fades follow the reading direction, and the scrollbar lanes are kept out of the mask.

10:0011:0012:0013:0014:0015:0016:0017:0018:0019:0020:0021:00
Sat

0

5

10

15

20

2

7

12

17

22

4

9

Sun

7

12

17

22

4

9

14

19

1

6

11

16

Mon

14

19

1

6

11

16

21

3

8

13

18

0

Tue

21

3

8

13

18

0

5

10

15

20

2

7

Wed

5

10

15

20

2

7

12

17

22

4

9

14

Thu

12

17

22

4

9

14

19

1

6

11

16

21

Fri

19

1

6

11

16

21

3

8

13

18

0

5

<script lang="ts">	import { Chip, ScrollArea, Text } from 'omaris';​	const CATEGORIES = [		'Grills',		'Breakfast',		'Sandwiches',		'Rice dishes',		'Soups',		'Salads',		'Desserts',		'Juices',		'Hot drinks'	];	const DAYS = ['Sat', 'Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri'];	const HOURS = Array.from({ length: 12 }, (_, i) => `${i + 10}:00`);​	let chosen = $state(Object.fromEntries(CATEGORIES.map((c) => [c, c === 'Grills'])));</script>​<div class="flex w-full max-w-md flex-col gap-6">	<ScrollArea orientation="horizontal" fade label="Categories">		<div class="flex gap-2 pb-2">			{#each CATEGORIES as name (name)}				<Chip selectable bind:selected={chosen[name]}>{name}</Chip>			{/each}		</div>	</ScrollArea>​	<ScrollArea		orientation="both"		fade		maxHeight="12rem"		label="Orders by hour"		class="rounded-shape-lg border border-border"	>		<table class="border-separate border-spacing-0">			<thead>				<tr>					<td class="sticky start-0 top-0 z-20 bg-surface"></td>					{#each HOURS as hour (hour)}						<th scope="col" class="sticky top-0 z-10 bg-surface px-3 py-2">							<Text variant="label-sm" tone="muted" tabular>{hour}</Text>						</th>					{/each}				</tr>			</thead>			<tbody>				{#each DAYS as day, d (day)}					<tr>						<th scope="row" class="sticky start-0 z-10 bg-surface px-3 py-2 text-start">							<Text variant="label-md">{day}</Text>						</th>						{#each HOURS as hour, h (hour)}							<td class="px-3 py-2 text-center">								<Text variant="body-sm" tabular>{(d * 7 + h * 5) % 23}</Text>							</td>						{/each}					</tr>				{/each}			</tbody>		</table>	</ScrollArea></div>

4. Overridden

It ships no height, so class="h-56" is the height — no fight with a default. classes reaches the viewport and the content, and the root's data-overflow-top lifts a sticky header once the list has moved under it.

Menu

9

Chicken tikka

9,000 IQD

Kubba Mosul

7,500 IQD

Dolma

8,000 IQD

Masgouf

22,000 IQD

Tepsi baytinjan

10,000 IQD

Kahi with geymar

4,000 IQD

Lentil soup

3,000 IQD

Fattoush

5,000 IQD

Klecha

2,500 IQD

<script lang="ts">	import { Badge, ScrollArea, Text } from 'omaris';​	const ITEMS = [		['Chicken tikka', '9,000'],		['Kubba Mosul', '7,500'],		['Dolma', '8,000'],		['Masgouf', '22,000'],		['Tepsi baytinjan', '10,000'],		['Kahi with geymar', '4,000'],		['Lentil soup', '3,000'],		['Fattoush', '5,000'],		['Klecha', '2,500']	] as const;</script>​<ScrollArea	aria-label="Menu"	class="group/menu h-56 w-72 max-w-full rounded-shape-2xl bg-surface-container"	classes={{ viewport: 'scroll-pt-12', content: 'pb-2' }}>	<div		class="sticky top-0 z-10 flex items-center justify-between bg-surface-container px-4 py-3 transition-shadow duration-200 ease-standard group-data-overflow-top/menu:shadow-1 motion-reduce:transition-none"	>		<Text variant="title-sm">Menu</Text>		<Badge size="sm" variant="tonal">{ITEMS.length}</Badge>	</div>	{#each ITEMS as [name, price] (name)}		<div class="flex items-center justify-between gap-3 px-4 py-2">			<Text variant="body-md" lines={1}>{name}</Text>			<Text variant="body-sm" tone="muted" tabular>{price} IQD</Text>		</div>	{/each}</ScrollArea>