Source Code

<div class="demo">
  <div class="phone-frame">
    <div class="contacts-screen">
      <div class="contacts-header">Contacts</div>
      <div class="contacts-scroll" id="contactsScroll"></div>
      <div class="az-index" id="azIndex"></div>
      <div class="az-bubble" id="azBubble" hidden></div>
    </div>
  </div>
</div>

Alphabet Jump Index — Drag-to-Scrub A–Z Contacts-Style List Navigation

Alphabet Jump Index — Contacts-Style A–Z Scroll Navigation · Mobile · Plain HTML, CSS & JS · Live preview

What's included

Features

Both the scrollable list and the A-Z index are generated from one shared grouped-by-letter data structure, preventing any mismatch between them
Full 26-letter index always shown for consistent spatial layout, with letters lacking data simply dimmed and inert
True drag-to-scrub behavior using elementFromPoint on every pointer move, not just discrete per-letter tap targets
Large letter-preview bubble during drag, positioned away from the finger so it stays visible while dragging
Sticky per-letter group headers in the scrollable list stay pinned at the top while their group scrolls underneath
Smooth-scroll jump animation to each letter group rather than an abrupt instant scroll position change
Pointer Events with setPointerCapture ensure the drag continues tracking correctly even if the pointer moves briefly outside the narrow index strip

About this UI Snippet

Alphabet Jump Index — Building a Real Contacts-App-Style Fast Scroll

Screenshot of the Alphabet Jump Index — Contacts-Style A–Z Scroll Navigation snippet rendered live

Scrolling through a long alphabetically sorted list one screen-height at a time is slow. The alphabet index familiar from every phone's contacts app solves this by letting a user tap or drag along a thin A–Z strip to jump instantly to any letter — and getting the *drag* behavior right (not just tap) is what makes it feel native rather than like a decorative row of buttons.

One shared data structure drives both the list and the index

