Frame Strip
Keyframe / page / artboard strip
Video frames
Slide pages
In / out picker
Reorder
What it is
A horizontal strip of frames, pages or artboards, built on preview-tile. One item is active at a time; items can be reordered and new ones added inline. An in/out variant swaps that single selection for two marks — a first and a last frame — which is how a clip range or an image-to-video conditioning pair gets picked.
Why it matters
Descript and CapCut put a frame strip under the player, Canva puts a page strip under the canvas, and Simplified puts an artboard strip beside it — three different products, one interaction. Freepik's frame conditioning uses the same strip to pick the first and last frame of a generated clip. Shipping three separate strips would mean three selection models and three keyboard stories for what users experience as the same control, so this is one component whose `kind` changes only what the tile contains. The active item is drawn with a ring rather than a border for a specific reason: a border is layout, so a bordered strip nudges every neighbouring tile by a few pixels each time the selection moves, and a strip that twitches as you arrow through it is unusable at frame-picking speed.
Observed in: Descript · CapCut · Canva · Simplified · Freepik
Anatomy
- 1
frame-stripRoot — the scrollable region (Carousel), labelled for the content kind. - 2
frame-strip-itemOne cell. Its height is the same whether or not the item is active. - 3
frame-strip-frameThe tile: a preview-tile, wrapped in a button in the select variant and inert in the in/out variant. - 4
preview-tileA8, composed rather than reimplemented — it owns the aspect, the label overlay, the badge corner and the selection ring. - 5
frame-strip-markIn/out variant only: the "In" or "Out" badge in the tile's badge corner. - 6
frame-strip-inToggle that marks this item as the in point. Its accessible name names the frame. - 7
frame-strip-outToggle that marks this item as the out point. - 8
frame-strip-moveReorder controls; revealed on hover or focus, disabled at each end of the strip. - 9
frame-strip-addTrailing add tile, in the same cell geometry as a frame. - 10
frame-strip-previousVisible scroll-left affordance; also what keeps the strip keyboard-reachable. - 11
frame-strip-nextVisible scroll-right affordance.
How to use it
Reach for it whenever a timeline, deck or canvas needs a row of navigable thumbnails. Set `kind` to video, slides or artboards — it changes the tile aspect and the wording of the add tile, nothing else. Pass `items` with a `label` on every entry (a timecode, a page number, an artboard name): the label is the tile's accessible name, and the thumbnail is decorative. Add `onReorder` to turn on the move controls and `onAdd` to append the add tile. Switch to `variant="in-out"` when the question is a range rather than a position — that variant marks two items, feeds them to reference-strip as first and last frame, and keeps the in point ahead of the out point for you.
Do
Use one strip for frames, pages and artboards, changing only
kind— selection, reorder and add should behave identically across all three.Give every item a real text label; leave the thumbnail decorative with an empty alt so the label is the tile's accessible name.
In the in/out variant, mark both ends in words — the In and Out badges say which is which without relying on the ring or a colour.
In00:00:00
00:00:04
Out00:00:08
Wire
onReorderrather than hand-rolling drag-only reordering; the move controls appear on hover and stay in the tab order for keyboard users.
Don't
Don't draw the active item with a border — a border is layout, and the strip shifts under the cursor every time the selection moves.
Don't ship frames identified only by their picture; without a label there is nothing to announce and nothing to tell two similar frames apart.
Accessibility
- Keyboard
- Do the arithmetic before shipping a long strip. A `select` frame is one stop; an `in-out` frame is two, the In and Out toggles; `onReorder` adds two more to every frame; the add tile is one; previous and next are two. Twelve frames with in/out marks and reorder is fifty tab stops.
- Left and Right scroll the strip. The Carousel base handles them on a capture-phase listener at the root and calls `preventDefault`, so they never move the selection or focus — and any control you nest inside the strip loses its own horizontal arrow keys to them.
- There is no roving tabindex, so Tab is the only way through the frames, and nothing marks off-screen items inert: Tab walks into frames that are scrolled out of view.
- The reorder controls are hidden with `opacity-0` rather than `display:none` precisely so they stay tabbable, and `group-focus-within` reveals them when Tab arrives. They are `disabled` at each end of the strip.
- Selecting a frame is Space or Enter on the frame button. There is no Delete, no Backspace and no Escape — and in the `in-out` variant the frame is not a control at all, so there is nothing to activate but the two toggles beneath it.
- Screen reader
- The strip is a `role="region"` with `aria-roledescription="carousel"`, named from `kind` — "Frames", "Pages" or "Artboards". Each cell is a `role="group"` announced as a slide.
- A frame's accessible name is its `label` and nothing else, which is why the prop is required: the thumbnail is decorative, and a frame identified only by its picture announces as an empty button. The active frame carries `aria-current`, not `aria-pressed` — "the frame you are on", not a toggle.
- Anything else inside the frame joins that name. A `badge` you pass, and the "Couldn't load" text a `failed` tile renders, both sit inside the button, so a failed frame announces as "Couldn't load 00:12".
- A `loading` frame announces nothing beyond its label. The pulsing skeleton is presentational and the state reaches the DOM only as `data-state`, so "still rendering" is a purely visual distinction.
- The In and Out toggles carry `aria-pressed` and name themselves after the frame — "In point at 00:12" — with the visible "In" `aria-hidden` so the two never concatenate. That is what stops frame 3's pair being indistinguishable from frame 4's in a list of controls.
- Setting an in point past the out point silently clears the far mark. There is no live region anywhere in the strip, so a mark the component removed on your behalf is announced as nothing.
- Focus
- Reorder is the trap. Pressing "Move left" repeatedly walks an item toward the front, and the moment it lands at index 0 that same button becomes `disabled` — focus is dropped and falls to `<body>` mid-reorder. Move focus to the item's other move control, or to the frame, inside your `onReorder`.
- Selecting a frame does not move focus, and previous/next only scroll — so the focused frame and the visible frames can disagree.
- The frame button and the add tile ship their own `focus-visible` ring. In, Out, the move controls and previous/next inherit the shared Button styles.
Watch out for
- Adding padding, a border or a size change to the active tile alongside the ring. The ring is a box-shadow and costs no layout; anything else you attach to the active state reintroduces exactly the reflow the ring was chosen to avoid.
- Reimplementing the thumbnail instead of composing preview-tile. The tile is what guarantees a fixed aspect and stable geometry across loading and failed items — a hand-rolled div re-derives all of it, usually without the failed state.
- Putting a control inside the tile's action slot while the tile is also clickable. That slot renders inside preview-tile's frame, so a button there sits inside a button and fails axe's nested-interactive rule; this component wraps an inert tile in its own single button instead.
- Hiding the reorder controls with display:none until hover. They must stay in the tab order — reveal them with opacity plus focus-within, or keyboard users can never reorder anything.
- Assuming the in/out marks can be set in any order. An in point after the out point is not a range, so setting one past the other clears the far end rather than keeping a pair that downstream conditioning cannot use.
Installation
npx shadcn@latest add https://super-ai-components.vercel.app/r/frame-strip.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.