Theme
Sound
Jump to a component
AniCanonKit · Story
StoryScrubber
StoryScrubber moves through the story in story order (when things happen in the world, not when they were released).
Live, from @anicanon/sakuga 2.0.0
Sakuga for iOS and Android is coming
iOS
Web only
StoryScrubber is made for the web, and isn’t part of Sakuga for iOS yet.
Android
Web only
StoryScrubber is made for the web, and isn’t part of Sakuga for Android yet.
- Use it for
- Moving through the story in story order, when things happen in the world: a column per event under arc brackets, and a handle that names the moment ("After Fire at Sé").
- Not for
- The playhead of an episode or a shortScrubber
- The whole cut at a glanceProgramBar
- The cut's tracks in timeTimeline
- The story's people, places and events as a mapStoryMap
Guidelines
- One column per event, grouped under arc brackets. Under each event, marks show what told it (episode, manga, short, Studio).
- The handle sits after an event and names it ("After Fire at Sé"), or "Now" at the end. Two handles compare two moments; the track moves the first, and the second is said with it ("After Fire at Sé, compared with Now").
- Events with no place yet wait in the Not placed box at the end.
- The track is a slider from 0 to the number of events, at the first handle. A screen reader says
valueText, or the first handle's label when it is left out. onChange(at)makes the track move by keyboard (arrows a step, Page Up and Page Down a tenth of the story, Home and End to either end);onFilter(filter)makes the filter buttons work. The events and arcs drawn in the track are listed beside it for screen readers, in story order.
import { StoryScrubber } from '@anicanon/sakuga/anicanonkit';
export default function StoryScrubberExample() {
return (
<StoryScrubber
events={[
{ type: 'event', inside: true, told: ['play'] },
{ type: 'twist', inside: true, told: ['manga'] },
{ type: 'revelation', told: ['play'] },
]}
arcs={[
{ name: 'Arc 1', count: 2, on: true },
{ name: 'Arc 2', count: 1 },
]}
handles={[{ at: 2, label: 'After Arthur taken' }]}
fill={[0, 2]}
unplaced={{ state: 'none' }}
filter={false}
/>
);
}PropsStoryScrubberProps
eventsrequired{ type: EventType; title?: string; told?: IconName[]; inside?: boolean; mark?: boolean; }[]arcsrequired{ name: string; count: number; on?: boolean; }[]handlesrequired{ at: number; label: string; }[]fillrequired[number, number]unplaced{ state?: 'event' | 'none'; on?: boolean; icon?: IconName; }filterstring | falseonChange(at: number) => voidArrow keys, Page and Home/End on the track: the new place, 0 to the number of events.
onFilter(filter: string) => voidA filter button pressed: all, ep, manga or shorts.
valueTextstringWhat a screen reader says for the moment shown; the first handle's label when left out.
classNamestringAn IconName is any of the names Icon lists.
Types
type EventType = 'event' | 'twist' | 'revelation' | 'clue' | 'goal';
Each token reads a role, and the role lands on a value in each theme. Change the role and every component that reads it follows.
Colour
scrub-trackStoryScrubber: the story-order line.bg-fillshade-a12rgba(11,10,16,0.12)bg-fillwhite-a14rgba(255,255,255,0.14)scrub-fillStoryScrubber: the stretch included at the handle.accentcamellia-600#c2335faccentcamellia-400#f4779fscrub-handleStoryScrubber: handle and its pill.accentcamellia-600#c2335faccentcamellia-400#f4779fscrub-handle-fgStoryScrubber: handle pill label.on-accentneutral-0#ffffffon-accentneutral-925#0b0a10scrub-node-bgStoryScrubber: event node fill.bg-canvasneutral-50#f6f6f8bg-canvasneutral-950#07070ascrub-node-ringStoryScrubber: event node ring.border-dividershade-a12rgba(11,10,16,0.12)border-dividerwhite-a12rgba(255,255,255,0.12)scrub-node-ring-onStoryScrubber: ring of an included event.border-controlshade-a52rgba(11,10,16,0.52)border-controlwhite-a45rgba(255,255,255,0.45)scrub-arcStoryScrubber: arc brackets.border-dividershade-a12rgba(11,10,16,0.12)border-dividerwhite-a12rgba(255,255,255,0.12)scrub-labelStoryScrubber: arc names, told-by marks and key.text-mutedneutral-600#5c5b67text-mutedneutral-400#a3a3a7scrub-label-onStoryScrubber: an arc inside the moment.text-secondaryneutral-800#2e2d36text-secondaryneutral-200#dcdcdescrub-unplaced-borderStoryScrubber: the Not placed box.border-placeholdershade-a24rgba(11,10,16,0.24)border-placeholderwhite-a22rgba(255,255,255,0.22)Space and shape
scrub-gapgap-lg12px
scrub-padding-topinset-xl24px
scrub-row-gapgap-xl16px
scrub-track-gapgap-sm6px
scrub-line-topinset-sm8px
scrub-fill-topinset-sm8px
scrub-fill-heightits own value
4px
scrub-col-gapgap-sm6px
scrub-col-out-opacityopacity-disabled0.35
scrub-node-sizeits own value
20px
scrub-node-ring-widthline-strong2px
scrub-col-node-in-ring-widthline-strong2px
scrub-node-mark-ring-widthline-strong2px
scrub-node-unplaced-border-widthline-hairline1px
scrub-told-gapgap-2xs2px
scrub-handle-topits own value
calc(-1 * 2px)
scrub-handle-border-left-widthline-strong2px
scrub-handle-translate-xits own value
calc(2px / -2)
scrub-pill-topits own value
calc(-1 * 24px)
scrub-pill-heightits own value
20px
scrub-pill-padding-xinset-sm8px
scrub-pill-radiusradius-full9999px
scrub-arc-padding-xinset-xs4px
scrub-arc-gapgap-xs4px
scrub-bracket-heightits own value
4px
scrub-bracket-border-widthline-hairline1px
scrub-bracket-radiusradius-xs4px
scrub-arcname-padding-xinset-xs4px
scrub-unplaced-widthits own value
92px
scrub-unplaced-gapgap-xs4px
scrub-unplaced-out-opacityopacity-disabled0.35
scrub-unplaced-box-out-inset-ring-widthline-strong2px
scrub-box-gapgap-sm6px
scrub-box-padding-topinset-sm8px
scrub-box-padding-bottominset-xs4px
scrub-box-radiusradius-sm8px
scrub-box-inset-ring-widthline-strong2px
scrub-none-heightits own value
40px
scrub-foot-gapgap-xl16px
scrub-key-gapgap-lg12px
scrub-key-span-gapgap-xs4px
scrub-told-icon-sizeicon-size-xs14px
scrub-key-icon-sizeicon-size-xs14px
scrub-forced-edge-widthline-hairline1px
scrub-track-focus-outline-offsetfocus-ring-offset3px
scrub-track-focus-radiusradius-xs4px
Type
scrub-line-heightline-strong2px
scrub-pill-fonttext-micro700 12px/16px "Funnel Sans", system-ui, -apple-system, "Segoe UI", sans-serif
scrub-arcname-fonttext-small-strong600 12px/16px "Funnel Sans", system-ui, -apple-system, "Segoe UI", sans-serif
scrub-none-fonttext-small-strong600 12px/16px "Funnel Sans", system-ui, -apple-system, "Segoe UI", sans-serif
scrub-key-fonttext-small-strong600 12px/16px "Funnel Sans", system-ui, -apple-system, "Segoe UI", sans-serif
Motion
scrub-col-opacity-durationmotion-base240ms
scrub-col-easingease-standardcubic-bezier(0.2, 0, 0, 1)
scrub-col-box-shadow-durationmotion-fast160ms
scrub-node-durationmotion-fast160ms
scrub-node-easingease-standardcubic-bezier(0.2, 0, 0, 1)
Checked automatically with axe-core 4.11.4 against WCAG 2.2 A and AA, on the live preview above, in light and dark, on 26 October 2026.
- Result
- No issues found, in light or in dark.
- By design
- A slider that Tab reaches, named "Show the world at a moment in the story" and read as
valueText(else the first handle's label, else "After event 3 of 12"); withonChangearrows move it an event, Page Up and Page Down a tenth of the story and Home and End to either end, and without it it reads as read-only. Beside it, screen readers get the events in story order (type, title, what told it, "in the moment shown") and the arcs as lists, and below it a group named "Show events told by" holds filter buttons that read "pressed" on the current one and runonFilter. - Not tested
- The automated check doesn't test keyboard use, focus order or what a screen reader says. The lines above are the design system's own account of them.