DocumentationComponentsTheme Creator
Theme CreatorGitHubIntroduction
InstallationUsageThemingMotion
Components
FieldFieldsetFormFile UploadColor PickerMediaTrimmer
AI agentsContributing
  1. Components
  2. MediaTrimmer

MediaTrimmer

A trim range drawn over a filmstrip, with an optional playhead and ruler — plus MediaSeekBar, a scrub bar with a hover preview frame. Three separately named sliders, full keyboard operation, and min/max selection length.

$ pnpm add @tessinaui/ui

When to use

  • A range cut from a clip. Use MediaTrimmer to pick the start and end of the part to keep, with the frames visible while you drag.
  • A single point in a clip. Use MediaSeekBar for the player scrub bar, with a preview frame as you hover or drag.
  • A selection with limits. Set minDuration and maxDuration and the handle you are dragging stops at the limit.
  • A playhead beside the trim. Pass playhead or defaultPlayhead so the trim range and the playback position stay in step.
  • A range that can be invalid. Pass errorMessage to say why, so both handles carry the reason.

When not to use

  • For a plain number range. A price or a volume has no frames to show. Use a Slider.
  • For a time of day or a window in a day. Use a TimePicker or a TimeRange.
  • For exact entry alone. Handles are for finding a cut by eye. Pair them with a typed field when someone needs to enter a precise time.
  • For a clip with no length to measure. Both components need a duration in seconds.

Installation

Terminal
npx shadcn@latest add https://tessinaui.com/r/media-trimmer.json

Usage

example.tsx
import { MediaTrimmer, MediaSeekBar } from "@tessinaui/ui";
example.tsx
<MediaTrimmer
  duration={30}
  value={range}
  onValueChange={setRange}
  defaultPlayhead={12}
  minDuration={1}
/>

MediaTrimmer picks a range out of a clip — the filmstrip trim every video editor ships. MediaSeekBar picks a point in one — the player scrub bar with a preview bubble every streaming app ships.

Neither is a Slider variant. The media rendering and the scrub mechanics are their own object: a slider has one value per thumb and no opinion about frames, while a trimmer has a selection, a playback position and a strip of images that all have to stay in step.

Not for a plain numeric range with no frames or playback position — use a Slider. Not for a coloured plate either: this is editor chrome, it has no on-color tone, and it paints page colours wherever it sits.

Examples

Default

A trim range with a playhead. Clicking the strip seeks; dragging a handle trims. The handles sit outside the selection, so the strip is inset by one handle's width on each side: at the clip bounds the handles stay inside the component.

Ruler and constraints

A longer clip with timecode ticks, and a selection held between 10 s and 3 min. In a narrow column the ruler draws fewer ticks rather than letting its labels collide. Both limits are enforced by moving the handle you are dragging — a trimmer that drags the far end along with your finger loses the edit you already made.

Sizes

Error

errorMessage explains why the selected range is invalid. It renders below the strip in a polite live region mounted from first render, marks both trim handles aria-invalid and describes them with the message — so a keyboard user moving a handle hears why — and implies intent="error" for the colour. Drag the selection past 15 seconds here.

Seek bar

A scrub bar with a buffered range and a preview bubble. previewAt returns the still for a position, or null when you have none — the bubble then shows the timecode alone rather than a broken image.

RTL

The timeline does not mirror under dir="rtl". Time runs left to right, the start handle stays on the left, and the arrow keys keep their physical meaning. dir="rtl" on MediaTrimmer and MediaSeekBar sets the direction around the track and defaults the names and the duration readout to Arabic. This example also passes its own group name, seek name and readout wording, which win over those defaults.

API Reference

MediaTrimmer

PropTypeDefaultDescription
durationnumber—Required. Total length of the media, in seconds
value[number, number]—Controlled selection, in seconds
defaultValue[number, number]whole clipUncontrolled initial selection
onValueChange(value) => void—Fires on every change
onValueCommitted(value) => void—Fires once when a drag ends, or immediately for a keyboard step
minDurationnumberstepShortest selection the handles may produce
maxDurationnumber—Longest selection the handles may produce
stepnumber0.1Granularity, in seconds
largeStepnumberstep × 10Step for PageUp/PageDown and Shift+arrow
playheadnumber—Controlled playback position. Omit both playhead props to hide it
defaultPlayheadnumber—Uncontrolled initial position
onPlayheadChange(time) => void—Fires when the playhead moves
framesMediaFrame[]—Stills to draw behind the track
showRulerbooleanfalseTick marks and timecodes above the strip
showDurationbooleantrueRead-out of the selected length below the strip
size"sm" | "md" | "lg""md"Strip height — 44 / 56 / 72px
intent"none" | "error" | "warning" | "success" | "info""none"Handle and selection colour
formatTime(seconds: number) => stringtimecodeHuman text for the read-out and aria-valuetext
labelstring"Trim media", Arabic under dir="rtl"Group name
handleLabels[string, string]["Trim start", "Trim end"], Arabic under dir="rtl"Names for the two trim handles
playheadLabelstring"Playback position", Arabic under dir="rtl"Name for the playhead
dir"ltr" | "rtl"—Sets the root's direction and picks the language of the names and the readout. The track stays left to right
stringsPartial<MediaTrimmerStrings>English, Arabic under dir="rtl"Override the readout's selected ("{duration} selected") and the default names: trimMedia, trimStart / trimEnd and playbackPosition. label, handleLabels and playheadLabel win
disabledbooleanfalse—
readOnlybooleanfalseFocusable, not changeable
errorMessageReactNode—Why the range is invalid. Announced politely, describes both trim handles and marks them aria-invalid; implies intent="error" unless another intent is set. Hidden while readOnly

