Skip to content
Navigation

Index Scrubber

The A-Z rail down the side of a long list — letters that magnify under the thumb, and a rail that condenses rather than overflowing.

Walkthrough

1. Basic

Drag the rail. The letters swell under the finger, the callout names the one you are on, and the list jumps to it as you go — groupByInitial shaped the data and the ids, and the scrubber found the scroller on its own.

A

Aland Barzani Aland Barzani
Amina Tofiq Amina Tofiq

B

Bahar Rasul Bahar Rasul

C

Chnar Salim Chnar Salim

D

Dilan Rashid Dilan Rashid

E

Evan Hoshyar Evan Hoshyar

F

Farah Jamil Farah Jamil

G

Goran Azad Goran Azad

H

Hawkar Sabir Hawkar Sabir

I

Ibrahim Latif Ibrahim Latif

J

Jwan Sirwan Jwan Sirwan

K

Karwan Aziz Karwan Aziz

L

Lana Abdullah Lana Abdullah

M

Mariam Salih Mariam Salih

N

Nasrin Hiwa Nasrin Hiwa

O

Omar Ali Omar Ali

P

Peshraw Diyar Peshraw Diyar

Q

Qasim Talib Qasim Talib

R

Rezan Shahen Rezan Shahen

S

Sara Mahmoud Sara Mahmoud

T

Tara Zana Tara Zana

U

Umed Kamaran Umed Kamaran

V

Viyan Soran Viyan Soran

W

Wria Halkawt Wria Halkawt

Y

Yusuf Karim Yusuf Karim

Z

