Dots

Alphabet Index

An A–Z rail for long lists, with press, keyboard and finger scrubbing, and sticky section headings it scrolls to.

Favourites, 2 people

  • MK
    Maya Klein@maya
  • TG
    Theo Grant@theo

A, 2 people

  • AM
    Alex Morgan@alex
  • AL
    Ana Lima@ana.lima

B, 1 person

  • BC
    Ben Carter@ben

C, 1 person

  • CH
    Chris Hale@chris

D, 1 person

  • DF
    Dana Fox@dana

J, 1 person

  • JL
    Jordan Lee@jordan

M, 3 people

  • MR
    Marco Rossi@marco.r
  • MK
    Maya Klein@maya
  • MN
    Mila Novak@mila

N, 1 person

  • NH
    Noor Haddad@noor

S, 2 people

  • SP
    Sam Price@sam
  • SO
    Sofia Ortiz@sofia

T, 2 people

  • TW
    Tara Wells@tara.w
  • TG
    Theo Grant@theo

Z, 1 person

  • ZP
    Zoe Park@zoe

#, 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
└── AlphabetIndex

Features

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

PropTypeDefaultDescription
keys(string | AlphabetIndexKey)[]A–Z and "#"The keys of the rail. "#" is named "Other".
favouritesbooleanfalseAdds a star key first, with the id "favourites".
availablestring[]-Ids of keys with entries. Others are dimmed and disabled. Leave out to enable all.
valuestring | null-The current key (controlled).
defaultValuestring | nullnullThe initial current key (uncontrolled).
onChange(id: string) => void-Called when a key is chosen by press, keyboard or scrubbing.
scrollTargetReact.RefObject<HTMLElement | null>-The scroll container with the sections. Choosing a key scrolls its section to the top.
aria-labelstring"Jump to letter"Names the toolbar.
aria-labelledbystring-Names the toolbar by another element.
classNamestring-Additional classes to apply to the wrapper.

AlphabetIndexKey

A key object for keys.

FieldTypeDefaultDescription
idstring-Required. Matches the indexKey of a section.
labelstring-Required. The accessible name, such as "Other".
contentReact.ReactNodethe idWhat the rail shows.

AlphabetIndexSection

One group of the list. Renders a <section>.

PropTypeDefaultDescription
indexKeystring-Required. The rail key, such as "A", "#" or "favourites".
aria-labelstring-Names the section when it has no heading.
...propsReact.ComponentProps<"section">-Props spread to the section.

AlphabetIndexHeading

The sticky section label.

PropTypeDefaultDescription
level2 | 3 | 43Heading level in the page outline.
countReact.ReactNode-Shown at the end, such as "4 people".
classNamestring-Additional classes to apply.