MediaSeekBar

PropTypeDefaultDescription
durationnumber—Required. Total length, in seconds
valuenumber—Controlled playback position
defaultValuenumber0Uncontrolled initial position
onValueChange(time) => void—Fires on every change
onValueCommitted(time) => void—Fires once when a scrub ends
bufferednumber—How far the media has buffered, drawn behind the played fill
stepnumber1Granularity, in seconds
largeStepnumberstep × 10Step for PageUp/PageDown and Shift+arrow
previewAt(seconds) => string | null—Still for a hovered or scrubbed position. null shows the timecode alone
showPreviewbooleantrueSuppress the preview bubble entirely
size"sm" | "md" | "lg""md"Track thickness
intentMediaTrimmerIntent"none"Fill colour
formatTime(seconds: number) => stringtimecodeHuman text
labelstring"Seek", Arabic under dir="rtl"Accessible name
dir"ltr" | "rtl"—Sets the root's direction and picks the language of the default name. The track stays left to right
disabledbooleanfalse—

MediaFrame

FieldTypeDescription
srcstringImage URL
timenumberWhere in the media the frame came from
altstringUnused by the strip, which is decorative — kept for consumers reusing the type

formatTimecode

formatTimecode(seconds, precise?) — 0:09, 1:15, 1:01:01. With precise, keeps one decimal under ten minutes.

MediaTrimmerSkeleton · MediaSeekBarSkeleton

Placeholders whose boxes come from the components' own height maps, so the two cannot drift.

Accessibility

VoiceOver + Safari: passed 2026-09-14. The root reads "Trim media, group"; handles read "0:04.2, Trim start, slider" and "0:22.8, Trim end, slider", each arrow press reads the new time, and the playhead reads "0:12.0, Playback position, slider". NVDA and JAWS are untested.

  • The trimmer is a role="group" containing three separately named sliders: trim start, trim end, and playback position. They answer different questions, and a single control with modes could not tell a screen-reader user which one was about to move.
  • Every handle announces a timecode, not raw seconds — aria-valuetext is 1:15, or 1:15.0 when the step is finer than a second.
  • The filmstrip and the ruler are aria-hidden. They are visual scales, and every value they show is already in a handle's aria-valuetext; announcing them would add "image, image, image" and nothing else.
  • An errorMessage is never colour alone: both trim handles become aria-invalid and are described by the message, and the message lands in a polite live region that exists before it does, so it is announced when it appears. The playhead is left alone — playback position is never the invalid input.
  • Keyboard: arrows step, Shift+arrow and PageUp/PageDown take the large step, Home/End jump to the clip bounds.
  • Handles are 12px wide and the playhead 2px, because they have to sit on the cut, so each target is an invisible centred pad: 44×44 on a touch screen (WCAG 2.5.5) and 24×24 with a mouse (WCAG 2.5.8). The seek bar's target is a full-width band of the same heights, so the thumb meets the floor wherever it is. At the start or end of the clip a handle's touch pad reaches 16px past the component, so on a touch screen leave 16px between the trimmer and the edge of a scrolling container, or the page can scroll sideways.
  • Without frames the strip draws placeholder cells rather than collapsing, so the control does not change height when thumbnails decode and take the handles with it.

Time runs left-to-right at every dir. A filmstrip is a sequence of real frames in capture order, so mirroring the track under RTL would play the clip backwards to look correct. The surrounding labels and layout still follow the document direction.

Color PickerAlert

On this page

When to useWhen not to useInstallationUsageExamplesDefaultRuler and constraintsSizesErrorSeek barRTLAPI ReferenceMediaTrimmerMediaSeekBarMediaFrameformatTimecodeMediaTrimmerSkeleton · MediaSeekBarSkeletonAccessibility