Sakuga Back to Sakuga
Theme
Sound
Jump to a component

AniCanonKit · Manga

LayerStrip

LayerStrip lists a page's layers by kind, in reading order, under the editor's canvas: panels, lettering, sound effects.

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

Live, from @anicanon/sakuga 2.0.0

Sakuga for iOS and Android is coming

iOS

Web only

LayerStrip is made for the web, and isn’t part of Sakuga for iOS yet.

Android

Web only

LayerStrip is made for the web, and isn’t part of Sakuga for Android yet.

Use it for
A page's layers under the editor's canvas: panels, lettering and sound effects, a track per kind in reading order, with the issue strip on top.
Not for

Guidelines

  • One track per kind; each item is its number (3.2) and, for words, how it is set and its first words. Panels show their picture once drawn.
  • Choosing an item selects it on the page and the inspector edits that kind: a panel's direction and frame, a balloon's type and shape, a sound effect's style.
  • Children sit on top: the issue's IssueStrip at size 18, so the page is always placed in its issue.
  • An empty kind says emptyLabel ("None yet"), never hides.
  • onSelect(track, item) makes choosing work: each kind is a listbox, one Tab stop that Left and Right move through, and Enter, Space or a click choose. muted marks words drawn into the art rather than set as text: the picture dims, the words stay readable.
LayerStripExample.jsxReact
import { IssueStrip, LayerStrip } from '@anicanon/sakuga/anicanonkit';

const tracks = [
  {
    kind: 'panels',
    label: 'Panels',
    items: [
      {
        number: '3.1',
        image: '/art/bad-memories-panel-sofa.webp',
        selected: true,
      },
      { number: '3.2' },
      { number: '3.3' },
      { number: '3.4' },
    ],
  },
  {
    kind: 'lettering',
    label: 'Lettering',
    items: [
      { number: '3.1', label: 'Caption · I’m Bruce' },
      { number: '3.2', label: 'Caption · Every snowfall…' },
    ],
  },
  { kind: 'sfx', label: 'Sound effects', items: [] },
];

const spreads = [
  [{ image: '/art/bad-memories-the-winter-p00.webp', label: 'Front cover' }],
  [
    {
      image: '/art/bad-memories-the-winter-bruce.webp',
      label: 'Page 3',
      selected: true,
    },
    { image: '/art/bad-memories-the-winter-p04.webp', label: 'Page 4' },
  ],
  [
    { image: '/art/bad-memories-the-winter-alarm.webp', label: 'Page 5' },
    {
      frames: [
        { x: 4, y: 4, w: 92, h: 30 },
        { x: 4, y: 36, w: 44, h: 28, state: 'drawing' },
        { x: 52, y: 36, w: 44, h: 28 },
        { x: 4, y: 66, w: 92, h: 30 },
      ],
      label: 'Page 6, drawing',
    },
  ],
  [
    {
      frames: [
        { x: 4, y: 4, w: 56, h: 44 },
        { x: 62, y: 4, w: 34, h: 44 },
        { x: 4, y: 50, w: 92, h: 46 },
      ],
      label: 'Page 7, laid out',
    },
    {
      frames: [
        { x: 4, y: 4, w: 92, h: 22 },
        { x: 4, y: 28, w: 44, h: 34 },
        { x: 52, y: 28, w: 44, h: 34 },
        { x: 4, y: 64, w: 30, h: 32 },
        { x: 36, y: 64, w: 60, h: 32 },
      ],
      label: 'Page 8, laid out',
    },
  ],
  [
    {
      frames: [
        { x: 4, y: 4, w: 92, h: 58 },
        { x: 4, y: 64, w: 92, h: 32 },
      ],
      label: 'Page 9, laid out',
    },
    { blank: true, label: 'Back cover, not made' },
  ],
];

export default function LayerStripExample() {
  return (
    <LayerStrip tracks={tracks}>
      <IssueStrip size={18} label="The issue" spreads={spreads} />
    </LayerStrip>
  );
}

PropsLayerStripProps

tracksrequired
LayerStripTrack[]
emptyLabel
string
children
ReactNode
className
string
onSelect
(track: number, item: number) => void
Makes each track a listbox, one Tab stop that Left and Right move through; Enter, Space or a click choose a layer.

