Sakuga Back to Sakuga
Theme
Sound
Jump to a component

Core · Actions

Button

Button runs an action with a text label: Play, Follow, Remind me, Done.

@anicanon/sakuga/uiv1.0Used onWeb · iOS · Android

Live, from @anicanon/sakuga 2.0.0

Sakuga for iOS and Android is coming

iOS

Coming soon

Button for iOS is in the roughs. The tokens it will read are the ones on this page, the same on every platform.

Android

Coming soon

Button for Android is in the roughs. The tokens it will read are the ones on this page, the same on every platform.

Use it for
An action with a word on it: the one main action of a view, the action beside it, and the confirmation in a dialog that deletes something.
Not for
  • An action inside a sentence or a row of textTextLink
  • An action that one glyph says better than a wordIconButton
  • Narrowing what a feed showsChip
  • Switching between views of the same thingSegmentedControl
  • A choice whose options each need a picture or a line of explanationOptionTiles
  • Signing in with Apple or GoogleProviderButton

Guidelines

primary
The one main action per view (Play, Follow creator, Done). Never two primaries side by side.
secondary
The supporting action beside the primary (Follow, More info).
outline
A standalone action that should not compete with the primary (Remind me).
ghost
Quiet actions in rows and headers (See all).
destructive
The one action that confirms a deletion or takes access away (Delete my account, Disconnect Claude), filled in vermilion (danger). It lives in the dialog that asks first, and it is that dialog's main action, in place of a primary. The way into it is a TextLink variant="destructive", never a red button on the page. Removals you can simply undo (Remove a tag, Unfollow) stay ghost.
  • size="lg" for hero actions, md by default, sm for desktop-only secondary actions. Every size except sm meets the 44px target.
  • shape="pill" on phones; the default rounded corner (radius-md) on desktop.
  • Labels say what happens, in sentence case. Add aria-label when the visible label needs its object ("Follow" → "Follow EBA").
  • Pass href to render a link that looks like a button.
  • trailing puts a value after the label, behind a hairline: { value, mark, label }. mark is drawn before the value as given: size it at the button's icon size and hide it from a screen reader (aria-hidden), as SpendButton does; label is read after the value, so "~100" is heard as "~100 fragments". The core knows no unit. An action that spends fragments is AniCanonKit's SpendButton, which fills trailing with the fragment mark. Keep the label a plain verb ("Generate", "Start", "Retake"); never write the price into the label.
  • media and media-secondary: the same two roles drawn over video or key art (hero Play and More info, Skip intro, shorts CTA). They never change with the theme.
  • Pressing sinks it to 97% on motion-instant; it settles back on motion-fast.
  • With sound on, a press plays the press sound (README · Sound).
  • A disabled primary turns into a quiet fill (button-disabled-bg), not a faded white. Say why it's disabled in a visible line near it, never only in a tooltip.
ButtonExample.jsxReact
import { Button } from '@anicanon/sakuga/ui';

export default function ButtonExample() {
  return (
    <>
      <Button variant="primary" size="lg" icon="play">
        Play S1 · E1
      </Button>
      <Button variant="secondary" size="lg" aria-label="Follow EBA">
        + Follow
      </Button>
      <Button variant="outline">Remind me</Button>
      <Button variant="ghost">See all</Button>
      <Button variant="destructive">Delete my account</Button>
      <Button
        variant="primary"
        shape="pill"
        size="lg"
        icon="play"
        style={{ flex: 1 }}
      >
        Play E1
      </Button>
      <Button variant="secondary" shape="pill" size="lg">
        Follow
      </Button>
    </>
  );
}

PropsButtonProps

variant
'primary' | 'secondary' | 'outline' | 'ghost' | 'media' | 'media-secondary' | 'destructive'Default 'primary'
media and media-secondary sit over video or artwork and never change with the theme. destructive: the one action that confirms a deletion or takes access away, in the dialog that asks for it.
size
'sm' | 'md' | 'lg'Default 'md'
shape
'rounded' | 'pill'
pill on phones; rounded (radius-md) on desktop.
icon
IconName
iconEnd
IconName
fullWidth
boolean
trailing
ButtonTrailing
as
ElementType
Renders as another element, such as a router's link.
children
ReactNode
className
string

An IconName is any of the names Icon lists.

Types

interface ButtonTrailing {
    value: ReactNode;
    /** Drawn before the value, at buttonIconSize(size). It must hide itself from a screen reader (aria-hidden); the label speaks for it. */
    mark?: ReactNode;
    /** Read after the value, e.g. "fragments", so "~100" is heard as "~100 fragments". */
    label?: string;
}
ButtonExample.swiftiOS · coming soon
ButtonExample.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

