Feature Card Row
"Popular features" / "Start from scratch"
Start from scratch
Popular features — enough cards to need the scroll affordance
What it is
A horizontally scrollable row of feature cards, used for both "Start from scratch" prompts and "Popular features" shortcuts. Each card is an entity-row's four slots — icon or thumbnail, title, description, trailing — turned on its side and stacked vertically into a card face, so the row and its menu-list cousins never drift apart in behavior.
Why it matters
It's the second thing a user sees on the home surface, right after the composer: a menu of what the product can do before they've typed anything. Descript, Zapier, CapCut and Spline all ship a version of this row, and all four solve the same tension — showing more options than fit on screen without hiding half of them behind a scrollbar nobody notices on a trackpad.
Observed in: Descript · Zapier · CapCut · Spline
Anatomy
- 1
feature-card-rowRoot — the scrollable region (Carousel), announced to AT via role="region". - 2
feature-card-row-itemOne card's slide wrapper; fixed width so cards don't reflow as the row scrolls. - 3
feature-card-row-cardThe card surface (shadcn Card) each item renders inside. - 4
feature-card-row-thumbnailFull-bleed image at the top of a card, present only in the with-thumbnail state. - 5
entity-rowComposed, not rebuilt — the card's icon/title/description/trailing, stacked vertically instead of in a row. - 6
feature-card-row-previousThe visible "scroll left" affordance the spec calls out — never optional chrome. - 7
feature-card-row-nextThe visible "scroll right" affordance; also what keeps the row reachable by keyboard.
How to use it
Reach for it on a home or empty-state surface when there are more actions or features than a single row can show without scrolling. Give every item in a row the same content shape — all icons or all thumbnails — pick icons for actions the user starts ("Start from scratch") and thumbnails for features that have a visual result worth previewing ("Popular features"). It is one component for both: the difference is entirely in the data you pass, not a variant prop.
Do
Wire onSelect on every item so the whole card is the click target, not just a link buried in the description.
Keep one content shape per row — all icon cards, or all thumbnail cards — so the row reads as one grid instead of a jumble.
Don't
Don't hand-roll a scrolling row with overflow-x-auto and no prev/next buttons — trackpad-only scroll hides half the content and nothing signals more cards exist.
Script to video
Turn a written script into a rough cut.
Clone a voice
Generate narration from a short sample.
Don't mix icon cards and thumbnail cards in the same row — pick one shape so the row doesn't look like two different components stitched together.
Accessibility
- Keyboard
- One tab stop per card, plus Previous and Next — and only for cards that have `onSelect`. An item without one composes `entity-row`'s non-interactive shape, which is a `div`: not focusable, not activatable, and invisible to everything below.
- Left and Right scroll the row, but only while focus is already inside it. The handler is a capture listener on the region and the region itself is not focusable, so the arrows work from a card or from Previous/Next and nowhere else. Both keys are `preventDefault`ed, so nothing inside a card can use them for anything else.
- Previous and Next disable themselves at the ends of the row, which removes them from the tab order — the number of tab stops changes as the user scrolls.
- There is no way to disable a card. `entity-row` takes a `disabled` prop and this component never passes it, so an unavailable feature has to be left out of `items` rather than dimmed.
- Screen reader
- Every card announces as a toggle button. `entity-row` renders `aria-pressed` whenever it has `onSelect`, so "Start from scratch" arrives as an unpressed toggle rather than as a button that opens something.
- A card's accessible name is its whole face read as one string — icon text, title, description and anything in `trailing`, concatenated. A card with a two-line description has a two-line name, which is the cost of making the whole card the target.
- The thumbnail is part of neither that name nor that target. It renders above `entity-row` and outside the button, so its `alt` is announced separately and clicking it does nothing — which is why a decorative-sounding alt costs more here than usual.
- The row announces as "carousel" through `aria-roledescription`, and each item as "slide", but neither has a name. The root is a `role="region"` with nothing labelling it, so it is a region that is never exposed as a landmark. Pass `aria-label` on `FeatureCardRow` to fix it — the component spreads your props onto that region.
- Previous and Next are named "Previous slide" and "Next slide" by visually-hidden text, and they are the only thing the row says about position. Nothing reports which card is on screen, or how many there are in total.
- Scrolling is silent. There is no live region anywhere, so pressing Next tells a listener that nothing changed.
- Focus
- `entity-row` supplies a card's focus ring and the shadcn `Button` supplies Previous and Next's. The region and the slide wrappers have none, and neither ever takes focus.
- Tab can put focus on a card that is scrolled out of view: the carousel positions with a transform and does not advance to follow focus, so a keyboard user can be working on a card they cannot see.
- Pressing Next does not move focus to a newly revealed card. Focus stays on the button — right for repeated presses, and it means the cards themselves are only ever reached by Tab.
Watch out for
- Reaching for a plain overflow-x-auto div instead of the Carousel base — that reimplements the keyboard-reachable region and visible next affordance the spec requires, and axe's scrollable-region-focusable rule already caught this exact class of bug once in this repo.
- Setting both
iconandthumbnailon the same item. Only the thumbnail renders (it sits above the entity-row and there's no icon slot left to fill) — pick one per item instead of relying on that precedence. - Shipping a thumbnail with a decorative-sounding alt like "feature preview" — the image is the only visual difference from the icon state, so screen reader users need the alt text to actually describe what the feature produces.
Installation
npx shadcn@latest add https://super-ai-components.vercel.app/r/feature-card-row.jsonRequires a shadcn app on Base UI (style base-nova). Radix-based styles such as new-york fail to typecheck: these components use Base UI's render prop and callbacks like onOpenChangeComplete, which Radix primitives do not have.