Session review
NewA research team’s session screen: the interview’s recording with a scrub preview, a transcript lit as it is spoken, and an assistant to question it.
session-review/session-review.tsxInsights in the design process
To explore how experienced product designers navigate the full product development lifecycle, from the first research questions to the handoff with engineering, and where the process breaks down when deadlines get close.
Thanks for joining, Nora. Before we start, is it okay if we record this session for the team?
OROwen ReyesOf course, go ahead. Happy to help.
Nora ValeGreat. Could you give me a quick picture of what you work on day to day?
OROwen ReyesI lead design for the payments team — mostly checkout, invoicing and the merchant dashboard. Four product designers and one researcher.
Nora ValeAnd how long have you been in that role?
OROwen ReyesA bit over two years. Before that I was at an agency, so it was a big shift to own a product end to end.
Nora ValeWhat changed the most when you moved in-house?
OROwen ReyesDepth. At the agency we shipped and left. Now I live with every decision for years, so I care a lot more about why we build something before how.
Nora ValeYou’ve mentioned you’ve worked across different stages of the product — could you tell me a bit more about your role in discovery versus delivery?
OROwen ReyesSure! In most of my projects, I’ve been involved from the research phase — talking to users, mapping pain points, identifying opportunities — all the way to designing and refining solutions with engineers. I love that balance between strategy and craft.
Nora ValeThat’s great. How do you typically prioritize what to design next when there’s a lot on the roadmap?
OROwen ReyesI try to align closely with product and engineering leads to understand impact and feasibility, but I also keep a running list of the small paper cuts users mention. Those rarely make the roadmap on their own.
Nora ValeHow do those paper cuts get in, then?
OROwen ReyesWe bundle them. Every quarter we pick a theme, say refunds, and fix five or six small things at once. It’s easier to argue for one clear story than for twenty tiny tickets.
Nora ValeWhere does research fit when timelines get tight?
OROwen ReyesHonestly, it’s the first thing to get squeezed. We’ve started doing lighter checks — five-minute calls with merchants, or an hour with the support tickets — instead of skipping it altogether.
Nora ValeWhat helps the team stay in sync with engineering along the way?
OROwen ReyesA weekly design review that engineers actually attend. We look at work in progress, not polished screens, so problems surface while they’re still cheap to fix.
Nora ValeIf you could change one thing about how your team works, what would it be?
OROwen ReyesMore time with users after launch. We celebrate shipping, then move on — but the most useful feedback shows up three weeks later, when people stop being polite.
Nora ValeThat’s a great note to end on. Anything you’d like to add?
OROwen ReyesJust that I’d love to see what comes out of these sessions. It’s rare that someone asks how we actually work.
Nora ValeWe’ll share the summary with you. Thanks again, Nora.
OROwen Reyes
Sorrel AI
Solve your doubts in seconds
Chat with the recording and get
insights at the moment.
The live component, numbered part by part. Every measurement is read from it as it is drawn. Point at a row to outline its part, or at a part to find its row.
When to use
- A research team reviewing a recorded interview: watching it, reading along, and pulling out what matters.
- A recording whose words matter as much as its picture, so the transcript follows the time and every line seeks back to it.
- Notes that point at moments: summary, comments and insights each carry a timestamp that plays from there.
- Asking questions of one recording, with answers that cite the moments they come from.
When not to use
- A video to simply watch. Use a plain player; the tabs and the assistant only get in the way.
- Comparing many sessions across a study. Use a table of sessions with tags and filters, and open this from a row.
- A live call. This reviews a recording after the fact; a call needs its own controls and presence.
- Editing the recording into clips or a reel. Use a timeline editor with in and out points.
- A general chat assistant. This one only answers about the session on screen; use a chat thread for the rest.
You can add this component with the shadcn CLI or by hand:
Into components/session-review of any React project, with no setup:
npx shadcn@latest add https://brunodelorence.com/r/session-review.json- Copy the component files
Copy these files into one folder of your project, keeping their relative imports together, or download them as a ZIP:
- Import it
As shown under Usage, below.
Either way it needs React 19 and a bundler that supports CSS Modules, such as Next.js or Vite. It has a dark colour scheme: it follows your page’s color-scheme, light unless the page sets one, or its colorScheme prop, with nothing to configure. Browsers without light-dark() show it light.
Usage
import { SessionReview } from './session-review';export function SessionPage() { return ( <SessionReview autoplay={false} defaultTime={128} onSeek={(time) => console.log('seek', time)} /> );}Each state below is a live instance, locked so you can study it.
The pieces it is built from, each with its measurements read live.
Review a recorded user interview without leaving the screen. Play, scrub with a preview of any moment, change the speed or go full screen; the transcript lights the phrase being said and keeps its line in view, and every timestamp seeks. Tabs hold the summary, comments and insights, and an assistant panel answers questions about the recording, streaming its replies with timestamps that jump to the moment. The recording is illustrated: no photo of anyone.
- Nothing plays: the time is one number the screen owns. A timer adds a quarter of a second, times the speed, four times a second while playing, and holds while the screen is off-screen or the tab hidden. The scrub bar, both clocks, which figure talks and the transcript’s current line and phrase all read that number.
- The picture is two inline SVG scenes, the participant in her room and the researcher’s picture-in-picture against a brick wall. While playing, each figure breathes and nods by CSS transforms on its groups, and the one speaking moves its mouth; pausing pauses every animation at once through animation-play-state.
- The scrub bar is a role="slider" driven by pointer events with pointer capture: hovering shows a preview (a small copy of the scene, leaning as she did at that moment, with the hovered time over the total), pressing or dragging seeks. The played and hovered spans are full-width bars scaled from the left, so nothing is laid out again as time moves.
- The transcript lights the line being spoken and, inside it, the phrase being said, four words at a time, from how far time has gone between this line and the next. Each seek or new line scrolls its line to the top of the box; scrolling by hand lets go until the Follow button brings it back.
- The assistant’s answers are canned (data.ts): an answer is text and timestamps, split into tokens and revealed one every 38 ms; timestamps become chips that seek. The composer sends on Enter, adds the current time from its plus button, fakes a dictation from the microphone and has a menu for short answers or a new chat. The panel folds into a rail or closes, and the header brings it back.
- Layout is by container queries on the root, so it answers to its own width: the full screen from 1100 px, 945 px tall with the tab’s content scrolling inside; under that the assistant moves below the recording; under 640 px the sidebar becomes a bar across the top. The player’s controls answer to the player’s own width, dropping the volume, casting and remaining time when narrow.
- While on screen and untouched, a demo pointer plays the recording, previews and seeks on the scrub bar, jumps from a timestamp, asks the assistant and follows a timestamp in its answer, then walks the tabs and starts over. Any real input hands control to the reader.
- Every colour is a light-dark() token on the root, so the screen follows the page’s color-scheme or the colorScheme prop; the recording keeps its own colours in both.
| Prop | Type | Default | Description |
|---|---|---|---|
session | Session | SESSION | The session on screen: study, title, description, facts, length, the two people and the transcript (data.ts has the type and an invented interview). |
defaultTime | number | 128 | Where the recording opens, in seconds. |
defaultTab | 'summary' | 'transcript' | 'comments' | 'insights' | 'transcript' | The tab that opens. |
assistant | 'open' | 'folded' | 'closed' | 'open' | The assistant’s panel at first: open, folded into a rail, or closed. |
onSeek | (time: number) => void | — | Called whenever the recording seeks, with the new time in seconds. |
autoplay | boolean | true | While on screen and untouched, a demo pointer plays, previews and seeks, jumps from a timestamp, asks the assistant and walks the tabs. Real input hands control to the reader. |
colorScheme | 'light' | 'dark' | 'system' | — | Light or dark colours, or the reader’s system setting; without it, the page’s color-scheme. |
defaultState | { preview?: number; menu?: 'more' | 'share'; asked?: string; unfollowed?: boolean; labels?: boolean } | — | Opens in a given state: a scrub preview (a share of the length), the More menu or Share open, a suggestion answered in full, the transcript let go of the time, or the sidebar’s labels shown. Giving one keeps the demo pointer off. |
className | string | — | Extra class for the root. |
The complete source behind this demo. Open source under the MIT license, © 2026 Bruno Delorence: copy it, change it, ship it.
// Copyright (c) 2026 Bruno Delorence. MIT License.'use client';import { useEffect, useEffectEvent, useRef, useState, useSyncExternalStore, type KeyboardEvent,} from 'react';import { Assistant, tokenize, type Message } from './assistant';import { COMMENTS, INSIGHTS, REPLIES, SESSION, START, SUGGESTIONS, SUMMARY, clock, replyTo, segmentAt,// Copyright (c) 2026 Bruno Delorence. MIT License.'use client';import { useEffect, useRef, useState, type KeyboardEvent, type PointerEvent } from 'react';import { clock } from './data';import { Icon, PlayerGlyph } from './icons';import { GuestScene, HostScene } from './scene';import styles from './session-review.module.css';/* * The recording's player. Nothing plays: the time is a number the screen owns and advances on * its own clock, and the picture is the illustrated scene of scene.tsx, which moves while * `playing`. The controls are real ones: * * - The scrub bar is a role="slider": hovering it shows a preview of that moment (a small copy of * the scene, leaning as she did then) with its time over the total; pressing or dragging seeks, * and the arrow keys step 5 s, Page Up / Down 30 s, Home and End to either end. * - The volume is a native range input with its speaker as a mute button; the speed button steps * through 1×, 1.5×, 2× and 0.5×; casting is a toggle; full screen is the browser's own. * - A press on the picture plays or pauses, and a large play button shows while paused. */const SPEEDS = [1, 1.5, 2, 0.5];// Copyright (c) 2026 Bruno Delorence. MIT License.'use client';import { useEffect, useRef, useState } from 'react';import { SUGGESTIONS, clock, type ReplyPart } from './data';import { AppMark, Icon } from './icons';import styles from './session-review.module.css';/* * The assistant's panel: ask the recording a question. Empty, it shows its mark, a headline and * three suggestions above the composer; once asked, the conversation takes the room. Answers are * canned (data.ts) and stream in token by token; every timestamp in an answer is a chip that * seeks the recording there. * * - The composer is a textarea: Enter sends, Shift+Enter breaks the line. The plus button adds * the current time as a reference, the sliders button opens a menu (a shorter answer, a new * chat), and the microphone fakes a dictation into the field. * - The two buttons at the top fold the panel into a rail, or close it; the screen keeps the rest. */export type Message = | { id: number; role: 'user'; text: string } | { id: number; role: 'assistant'; tokens: ReplyPart[]; shown: number };// Copyright (c) 2026 Bruno Delorence. MIT License.'use client';import { useId } from 'react';import styles from './session-review.module.css';/* * The recording's pictures, all illustrated: no photo of anyone. The participant sits in a bright * room by a window; the researcher's tile shows him against a brick wall, a plant beside him. * Each figure breathes and sways while the recording plays, and its mouth moves while it is the * one speaking (data-speaking); the stylesheet holds every animation, paused with the recording * (data-playing on the player) and dropped under reduced motion. Gradient ids come from useId, so * any number of copies (the hover preview, the thumbnails) can share a page. *//** The participant, in her room. `lean` (−1 to 1) tilts her, for the preview of another moment. */export function GuestScene({ speaking = false, lean = 0, className,}: { speaking?: boolean; lean?: number;// Copyright (c) 2026 Bruno Delorence. MIT License./* * The session on screen: an invented research interview, its transcript, summary, comments and * insights, and the assistant's canned answers. Times are in seconds from the start of the * recording; every timestamp in the screen is a button that seeks to it. */export type Speaker = 'host' | 'guest';export type Segment = { at: number; speaker: Speaker; text: string };export type Person = { name: string; initials: string; role: string };export type Session = { /** The study it belongs to, and the participant's label in it. */ study: string; label: string; title: string; description: string; date: string; products: string; company: string; /** Length of the recording, in seconds. */// Copyright (c) 2026 Bruno Delorence. MIT License./* * The screen's icons: one 24-unit grid, drawn as strokes in currentColor, sized by the stylesheet * (the lab sizes an unsized SVG to 16 px). The player's glyphs (play, pause, the speaker) are * filled, as a player draws them. The app's mark and the assistant's are filled shapes of their * own. */export type IconName = | 'panel' | 'search' | 'home' | 'studies' | 'calendar' | 'folder' | 'people' | 'settings' | 'chevron' | 'more' | 'video' | 'cast' | 'expand' | 'minimize'// Copyright (c) 2026 Bruno Delorence. MIT License.'use client';import type { CSSProperties, Ref } from 'react';/* * The demo's pointer, and the steps that move it. A demo is a script of awaited steps: wait, or * glide the pointer somewhere. Stopping the demo (the reader takes over, or it scrolls away) ends * the step in progress at once: its promise rejects, which ends the script right there, and the * pointer fades out. * * Each glide is a curved flight played by the Web Animations API, so nothing runs frame by frame. * The points of the flight are handed to whoever wants to move along with it. The arrow and the * hand are HTML boxes that swap by their visibility: a change of style inside an SVG would lay * the SVG out again. */export type Point = { x: number; y: number };/** A glide's path and timing, for an animation that plays along with it. */export type Flight = { points: Point[]; timing: KeyframeAnimationOptions };export type Demo = {// Copyright (c) 2026 Bruno Delorence. MIT License.'use client';import { useCallback, useEffect, useRef, useState } from 'react';/* * When a demo may play on its own: while part of it is on screen and the tab is visible, never * under reduced motion, and not while the reader is using it. `take()` hands it to the reader; it * takes back over `idle` ms after their last input. */export function useAutoplay<T extends HTMLElement>(idle = 6000) { const ref = useRef<T>(null); const [visible, setVisible] = useState(false); const [reduced, setReduced] = useState(true); const [taken, setTaken] = useState(false); const timer = useRef(0); useEffect(() => { const element = ref.current; if (!element) return; let onScreen = false; const update = () => setVisible(onScreen && !document.hidden); const observer = new IntersectionObserver(/* Session review (session-review.tsx): a white research app with a pale icon sidebar, the recording in a rounded frame with white controls, a segmented tab bar, a transcript lit in pale blue where it is being spoken, and a pale grey assistant panel with a black send button. At night near-black, the recording unchanged. Colours are the tokens below, light-dark(light, dark) where the two differ: the screen takes the color-scheme it inherits from the page, or the one its colorScheme prop sets (data-color-scheme). Layout is by container queries on the root: the full screen from 1100 px (945 px tall; the tab's content scrolls inside), the assistant under the recording below that, the sidebar a bar across the top under 640 px. The player answers to its own width for its controls. Motion. The figures breathe, nod and talk by transforms on their SVG groups, paused with the recording; scrub previews, menus and the toast fade in. Reduced motion stills it all. */.root { --bg: #fff; --side: #fafafa; --panel: #fafafa; --field: #fff; --line: #efefef; --line-2: #e2e2e2; --line-3: #ececec;- Landmarks and headings: the sidebar is an aside with a nav, the screen a main with an h1, the breadcrumb marks the current page, and the assistant is a labelled section with an h2.
- The scrub bar is a role="slider" with its value as text ("02:08 of 08:24"): arrow keys step 5 s, Page Up and Down 30 s, Home and End go to either end. The volume is a native range input; play, mute, speed, casting and full screen are named buttons, with aria-pressed where they toggle.
- The tabs are a tablist with arrow keys, Home and End. The transcript is a focusable ordered list; the current line has aria-current="time", and every timestamp is a button named "Play from 02:34".
- The kebab menu and the composer’s settings are role="menu" with menuitem and menuitemcheckbox items: arrow keys move, Escape closes and returns to the button, a press outside closes them. Share opens a labelled dialog that Escape closes.
- The assistant’s conversation is a role="log" (aria-live polite, aria-busy while streaming); the toast and the player’s status (playing, paused, sought) are polite live regions that speak on change, not on every tick.
- Every control shows a :focus-visible ring. Reduced motion stills the figures, streams answers at once, scrolls the transcript without smoothing and turns the demo off. The scenes, avatars and the demo pointer are aria-hidden.