- Accordion
- Action Bar
- Action Toast
- Alert Banner
- Alert Dialog
- Alert
- Alphabet Index
- Amount Field
- Aspect Ratio
- Attachment
- Availability List
- Avatar
- Badge
- Bank Card
- Breadcrumb
- Bubble
- Button Group
- Button
- Calendar
- Camera Frame
- Card Design Picker
- Card Details
- Card Peek
- Card Stack
- Card
- Carousel
- Chart
- Chat Thread
- Checkbox
- Chip Group
- Choice Tile
- Code Block
- Code Dots
- Collapsible
- Combobox
- Command
- Context Menu
- Copy Field
- Countdown
- Country Select
- Data Table
- Date Field
- Date Picker
- Date Range Picker
- Dialog
- Direction
- Document Paper
- Drawer
- Dropdown Menu
- Empty
- Entity Avatar
- Field
- File Trigger
- File Upload Item
- Hover Card
- Input Group
- Input OTP
- Input
- Item
- Kbd
- Key Value
- Keypad
- Label
- Live Region
- Marker
- Message Scroller
- Message
- Money Amount
- Native Select
- Number Field
- Pagination
- Percent Split Editor
- Phone Field
- Popover
- Progress
- QR Code
- Questionnaire
- Radio Group
- Range Histogram
- Rating
- Resizable
- Scroll Area
- Search Field
- Select
- Separator
- Share Bar
- Sheet
- Sidebar
- Skeleton
- Slider
- Sonner
- Sortable List
- Spinner
- Status Timeline
- Step Header
- Steps List
- Swipe Row
- Switch
- Table
- Tabs
- Text Field
- Textarea
- Tier Ladder
- Toggle Group
- Toggle
- Token Search Field
- Tooltip
- Trace
- Transaction Row
- Usage Meter
Favourites, 2 people
- Maya Klein@mayaMK
- Theo Grant@theoTG
A, 2 people
- Alex Morgan@alexAM
- Ana Lima@ana.limaAL
B, 1 person
- Ben Carter@benBC
C, 1 person
- Chris Hale@chrisCH
D, 1 person
- Dana Fox@danaDF
J, 1 person
- Jordan Lee@jordanJL
M, 3 people
- Marco Rossi@marco.rMR
- Maya Klein@mayaMK
- Mila Novak@milaMN
N, 1 person
- Noor Haddad@noorNH
S, 2 people
- Sam Price@samSP
- Sofia Ortiz@sofiaSO
T, 2 people
- Tara Wells@tara.wTW
- Theo Grant@theoTG
Z, 1 person
- Zoe Park@zoeZP
#, 1 person
- +1 415 555 0136Phone number#
"use client"
import * as React from "react"The AlphabetIndex is a vertical A–Z rail next to a long sorted list, such as contacts, recipients or countries. Press a letter, or slide a finger along the rail to scrub through the list; a bubble shows the letter under the finger. Pair it with AlphabetIndexSection and AlphabetIndexHeading to group the list and keep each letter's heading at the top while it scrolls. Use it for lists of more than a few screens; for short lists, or while the list is filtered by search, show the list alone.
Installation#
pnpm dlx shadcn@latest add @dots/alphabet-index
Usage#
import {
AlphabetIndex,
AlphabetIndexHeading,
AlphabetIndexSection,
} from "@/components/ui/alphabet-index"const listRef = React.useRef<HTMLDivElement>(null)
<div className="relative flex h-96">
<div ref={listRef} className="flex-1 overflow-y-auto">
<AlphabetIndexSection indexKey="A">
<AlphabetIndexHeading count="2 people">A</AlphabetIndexHeading>
<ul>{/* rows */}</ul>
</AlphabetIndexSection>
</div>
<AlphabetIndex
available={["A"]}
scrollTarget={listRef}
className="absolute end-0 top-1/2 -translate-y-1/2"
/>
</div>Composition#
Use the following composition to build an indexed list:
Wrapper
├── Scroll container
│ └── AlphabetIndexSection
│ ├── AlphabetIndexHeading
│ └── List rows
└── AlphabetIndexFeatures#
- A–Z and "#" by default, or your own keys with labels and custom content
- An optional favourites key with a star, placed first
- Keys without entries stay on the rail, dimmed and disabled, so the rail never jumps
- Finger and mouse scrubbing with a letter bubble; scrubbing over an empty letter moves to the next one with entries
- Scrolls only the list container to the chosen section, never the page or a sheet around it
- Sticky section headings with an optional count
- Controlled or uncontrolled current key
Scroll to sections#
Pass the scroll container as scrollTarget. Choosing a key scrolls the AlphabetIndexSection with the same indexKey to the top. Use value and onChange to keep the current key in your state, as in the example at the top of this page.
const listRef = React.useRef<HTMLDivElement>(null)
const [current, setCurrent] = React.useState("favourites")
<AlphabetIndex
favourites
available={available}
value={current}
onChange={setCurrent}
scrollTarget={listRef}
/>Available keys#
List the keys that have entries in available. The others stay on the rail, dimmed and disabled. Leave it out to enable every key.
Press or slide along the rail
"use client"
import * as React from "react"Custom keys#
Pass keys as strings or objects with an id, an accessible label and optional content. Give the rail its own name with aria-label when it is not about letters of people's names.
Countries starting with B
"use client"
import * as React from "react"Favourites#
Set favourites to add a star key first, with the id "favourites". Give the matching section indexKey="favourites".
<AlphabetIndexSection indexKey="favourites">
<AlphabetIndexHeading count="2 people">Favourites</AlphabetIndexHeading>
{/* rows */}
</AlphabetIndexSection>
<AlphabetIndex favourites available={["favourites", "A", "B"]} />Accessibility#
The rail is a React Aria Toolbar with vertical orientation, so arrow keys move along the keys and Tab leaves the rail. Each key is a React Aria Button; keys without entries are disabled and skipped. The current key has aria-current.
Name the rail#
The rail is named "Jump to letter" by default. aria-label and aria-labelledby go to the toolbar, not the wrapper.
<AlphabetIndex aria-label="Jump to country" />Label symbol keys#
A key whose id is not readable on its own needs a label, such as { id: "#", label: "Other" }. The star key is named "Favourites".
Name each section#
AlphabetIndexSection renders a <section> named by the AlphabetIndexHeading inside it. Without a heading, pass aria-label. Set the heading level to fit the page outline. The count is read after a comma, such as "A, 2 people".
Hide while filtering#
When the list is filtered by search, hide the rail. Its keys would no longer match the rows.
API Reference#
AlphabetIndex#
The rail. Renders a <div> around a React Aria Toolbar.
| Prop | Type | Default | Description |
|---|---|---|---|
keys | (string | AlphabetIndexKey)[] | A–Z and "#" | The keys of the rail. "#" is named "Other". |
favourites | boolean | false | Adds a star key first, with the id "favourites". |
available | string[] | - | Ids of keys with entries. Others are dimmed and disabled. Leave out to enable all. |
value | string | null | - | The current key (controlled). |
defaultValue | string | null | null | The initial current key (uncontrolled). |
onChange | (id: string) => void | - | Called when a key is chosen by press, keyboard or scrubbing. |
scrollTarget | React.RefObject<HTMLElement | null> | - | The scroll container with the sections. Choosing a key scrolls its section to the top. |
aria-label | string | "Jump to letter" | Names the toolbar. |
aria-labelledby | string | - | Names the toolbar by another element. |
className | string | - | Additional classes to apply to the wrapper. |
AlphabetIndexKey#
A key object for keys.
| Field | Type | Default | Description |
|---|---|---|---|
id | string | - | Required. Matches the indexKey of a section. |
label | string | - | Required. The accessible name, such as "Other". |
content | React.ReactNode | the id | What the rail shows. |
AlphabetIndexSection#
One group of the list. Renders a <section>.
| Prop | Type | Default | Description |
|---|---|---|---|
indexKey | string | - | Required. The rail key, such as "A", "#" or "favourites". |
aria-label | string | - | Names the section when it has no heading. |
...props | React.ComponentProps<"section"> | - | Props spread to the section. |
AlphabetIndexHeading#
The sticky section label.
| Prop | Type | Default | Description |
|---|---|---|---|
level | 2 | 3 | 4 | 3 | Heading level in the page outline. |
count | React.ReactNode | - | Shown at the end, such as "4 people". |
className | string | - | Additional classes to apply. |