Display
Scroll area
A box that scrolls, and says so: its edges fade where there's more, a thin scrollbar shows while you scroll or point at it, and a button offers the rest.
Installation
npx sinaui add scroll-areaCopy src/lib/ui/ScrollArea.svelte, scroll-edges.ts, Icon.svelte and tokens.css. No other dependencies.
For the fading edges alone on your own scrolling element, add {@attach scrollEdges} and data-fade (or data-fade="x" sideways).
Examples
Long text
The arrow appears while there's more below and moves most of a screen at a time; it leaves once you reach the end.
Reading room rules
- Wash your hands. Before touching any manuscript, and again after lunch.
- No ink at the desks. Take notes in pencil or on your own tablet; ink stays in the copyists’ room.
- Turn pages from the corner. Never from the spine: bindings from Baghdad are older than they look.
- Ask before copying. Copies of rare works are made by the house copyists, and credited to them.
- Borrow for forty days. Renew once from your reading list; bring the pledge you left when you return it.
- Keep maps flat. Al-Idrisi’s sheets are stored flat and read on the long tables only.
- Speak softly. Debate belongs in the courtyard; the reading room is for reading.
- Return by sunset. The room closes at the evening call; lamps are not allowed near the shelves.
- Report damage. Tell a librarian at once. Nobody is fined for honest reporting.
<script lang="ts">
import ScrollArea from '#lib/ui/ScrollArea.svelte';
const rules = [
['Wash your hands', 'Before touching any manuscript, and again after lunch.'],
[
'No ink at the desks',
'Take notes in pencil or on your own tablet; ink stays in the copyists’ room.'
],
[
'Turn pages from the corner',
'Never from the spine: bindings from Baghdad are older than they look.'
],
[
'Ask before copying',
'Copies of rare works are made by the house copyists, and credited to them.'
],
[
'Borrow for forty days',
'Renew once from your reading list; bring the pledge you left when you return it.'
],
['Keep maps flat', 'Al-Idrisi’s sheets are stored flat and read on the long tables only.'],
['Speak softly', 'Debate belongs in the courtyard; the reading room is for reading.'],
[
'Return by sunset',
'The room closes at the evening call; lamps are not allowed near the shelves.'
],
['Report damage', 'Tell a librarian at once. Nobody is fined for honest reporting.']
];
</script>
<div class="card">
<h3>Reading room rules</h3>
<ScrollArea label="Reading room rules" maxHeight="15rem">
<ol>
{#each rules as [title, body] (title)}
<li><strong>{title}.</strong> {body}</li>
{/each}
</ol>
</ScrollArea>
</div>
<style>
.card {
display: grid;
gap: 0.75rem;
inline-size: min(100%, 26rem);
}
h3 {
margin: 0;
font-size: 1rem;
}
ol {
display: grid;
gap: 0.75rem;
margin: 0;
padding-block: 0;
padding-inline: 1.5rem 0.25rem;
color: var(--ui-muted);
font-size: 0.875rem;
line-height: 1.55;
}
strong {
color: var(--ui-fg);
font-weight: 600;
}
</style>In a dialog
Anything someone should read before agreeing: the buttons stay in view while the text scrolls.
<script lang="ts">
import Button from '#lib/ui/Button.svelte';
import Dialog from '#lib/ui/Dialog.svelte';
import ScrollArea from '#lib/ui/ScrollArea.svelte';
let open = $state(false);
</script>
<Button variant="secondary" onclick={() => (open = true)}>Get a reader’s card</Button>
<Dialog bind:open title="Reader’s card" description="What the House of Wisdom keeps about you.">
<ScrollArea label="What we keep" maxHeight="10rem">
<div class="body">
<p><strong>Your name and city.</strong> Printed on the card, so librarians can greet you.</p>
<p><strong>What you borrow.</strong> Kept until each work is returned, then deleted.</p>
<p><strong>Your reading list.</strong> Only you can see it. Clear it any time.</p>
<p><strong>Nothing else.</strong> No tracking, no selling, no sharing with anyone.</p>
<p>
You can ask for everything we keep, or for all of it to be deleted, at the front desk or by
letter.
</p>
</div>
</ScrollArea>
{#snippet footer()}
<Button variant="secondary" onclick={() => (open = false)}>Not now</Button>
<Button onclick={() => (open = false)}>Get my card</Button>
{/snippet}
</Dialog>
<style>
.body {
display: grid;
gap: 0.75rem;
font-size: 0.875rem;
line-height: 1.55;
}
p {
margin: 0;
color: var(--ui-muted);
}
strong {
color: var(--ui-fg);
}
</style>Props
| Prop | Type | Default |
|---|---|---|
label | string (required) | |
maxHeight | string (required) | |
children | Snippet |
Accessibility
- The box is a named region that takes focus, so it scrolls with the arrow keys, Page Down and Space.
- The arrow button is for pointers and touch; keyboard users already scroll the region.
- The thin scrollbar is for pointers too: drag its thumb, or press above or below it to move a screenful. Scrolling itself stays native.
- With reduced motion, the button fades without rising and scrolling jumps instead of gliding.