Theme
Sound
Jump to a component
AniCanonKit · Story
StoryMap
StoryMap draws the story as hubs and satellites: characters (portraits) and places (art) are hubs; events are small shapes between the hubs they involve, linked to each.
Live, from @anicanon/sakuga 2.0.0
Sakuga for iOS and Android is coming
iOS
Web only
StoryMap is made for the web, and isn’t part of Sakuga for iOS yet.
Android
Web only
StoryMap is made for the web, and isn’t part of Sakuga for Android yet.
- Use it for
- The story drawn as hubs and satellites: characters and places as hubs, events as small shapes between the ones they involve, and a selection's causes and conflicts.
- Not for
- Moving through the story in story orderStoryScrubber
- The same characters, places and events as rows in a panelEntityRow
- The open questions the audience waits onQuestionRow
Guidelines
- States sit on hubs as badges. A selection lights its hubs (
focus) and dims the rest (dim); events after the moment areghost. - A proposed event (being added) is drawn dashed in the accent; a just-added one is
newwith a New badge. arcsdraw causes and conflicts of the selection;dashedmarks a conflict.- The map keeps the size it is drawn at (
width,height). In a wider box it sits in the middle; the legend, the chip and the tools keep the box's corners. - Positions are given by the page. Never draw every link at full weight.
onSelect(id, kind)makes each hub and event a button; all of them are one Tab stop that the arrows move through.onZoomIn,onFitandonChipRemovemake the tools and the chip's close work.
import { StoryMap } from '@anicanon/sakuga/anicanonkit';
export default function StoryMapExample() {
return (
<StoryMap
width={520}
height={300}
tools={false}
hubs={[
{
id: 'a',
kind: 'character',
x: 120,
y: 120,
size: 56,
initial: 'S',
label: 'Smith',
state: 'focus',
},
{
id: 'b',
kind: 'place',
x: 380,
y: 160,
label: 'Sé station',
badge: { label: 'Burning', variant: 'alert', icon: 'trait-fierce' },
state: 'focus',
},
]}
events={[
{
id: 'e',
x: 250,
y: 110,
type: 'event',
label: 'Fire at Sé',
state: 'focus',
},
]}
edges={[
{ x1: 250, y1: 110, x2: 120, y2: 120, tone: 'accent' },
{ x1: 250, y1: 110, x2: 380, y2: 160, tone: 'accent' },
]}
/>
);
}PropsStoryMapProps
hubsrequiredStoryMapHub[]eventsrequiredStoryMapEvent[]edgesrequiredStoryMapEdge[]heightnumberwidthnumberlabelstringchipstringtoolsbooleanlegendbooleanonSelect(id: string, kind: 'hub' | 'event') => voidMakes each hub and event a button, all one Tab stop.
onZoomInMouseEventHandleronFitMouseEventHandleronChipRemoveMouseEventHandlerThe chip's close.
arcsStoryMapArc[]childrenReactNodeDrawn over the map, after the events.
classNamestringTypes
interface StoryMapHub { id: string; kind: 'character' | 'place'; x: number; y: number; size?: number; image?: string; initial?: string; label: string; badge?: BadgeSpec; state?: 'focus' | 'dim'; }
interface StoryMapEvent { id: string; x: number; y: number; type: EventType; label: string; state?: 'focus' | 'dim' | 'ghost' | 'proposed' | 'new'; unplaced?: boolean; badge?: BadgeSpec; }
interface StoryMapEdge { x1: number; y1: number; x2: number; y2: number; tone?: 'accent'; dim?: boolean; dashed?: boolean; }
interface StoryMapArc { x1: number; y1: number; x2: number; y2: number; dashed?: boolean; bend?: number; }
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
map-bgStoryMap: ground.bg-surfaceneutral-0#ffffffbg-surfaceneutral-900#111117map-borderStoryMap: outline and legend outline.border-hairlineshade-a08rgba(11,10,16,0.08)border-hairlinewhite-a06rgba(255,255,255,0.06)map-edgeStoryMap: a link between an event and a hub.border-dividershade-a12rgba(11,10,16,0.12)border-dividerwhite-a12rgba(255,255,255,0.12)map-edge-activeStoryMap: links, causes and conflicts of the selection.accentcamellia-600#c2335faccentcamellia-400#f4779fmap-hub-ringStoryMap: ring around hubs and event nodes.border-dividershade-a12rgba(11,10,16,0.12)border-dividerwhite-a12rgba(255,255,255,0.12)map-focusStoryMap: ring on a selected or lit hub or event.accentcamellia-600#c2335faccentcamellia-400#f4779fmap-node-bgStoryMap: event node fill.bg-canvasneutral-50#f6f6f8bg-canvasneutral-950#07070amap-node-emptyStoryMap: a hub with no art yet.bg-surface-raisedneutral-100#ececf0bg-surface-raisedneutral-875#1a1a21map-labelStoryMap: event labels and legend.text-secondaryneutral-800#2e2d36text-secondaryneutral-200#dcdcdemap-label-strongStoryMap: hub names.text-primaryneutral-925#0b0a10text-primaryneutral-0#ffffffmap-label-accentStoryMap: the label of a selected, new or proposed event.text-accentcamellia-700#b42354text-accentcamellia-400#f4779fmap-legend-bgStoryMap: legend surface.bg-canvasneutral-50#f6f6f8bg-canvasneutral-950#07070aSpace and shape
map-radiusradius-lg16px
map-inset-ring-widthline-hairline1px
map-edge-stroke-widthits own value
1.2
map-edge-accent-stroke-widthits own value
1.8
map-edge-dim-opacityopacity-dim0.2
map-edge-dashed-dasharrayits own value
4 4
map-arc-stroke-widthits own value
2
map-arc-dashed-dasharrayits own value
5 4
map-hub-gapgap-xs4px
map-hub-dim-opacityopacity-dim0.2
map-hub-b-padding-xinset-xs4px
map-hub-b-radiusradius-xs4px
map-art-widthits own value
108px
map-art-heightits own value
64px
map-art-radiusradius-md12px
map-art-ring-widthline-strong2px
map-art-outer-ring-widthring-outercalc(2px + 2px)
map-face-ring-widthline-heavy3px
map-face-outer-ring-widthring-outercalc(2px + 2px)
map-hub-focus-ring-widthline-heavy3px
map-hub-focus-outer-ring-widthring-outercalc(2px + 2px)
map-badge-topits own value
calc(-1 * 8px)
map-badge-rightits own value
calc(-1 * 8px)
map-event-gapgap-2xs2px
map-event-i-sizeits own value
20px
map-event-i-ring-widthline-strong2px
map-event-span-padding-xinset-xs4px
map-event-span-radiusradius-xs4px
map-event-i-dim-opacityopacity-dim0.2
map-event-i-ghost-opacityopacity-dim0.2
map-event-i-unplaced-border-widthline-hairline1px
map-event-i-focus-ring-widthline-strong2px
map-event-i-proposed-ring-widthline-strong2px
map-event-i-proposed-border-widthline-hairline1px
map-evbadge-leftinset-xs4px
map-evbadge-topits own value
calc(-1 * 24px)
map-legend-leftinset-md12px
map-legend-bottominset-md12px
map-legend-gapgap-lg12px
map-legend-padding-yinset-sm8px
map-legend-padding-xinset-md12px
map-legend-radiusradius-md12px
map-legend-inset-ring-widthline-hairline1px
map-legend-span-gapgap-sm6px
map-tools-insetinset-md12px
map-tools-gapgap-sm6px
map-chip-leftinset-md12px
map-chip-topinset-md12px
map-focus-outline-offsetfocus-ring-offset3px
map-focus-radiusradius-sm8px
map-art-icon-sizeicon-size-sm16px
map-forced-edge-widthline-hairline1px
map-forced-focus-widthline-strong2px
Type
map-hub-b-fonttext-micro700 12px/16px "Funnel Sans", system-ui, -apple-system, "Segoe UI", sans-serif
map-face-initial-font-familyfont-sansFunnel Sans
map-face-initial-font-weightweight-bold700
map-event-span-fonttext-small-strong600 12px/16px "Funnel Sans", system-ui, -apple-system, "Segoe UI", sans-serif
map-event-span-font-weightweight-bold700
map-legend-fonttext-small-strong600 12px/16px "Funnel Sans", system-ui, -apple-system, "Segoe UI", sans-serif
Motion
map-hub-opacity-durationmotion-base240ms
map-hub-easingease-standardcubic-bezier(0.2, 0, 0, 1)
map-hub-box-shadow-durationmotion-fast160ms
map-art-durationmotion-fast160ms
map-art-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 group named by
label(default "Story map: characters and places as hubs, events between them") whose hubs and events are one Tab stop the arrows move through, each read with what the drawing shows (an event's type, "proposed", "new", "not at this moment", "not placed", "current" on the focus), and withonSelecteach is a button. Tab then reaches the chip, a button named "Remove" and its words (onChipRemove), "Zoom in" (onZoomIn) and "Fit the map" (onFit); a dimmed or ghost one fades its art and marks while its words stay readable. - 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.