Sakuga Back to Sakuga
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.

@anicanon/sakuga/anicanonkitv1.0Used onWebFrom AniCanon™

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 are ghost.
  • A proposed event (being added) is drawn dashed in the accent; a just-added one is new with a New badge.
  • arcs draw causes and conflicts of the selection; dashed marks 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, onFit and onChipRemove make the tools and the chip's close work.
StoryMapExample.jsxReact
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

hubsrequired
StoryMapHub[]
eventsrequired
StoryMapEvent[]
edgesrequired
StoryMapEdge[]
height
number
width
number
label
string
chip
string
tools
boolean
legend
boolean
onSelect
(id: string, kind: 'hub' | 'event') => void
Makes each hub and event a button, all one Tab stop.
onZoomIn
MouseEventHandler
onFit
MouseEventHandler
onChipRemove
MouseEventHandler
The chip's close.
arcs
StoryMapArc[]
children
ReactNode
Drawn over the map, after the events.
className
string

Types

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;
}
StoryMapExample.swiftiOS · coming soon
StoryMapExample.ktAndroid · coming soon

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#ffffff
bg-surfaceneutral-900#111117
map-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#c2335f
accentcamellia-400#f4779f
map-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#c2335f
accentcamellia-400#f4779f
map-node-bgStoryMap: event node fill.
bg-canvasneutral-50#f6f6f8
bg-canvasneutral-950#07070a
map-node-emptyStoryMap: a hub with no art yet.
bg-surface-raisedneutral-100#ececf0
bg-surface-raisedneutral-875#1a1a21
map-labelStoryMap: event labels and legend.
text-secondaryneutral-800#2e2d36
text-secondaryneutral-200#dcdcde
map-label-strongStoryMap: hub names.
text-primaryneutral-925#0b0a10
text-primaryneutral-0#ffffff
map-label-accentStoryMap: the label of a selected, new or proposed event.
text-accentcamellia-700#b42354
text-accentcamellia-400#f4779f
map-legend-bgStoryMap: legend surface.
bg-canvasneutral-50#f6f6f8
bg-canvasneutral-950#07070a

Space and shape

map-radius
radius-lg
16px
map-inset-ring-width
line-hairline
1px
map-edge-stroke-width
its own value
1.2
map-edge-accent-stroke-width
its own value
1.8
map-edge-dim-opacity
opacity-dim
0.2
map-edge-dashed-dasharray
its own value
4 4
map-arc-stroke-width
its own value
2
map-arc-dashed-dasharray
its own value
5 4
map-hub-gap
gap-xs
4px
map-hub-dim-opacity
opacity-dim
0.2
map-hub-b-padding-x
inset-xs
4px
map-hub-b-radius
radius-xs
4px
map-art-width
its own value
108px
map-art-height
its own value
64px
map-art-radius
radius-md
12px
map-art-ring-width
line-strong
2px
map-art-outer-ring-width
ring-outer
calc(2px + 2px)
map-face-ring-width
line-heavy
3px
map-face-outer-ring-width
ring-outer
calc(2px + 2px)
map-hub-focus-ring-width
line-heavy
3px
map-hub-focus-outer-ring-width
ring-outer
calc(2px + 2px)
map-badge-top
its own value
calc(-1 * 8px)
map-badge-right
its own value
calc(-1 * 8px)
map-event-gap
gap-2xs
2px
map-event-i-size
its own value
20px
map-event-i-ring-width
line-strong
2px
map-event-span-padding-x
inset-xs
4px
map-event-span-radius
radius-xs
4px
map-event-i-dim-opacity
opacity-dim
0.2
map-event-i-ghost-opacity
opacity-dim
0.2
map-event-i-unplaced-border-width
line-hairline
1px
map-event-i-focus-ring-width
line-strong
2px
map-event-i-proposed-ring-width
line-strong
2px
map-event-i-proposed-border-width
line-hairline
1px
map-evbadge-left
inset-xs
4px
map-evbadge-top
its own value
calc(-1 * 24px)
map-legend-left
inset-md
12px
map-legend-bottom
inset-md
12px
map-legend-gap
gap-lg
12px
map-legend-padding-y
inset-sm
8px
map-legend-padding-x
inset-md
12px
map-legend-radius
radius-md
12px
map-legend-inset-ring-width
line-hairline
1px
map-legend-span-gap
gap-sm
6px
map-tools-inset
inset-md
12px
map-tools-gap
gap-sm
6px
map-chip-left
inset-md
12px
map-chip-top
inset-md
12px
map-focus-outline-offset
focus-ring-offset
3px
map-focus-radius
radius-sm
8px
map-art-icon-size
icon-size-sm
16px
map-forced-edge-width
line-hairline
1px
map-forced-focus-width
line-strong
2px

Type

map-hub-b-font
text-micro
700 12px/16px "Funnel Sans", system-ui, -apple-system, "Segoe UI", sans-serif
map-face-initial-font-family
font-sans
Funnel Sans
map-face-initial-font-weight
weight-bold
700
map-event-span-font
text-small-strong
600 12px/16px "Funnel Sans", system-ui, -apple-system, "Segoe UI", sans-serif
map-event-span-font-weight
weight-bold
700
map-legend-font
text-small-strong
600 12px/16px "Funnel Sans", system-ui, -apple-system, "Segoe UI", sans-serif

Motion

map-hub-opacity-duration
motion-base
240ms
map-hub-easing
ease-standard
cubic-bezier(0.2, 0, 0, 1)
map-hub-box-shadow-duration
motion-fast
160ms
map-art-duration
motion-fast
160ms
map-art-easing
ease-standard
cubic-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 with onSelect each 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.