Zainab Hassan Zainab Hassan
<script lang="ts">	import { Avatar, IndexScrubber, List, ListItem, Text, groupByInitial } from 'omaris';​	const NAMES = [		'Aland Barzani',		'Amina Tofiq',		'Bahar Rasul',		'Chnar Salim',		'Dilan Rashid',		'Evan Hoshyar',		'Farah Jamil',		'Goran Azad',		'Hawkar Sabir',		'Ibrahim Latif',		'Jwan Sirwan',		'Karwan Aziz',		'Lana Abdullah',		'Mariam Salih',		'Nasrin Hiwa',		'Omar Ali',		'Peshraw Diyar',		'Qasim Talib',		'Rezan Shahen',		'Sara Mahmoud',		'Tara Zana',		'Umed Kamaran',		'Viyan Soran',		'Wria Halkawt',		'Yusuf Karim',		'Zainab Hassan'	];​	const groups = groupByInitial(NAMES, (name) => name, { fill: true });	let active = $state<string | undefined>('A');</script>​<div class="relative w-full max-w-sm overflow-hidden rounded-shape-lg border border-border">	<!-- svelte-ignore a11y_no_noninteractive_tabindex -->	<div class="h-96 overflow-y-auto pe-5" tabindex="0" role="group" aria-label="Contacts">		{#each groups.filter((group) => group.items.length) as group (group.key)}			<Text				as="h3"				variant="label-md"				id={group.id}				class="sticky top-0 z-1 bg-surface-container/95 px-4 py-1.5 text-primary backdrop-blur-sm"			>				{group.label}			</Text>			<List variant="plain">				{#each group.items as name (name)}					<ListItem headline={name} density="compact">						{#snippet leading()}<Avatar {name} size="sm" />{/snippet}					</ListItem>				{/each}			</List>		{/each}	</div>​	<IndexScrubber sections={groups} bind:active class="absolute inset-y-1 end-0.5" /></div>

2. Condensed

Nowhere near the room for 27 letters. The rail drops to every second or third and puts a dot in the gap, the way a phone does — and the drag still addresses every letter, so nothing is lost by not being drawn.

A

Asha
Aoran

B

Bsha
Boran

C

Csha
Coran

D

Dsha
Doran

E

Esha
Eoran

F

Fsha
Foran

G

Gsha
Goran

H

Hsha
Horan

I

Isha
Ioran

J

Jsha
Joran

K

Ksha
Koran

L

Lsha
Loran

M

Msha
Moran

N

Nsha
Noran

O

Osha
Ooran

P

Psha
Poran

Q

Qsha
Qoran

R

Rsha
Roran

S

Ssha
Soran

T

Tsha
Toran

U

Usha
Uoran

V

Vsha
Voran

W

Wsha
Woran

X

Xsha
Xoran

Y

Ysha
Yoran

Z

Zsha
Zoran
<script lang="ts">	import { IndexScrubber, List, ListItem, Text, groupByInitial, ALPHABET } from 'omaris';​	const WORDS = ALPHABET.flatMap((letter) => [`${letter}sha`, `${letter}oran`]);	const groups = groupByInitial(WORDS, (word) => word, { fill: true });</script>​<div class="relative w-full max-w-sm overflow-hidden rounded-shape-lg border border-border">	<!-- svelte-ignore a11y_no_noninteractive_tabindex -->	<div class="h-44 overflow-y-auto pe-5" tabindex="0" role="group" aria-label="Contacts">		{#each groups as group (group.key)}			<Text as="h3" variant="label-md" id={group.id} class="px-4 py-1 text-primary">				{group.label}			</Text>			<List variant="plain">				{#each group.items as word (word)}					<ListItem headline={word} density="compact" />				{/each}			</List>		{/each}	</div>​	<IndexScrubber sections={groups} class="absolute inset-y-1 end-0.5" /></div>

3. Your own callout

The callout is a snippet, so it can carry more than a letter — a count, a label, a preview of the first row. bubbleShape picks any of the 36 outlines for the default one; this one is replaced outright.

A

Akre
Amedi

B

Baghdad
Basra

C

Chamchamal

D

Duhok

E

Erbil

H

Halabja

K

Karbala
Kirkuk

M

Mosul

N

Najaf

R

Ranya

S

Soran
Sulaymaniyah

Z

Zakho
<script lang="ts">	import { IndexScrubber, List, ListItem, Text, groupByInitial } from 'omaris';​	const CITIES = [		'Akre',		'Amedi',		'Baghdad',		'Basra',		'Chamchamal',		'Duhok',		'Erbil',		'Halabja',		'Karbala',		'Kirkuk',		'Mosul',		'Najaf',		'Ranya',		'Soran',		'Sulaymaniyah',		'Zakho'	];​	const groups = groupByInitial(CITIES, (city) => city);</script>​<div class="relative w-full max-w-sm overflow-hidden rounded-shape-lg border border-border">	<!-- svelte-ignore a11y_no_noninteractive_tabindex -->	<div class="h-72 overflow-y-auto pe-5" tabindex="0" role="group" aria-label="Contacts">		{#each groups as group (group.key)}			<Text as="h3" variant="label-md" id={group.id} class="px-4 py-1.5 text-primary">				{group.label}			</Text>			<List variant="plain">				{#each group.items as city (city)}					<ListItem headline={city} density="compact" />				{/each}			</List>		{/each}	</div>​	<IndexScrubber sections={groups} class="absolute inset-y-1 end-0.5">		{#snippet callout(section)}			<div				class="flex items-baseline gap-2 rounded-shape-md bg-inverse-surface px-4 py-2 text-inverse-surface-foreground shadow-4"			>				<span class="text-title-lg font-semibold">{section.label}</span>				<span class="text-label-sm opacity-70">{section.count} cities</span>			</div>		{/snippet}	</IndexScrubber></div>

4. Overridden

The overridden case: the rail on the leading edge, the magnification turned off for a plain one, and every part restyled through classes.

A

analytics

B

billing

C

couriers

D

delivery

E

exports

F

fraud

G

gateway

H

hours

I

invoices

K

kitchen

L

loyalty

M

menu
<script lang="ts">	import { IndexScrubber, List, ListItem, Text, groupByInitial } from 'omaris';​	const TAGS = [		'analytics',		'billing',		'couriers',		'delivery',		'exports',		'fraud',		'gateway',		'hours',		'invoices',		'kitchen',		'loyalty',		'menu'	];​	const groups = groupByInitial(TAGS, (tag) => tag);</script>​<div	class="relative w-full max-w-sm overflow-hidden rounded-shape-lg bg-surface-container-low ps-9">	<!-- svelte-ignore a11y_no_noninteractive_tabindex -->	<div class="h-64 overflow-y-auto" tabindex="0" role="group" aria-label="Contacts">		{#each groups as group (group.key)}			<Text as="h3" variant="label-md" id={group.id} class="px-4 py-1.5">{group.label}</Text>			<List variant="plain">				{#each group.items as tag (tag)}					<ListItem headline={tag} density="compact" />				{/each}			</List>		{/each}	</div>​	<IndexScrubber		side="start"		magnify={0}		bulge={0}		bubbleShape="pill"		bubbleSize={64}		sections={groups}		classes={{			rail: 'bg-inverse-surface/90 px-1.5',			row: 'text-inverse-surface-foreground/70',			dot: 'bg-inverse-surface-foreground/50'		}}		class="absolute inset-y-1 start-0.5"	/></div>