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.
<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.
<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.
<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.
<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>