Types

interface LayerStripTrack {
    kind: 'panels' | 'lettering' | 'sfx';
    label: string;
    icon?: IconName;
    items: {
        number: string;
        label?: string;
        image?: string;
        selected?: boolean; /** Drawn into the art rather than set as text. */
        muted?: boolean;
    }[];
}
LayerStripExample.swiftiOS · coming soon
LayerStripExample.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

layers-bgLayerStrip: the strip.
bg-surfaceneutral-0#ffffff
bg-surfaceneutral-900#111117
layers-edgeLayerStrip: its edge and divider.
border-hairlineshade-a08rgba(11,10,16,0.08)
border-hairlinewhite-a06rgba(255,255,255,0.06)
layers-headLayerStrip: a kind's name.
text-secondaryneutral-800#2e2d36
text-secondaryneutral-200#dcdcde
layers-itemLayerStrip: a layer.
bg-fill-subtleshade-a06rgba(11,10,16,0.06)
bg-fill-subtlewhite-a08rgba(255,255,255,0.08)
layers-item-selectedLayerStrip: the selected layer.
bg-surface-raisedneutral-100#ececf0
bg-surface-raisedneutral-875#1a1a21
layers-selectedLayerStrip: the selected layer's ring.
accentcamellia-600#c2335f
accentcamellia-400#f4779f
layers-textLayerStrip: a layer's number.
text-primaryneutral-925#0b0a10
text-primaryneutral-0#ffffff
layers-mutedLayerStrip: counts, lines and empty text.
text-mutedneutral-600#5c5b67
text-mutedneutral-400#a3a3a7
layers-emptyLayerStrip: a panel not drawn yet.
border-placeholdershade-a24rgba(11,10,16,0.24)
border-placeholderwhite-a22rgba(255,255,255,0.22)

Space and shape

layers-head-icon-size
icon-size-sm
16px
layers-gap
gap-sm
6px
layers-padding-y
inset-sm
8px
layers-padding-x
inset-md
12px
layers-radius
radius-md
12px
layers-inset-ring-width
line-hairline
1px
layers-top-padding-bottom
inset-sm
8px
layers-top-margin-bottom
gap-xs
4px
layers-top-border-bottom-width
line-hairline
1px
layers-track-column-1
its own value
132px
layers-track-gap
gap-md
8px
layers-track-min-height
its own value
44px
layers-head-gap
gap-md
8px
layers-items-gap
gap-sm
6px
layers-items-padding
inset-2xs
2px
layers-item-gap
gap-md
8px
layers-item-height
size-control-md
44px
layers-item-padding-right
inset-sm
8px
layers-item-padding-left
inset-xs
4px
layers-item-radius
radius-sm
8px
layers-item-img-size
its own value
32px
layers-item-img-radius
radius-xs
4px
layers-item-i-max-width
its own value
180px
layers-item-selected-inset-ring-width
line-strong
2px
layers-item-muted-opacity
opacity-muted
0.6
layers-empty-size
its own value
32px
layers-empty-radius
radius-xs
4px
layers-empty-inset-ring-width
line-strong
2px
layers-forced-edge-width
line-hairline
1px
layers-forced-selected-width
line-strong
2px

Type

layers-head-font
text-small-strong
600 12px/16px "Funnel Sans", system-ui, -apple-system, "Segoe UI", sans-serif
layers-head-small-font-weight
weight-medium
500
layers-item-font
text-small-strong
600 12px/16px "Funnel Sans", system-ui, -apple-system, "Segoe UI", sans-serif
layers-item-i-font-weight
weight-medium
500
layers-none-font
text-small
500 12px/16px "Funnel Sans", system-ui, -apple-system, "Segoe UI", sans-serif

Motion

layers-item-duration
motion-fast
160ms
layers-item-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
Each track reads its name and count, then its layers: with onSelect, a listbox named by the track and one Tab stop, where Left and Right move between layers, Enter, Space or a click choose one, and the chosen one reads "selected"; without it, a list whose selected item reads "current". Each layer reads its number and label, with "drawn into the art" when muted, pictures are hidden, and an empty track reads "None yet".
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.