button-primary-bgButton primary: fill.
action-primaryneutral-925#0b0a10
action-primaryneutral-0#ffffff
button-primary-fgButton primary: label and icon.
text-inverseneutral-0#ffffff
text-inverseneutral-925#0b0a10
button-secondary-bgButton secondary: fill.
action-secondaryshade-a12rgba(11,10,16,0.12)
action-secondarywhite-a14rgba(255,255,255,0.14)
button-secondary-fgButton secondary: label and icon.
text-primaryneutral-925#0b0a10
text-primaryneutral-0#ffffff
button-outline-borderButton outline: edge.
border-controlshade-a52rgba(11,10,16,0.52)
border-controlwhite-a45rgba(255,255,255,0.45)
button-outline-fgButton outline: label and icon.
text-primaryneutral-925#0b0a10
text-primaryneutral-0#ffffff
button-ghost-fgButton ghost: label and icon.
text-secondaryneutral-800#2e2d36
text-secondaryneutral-200#dcdcde
button-ghost-hover-bgButton ghost: hover fill.
bg-fill-subtleshade-a06rgba(11,10,16,0.06)
bg-fill-subtlewhite-a08rgba(255,255,255,0.08)
button-disabled-bgButton primary or media while it can't be used yet: a quiet fill, never a faded white.
bg-fill-subtleshade-a06rgba(11,10,16,0.06)
bg-fill-subtlewhite-a08rgba(255,255,255,0.08)
button-disabled-fgButton label while disabled.
text-mutedneutral-600#5c5b67
text-mutedneutral-400#a3a3a7
button-media-bgButton media: fill over video or artwork.
action-medianeutral-0#ffffff
action-medianeutral-0#ffffff
button-media-fgButton media: label and icon.
text-on-action-medianeutral-925#0b0a10
text-on-action-medianeutral-925#0b0a10
button-media-secondary-bgButton media-secondary: glass fill over video or artwork.
action-media-secondaryglass-dark-a42rgba(28,28,34,0.42)
action-media-secondaryglass-dark-a42rgba(28,28,34,0.42)
button-media-secondary-fgButton media-secondary: label and icon.
text-on-medianeutral-0#ffffff
text-on-medianeutral-0#ffffff
button-media-secondary-borderButton media-secondary: edge.
border-glass-mediawhite-a16rgba(255,255,255,0.16)
border-glass-mediawhite-a16rgba(255,255,255,0.16)
button-destructive-bgButton destructive: the fill of the action that confirms a deletion or takes access away.
dangervermilion-600#c8321e
dangervermilion-600#c8321e
button-destructive-fgButton destructive: its label and icon.
on-dangerneutral-0#ffffff
on-dangerneutral-0#ffffff

Space and shape

button-radius
radius-md
12px
button-pill-radius
radius-full
9999px
button-height-sm
size-control-sm
36px
button-height-md
size-control-md
44px
button-height-lg
size-control-lg
52px
button-gap
gap-md
8px
button-active-scale
scale-press
.97
button-padding-x-sm
inset-md
12px
button-padding-x-md
inset-lg
16px
button-padding-x-lg
inset-xl
24px
button-outline-inset-ring-width
line-hairline
1px
button-media-secondary-blur
blur-glass
blur(22px) saturate(1.8)
button-media-secondary-inset-ring-width
line-hairline
1px
button-disabled-opacity
opacity-disabled
0.35
button-icon-size-lg
icon-size-sm
16px
button-icon-size
icon-size-sm
16px
button-cost-gap
gap-xs
4px
button-cost-margin-left
gap-2xs
2px
button-cost-padding-left
inset-md
12px
button-cost-border-left-width
line-hairline
1px
button-cost-padding-left-sm-md
inset-sm
8px
button-forced-edge-width
line-hairline
1px

Type

button-font-family
font-sans
Funnel Sans
button-font-weight
weight-bold
700
button-font-size-sm
text-label-bold-size
14px
Aa
button-line-height-sm
text-label-bold-line-height
18px
button-font-size-md
text-label-bold-size
14px
Aa
button-line-height-md
text-label-bold-line-height
18px
button-font-size-lg
its own value
16px
Aa
button-line-height-lg
its own value
20px
button-secondary-font-weight
weight-strong
600
button-outline-font-weight
weight-strong
600
button-ghost-font-weight
weight-strong
600
button-media-secondary-font-weight
weight-strong
600
button-cost-font-weight
weight-strong
600

Motion

button-duration
motion-fast
160ms
button-easing
ease-standard
cubic-bezier(0.2, 0, 0, 1)
button-active-duration
motion-instant
100ms

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 native button (a link with href): Tab reaches it, Enter activates it, and Space does too on a button; disabled takes a button out of the tab order. A screen reader hears its text (or aria-label), with trailing's value and its label read after it ("~100 fragments"); icons and the mark are hidden. With href and disabled it is drawn as a disabled button, out of the tab order, since a link cannot be disabled. In forced colours it keeps an edge in ButtonText, and a disabled one turns GrayText.
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.