Stretch Index

A list of rows that open in place. The row you're on grows taller, its tab widens into a photo, the title slides over to make room and the details rise out from under it, while the rows below glide down.

  • List
  • Hover
  • Layout

Uses motion, clsx, tailwind-merge

Inspired by State of AI in Design

Open in ChatGPTOpen in Claude

Customer stories

What changed after they switched

  • 01
    A bank that now ships every FridayHalcyon Bank

    Thirty engineers moved from quarterly release trains to weekly ones, without a rollback in the first year.

    52 releases in 2025Read the story
  • 02
    One booking system for forty restaurantsCommon Table

    Hosts used to juggle three tools a night. Now every table, waitlist and deposit runs through one screen.

    38% fewer no-showsRead the story
  • 03
    Checkout, rebuilt for the phone firstFern & Pilot

    Two thirds of orders come from phones. A one-page checkout took paying from three minutes to forty seconds.

    21% more completed ordersRead the story
  • 04
    Eleven designers, one libraryNorthstar

    Six drifting UI kits became one shared library that engineers pull from directly, so mockups and code finally match.

    6 kits down to 1Read the story
  • 05
    Code review in half the timeRelay

    A team across nine time zones switched to smaller pull requests and recorded walkthroughs, and stopped waiting a day for review.

    Reviews 2× fasterRead the story

Installation

Add the component on its own with the CLI, or copy the file in by hand.

The CLI writes components/ui/stretch-index.tsx and installs motion, clsx and tailwind-merge. It needs the @keyframeui registry in components.json; add it once per project.

npx shadcn@latest add @keyframeui/stretch-index

Examples

Finished sections built with Stretch Index. Each one installs whole, with the component included.

Open Roles

A careers page with `openOn="click"` and `numbered={false}`: rows open like an accordion, a second click closes them, and the Apply link inside stays clickable. In a narrow frame the team and place move under the title.

Open roles

5 positions

  • Senior Product DesignerDesign · Remote, Europe

    Own the editor from the first sketch to the release notes. You'll pair with two engineers and talk to customers every week.

    Apply €85–105k
  • Design EngineerDesign · Lisbon or remote

    Build the components our designers draw, and the motion that makes them feel right. React, TypeScript and a good eye.

    Apply €80–100k
  • Staff Backend EngineerEngineering · Lisbon

    Scale the sync engine that keeps 40,000 teams' documents in step. Go, Postgres, and careful thinking about conflicts.

    Apply €110–135k
  • Product Manager, BillingProduct · Remote, Europe

    Make paying us the easiest part of using us: plans, invoices, taxes in 30 countries, and the emails nobody wants to get.

    Apply €90–110k
  • Support LeadOperations · Lisbon

    Lead the six people who answer every ticket within four hours, and turn what you hear into fixes on the roadmap.

    Apply €60–72k

Podcast Episodes

A podcast's latest episodes on a dark panel, with square covers through `imageAspect={1}` and episode numbers through `number`. With `collapseOnLeave`, the list folds shut when the pointer leaves it.

Frame Rate

Latest episodes

Every other Tuesday

  • 24
    Springs, and why nothing should stop dead52 min

    With Ana Ruiz, motion lead at Tessel

    Why a spring that settles beats a curve that ends, and the three numbers Ana tunes first.

  • 23
    Designing the empty state41 min

    With Kofi Mensah, product designer at Tally

    The first screen most people see is the one with nothing on it. How to make it teach, not apologize.

  • 22
    Type that moves47 min

    With Lena Hart, type designer

    Variable fonts, kinetic headlines, and when animating letters helps reading instead of hurting it.

  • 21
    The case for fewer animations38 min

    With Sam Okoro, design director at Quill

    Sam cut 60% of his app's transitions and support tickets went down. What stayed, and why.

  • 20
    Prototyping in the browser55 min

    With Mira Chen, design engineer at Parcel

    Skipping the mockup and sketching straight in code: what you gain, and the habits that keep it fast.

Props

childrenRequired

TypeReact.ReactNode

The rows, as StretchIndexItems.

defaultIndex

Typenumber | nullDefaultnull

The row that starts open. null starts with every row closed.

index

Typenumber | null

The open row, or null for none. Set it with onIndexChange to control the list from outside.

onIndexChange

Type(index: number | null) => void

Called when a row opens, or with null when the open row closes.

openOn

Type"hover" | "click"Default"hover"

hover opens the row under the pointer once it slows down, and the row keyboard focus reaches. click waits for a click, tap or Enter, and a second one closes a row without a link.

collapseOnLeave

TypebooleanDefaultfalse

Close the open row when the pointer or keyboard focus leaves the list. Otherwise the last row opened stays open.

dim

TypebooleanDefaulttrue

Fade the other rows back while one is open and the pointer or keyboard focus is in the list.

numbered

TypebooleanDefaulttrue

Show a 01, 02, … column before the titles.

imageWidth

TypestringDefault"clamp(5rem, 24cqi, 11rem)"

Width of an open row's photo, as a CSS length. cqi is a percentage of the list's width.

imageAspect

TypenumberDefault4 / 3

Width-to-height ratio of an open row's photo. 1 for square covers.

className

Typestring

Classes for the list. Rows draw their dividers in the text color, so set text-* here to recolor the whole list.

item.titleRequired

TypeReact.ReactNode

StretchIndexItem: the row's title. It's laid out at its open width, so it never rewraps while the row opens.

item.meta

TypeReact.ReactNode

StretchIndexItem: short text at the end of the row, like a year, a company or a length. Moves under the title in lists narrower than 32rem.

item.number

TypeReact.ReactNode

StretchIndexItem: replaces the automatic 01, 02, … for this row.

item.href

Typestring

StretchIndexItem: makes the whole row a link. On touch, and in click lists, the first tap opens the row and the second follows the link.

item.image

Typestring

StretchIndexItem: photo URL. The tab widens into a frame around it as the row opens. Without one, the frame fills with the accent.

item.accent

Typestring

StretchIndexItem: the color the tab warms to on hover and open, and the frame's color before the photo shows. Defaults to the text color.

item.children

TypeReact.ReactNode

StretchIndexItem: details that rise out from under the title when the row opens. Links and buttons in here stay clickable.

item.className

Typestring

StretchIndexItem: classes for the row. It's a group/row with data-state="open" or "closed".