The groups object — contacts bucketed by first letter — is built exactly once, up front, and is what *both* the scrollable contact list and the A–Z index bar are generated from. This matters: if the list and the index were built from two separate pieces of logic, they could disagree about which letters actually have contacts (for example, the index showing "Q" as active/available when no contact's name actually starts with Q). Deriving both from the same groups object structurally prevents that kind of mismatch.

Every letter of the alphabet is shown, even ones with no contacts

The index renders all 26 letters, not just the ones with matching names — letters with zero contacts are simply dimmed and made inert (cursor: default, no jump target). This is a deliberate choice: an index that only shows *available* letters would constantly change its own visual layout and letter spacing depending on the dataset, making it much harder to reliably tap or drag to a specific letter's position from muscle memory. A full, fixed 26-letter strip stays visually and spatially consistent regardless of which letters happen to have data.

Dragging works via `elementFromPoint`, not per-letter event listeners

The key to real drag-to-scrub behavior: rather than relying on pointerenter/pointerleave events on each individual letter (which don't reliably fire correctly once a pointer has been captured by the container during a drag), the pointermove handler calls document.elementFromPoint(clientX, clientY) on every move to directly ask "what element is physically under the pointer right now" — then checks if that element is a letter in the index. This is what makes a single continuous drag gesture, from the top of the strip to the bottom, correctly and smoothly sweep through every letter it passes over, exactly like swiping a finger down a real phone's contacts index.

The large letter bubble gives feedback away from the user's own finger

While dragging, a large, semi-transparent bubble shows the currently-active letter positioned to the left of the index strip — not directly under the finger, where it would be physically obscured by the hand doing the dragging. This mirrors the exact feedback pattern real mobile contacts apps use, giving clear visual confirmation of which letter is currently selected without requiring the user to lift their finger to see it.

Build with AI

Build, Understand, Optimize, and Extend It With AI

Ask an AI assistant to explain why document.elementFromPoint() is the right technique for detecting which letter is under the pointer during a continuous drag, and why relying on individual pointerenter/pointerleave listeners per letter would behave incorrectly once the pointer has been captured by the container. It's also worth asking for a version that adds haptic feedback (via the Vibration API) each time the drag crosses into a new letter, or one that supports a live search filter combined with the alphabet index, hiding letters whose contacts have all been filtered out.

Prompt to recreate it

Copy this into your AI assistant of choice to build the effect from scratch, or as a jumping-off point for your own variant:

text
Build an alphabet jump-index list navigation component in HTML, CSS, and vanilla JavaScript using the Pointer Events API — no external library.

Requirements:
- A scrollable list of at least 30 names, grouped by first letter with a sticky header label for each letter group, alongside a narrow vertical A-Z index strip.
- Build both the scrollable grouped list AND the A-Z index strip from one single shared data structure (contacts grouped by first letter) computed once up front — do not maintain the list of "which letters have contacts" separately in two places.
- The index strip must display all 26 letters of the alphabet at all times for consistent, predictable spatial layout — letters with no matching contacts should be visually dimmed and functionally inert (no jump target), rather than being omitted from the strip entirely.
- Implement true drag-to-scrub navigation: on pointerdown and on every pointermove while dragging, use document.elementFromPoint() with the pointer's current coordinates to determine which letter element is physically underneath it right now, and smooth-scroll the list to that letter's group if it has one — this must work continuously as a single unbroken drag sweeps down the entire strip, not just as discrete taps on individual letters.
- While dragging, show a large, clearly legible letter-preview bubble positioned to the side of the index strip (not directly under the pointer, where it would be obscured), updating live to show whichever letter is currently active.
- Use setPointerCapture on pointerdown so the drag continues tracking correctly even if the pointer briefly moves outside the narrow index strip's bounds during a fast drag.

Want to tighten it up first? Run this prompt through the AI Prompt Studio to score it across 8 quality dimensions, catch anti-patterns, and tune the wording for Claude, ChatGPT, or Gemini before you paste it in.

Step by step

How to Use

  1. 1
    Tap a single letter in the A-Z stripInstantly scrolls the list to that letter's group, if any contacts exist for it.
  2. 2
    Press and drag up or down the stripA large letter bubble appears showing the currently-touched letter, and the list continuously jumps to match as you drag through each letter.
  3. 3
    Notice dimmed, inert lettersLetters with no matching contacts (like Q or X here) are shown dimmed and simply do nothing when tapped or dragged over — the index still shows the full alphabet for spatial consistency.
  4. 4
    Release the dragThe letter bubble disappears and the active-letter highlight in the index clears, leaving the list scrolled to wherever the drag ended.
  5. 5
    Adapt to your own datasetReplace the NAMES array with your own data — grouping, index generation, and drag scrubbing all work generically from whatever letters are actually present.

Real-world uses

Common Use Cases

CONTACTS
Contact lists and address books
The canonical use case — fast navigation through a long alphabetically sorted list of people.
DIRECTORY
Employee or member directories
Internal company directories or membership lists benefit from the exact same fast-scrub navigation pattern.
CATALOG
Alphabetical product or content catalogs
Glossaries, ingredient lists, or any alphabetically organized reference content with many entries.
Mobile-first long-list navigation
Any mobile UI with a long scrollable list benefits from a fast jump mechanism that avoids lengthy manual scrolling.
Related: Swipe Tab Switcher
See the Swipe Tab Switcher for a related mobile pattern worth pairing with this one.
Related: Mobile Boarding Pass Screen
See the Mobile Boarding Pass Screen for a related mobile pattern worth pairing with this one.
Related: Mobile Appearance Settings Screen
See the Mobile Appearance Settings Screen for a related mobile pattern worth pairing with this one.
Related: Mobile Delivery Order Tracking Screen
See the Mobile Delivery Order Tracking Screen for a related mobile pattern worth pairing with this one.

Got questions?

Frequently Asked Questions

A variable-length index that only shows available letters would change its own layout and letter positions depending on the dataset, making it much harder to reliably tap or drag to a specific letter from spatial memory. A fixed, full alphabet keeps the index visually and positionally consistent regardless of which letters actually have data.

On every pointermove event during a drag, the code calls document.elementFromPoint() with the pointer's current coordinates to directly ask which DOM element is physically underneath it right now, then checks whether that element is one of the index letters — this is what allows a single continuous swipe to sweep smoothly through every letter it passes.

Nothing happens for that specific letter — jumpToLetter() checks whether a matching group element exists in the DOM before attempting to scroll, and simply does nothing if it doesn't, since there's no valid scroll target for a letter with zero contacts.

A finger dragging along the strip would physically obscure any feedback placed directly underneath it. Positioning the bubble to the left keeps it clearly visible throughout the entire drag gesture, matching the same feedback placement real mobile contacts apps use.

Both are generated from the exact same groups object, built once from the source data before either is rendered — there is no separate, independently-maintained list of "available letters" that could drift out of sync with what the scrollable list actually contains.

Yes — the interaction is built entirely on Pointer Events, which unify mouse, touch, and pen input under one API, so clicking and dragging with a mouse produces the identical scrubbing behavior as a finger swipe on a touchscreen.