๐Ÿ™ Onigiri

The Crunchyroll design system, rendered live from its own tokens and components. It cannot drift from what ships.

Docs draft

Button

When to use it

useTriggering an action in the current view: start playback, submit a form, confirm a dialog.
avoidNavigating to another page. That is a Link, even when it looks prominent. link
avoidMore than one primary button per view. Primary is the single main action; everything else steps down a weight. button Outline/ghost.

Anatomy

container โ€” Pill (radius.full), 44px tall. Hugs its content horizontally.

leading icon โ€” Optional, 20px. Waits on the Icon set; placeholder swap target until then.

label โ€” Sentence case. The component never transforms case.

trailing icon โ€” Optional, mirrors leading icon.

spinner โ€” Replaces the label in the loading state; label stays in the accessibility tree.

Variants

variant ยท primary / secondary / outline / ghost / brand-outline / brand-ghost โ€” primary = filled accent pill; secondary = inverse fill; outline/ghost = quiet neutrals; brand-outline/brand-ghost = orange text roles on quiet surfaces. One size (44px), per the reference โ€” a size axis is added only when a rebuilt component demands one.

States

rest โ€” At rest with no interaction.

hover โ€” Pointer is over the control.

pressed โ€” Actively pressed by pointer or key.

focused โ€” Holds keyboard focus and shows the focus ring.

disabled โ€” Not interactive; rendered with the lifted disabled tokens so it stays perceivable.

loading โ€” Waiting on content; shows the loading treatment.

Guidelines

mustPrimary buttons carry ink text (color.accent.on-accent) on every accent fill state, never white. โ€” White on brand orange is 2.9:1; ink holds 8.4:1 on hover and 4.9:1 on pressed. The gate contracts all three fills.
mustBrand-variant text uses color.accent.text and color.accent.text-pressed, never raw brand orange or the hover-lift orange. โ€” The old DS-update reference put orange.450 text on the tint wash at 2.2:1; the contract color.accent.text on color.accent.wash replaces it at 5.7:1.
must-notDo not transform label case in content; the component renders labels exactly as written, sentence case. โ€” Ruled with the pill shape from the DS-update reference; the earlier uppercase treatment is dead.
shouldWrite labels as verbs describing the action: Start watching, Add to Crunchylist. โ€” Buttons act; links navigate.
must-notButton labels never wrap: the pill is single-line (white-space: nowrap in the component). A container too narrow for its buttons reflows or truncates around them; it does not bend the pill into two lines. โ€” The 44px pill height assumes one line โ€” a wrapped label overflows the anatomy and breaks the icon/label baseline. Caught 2026-09-01 in the HeroCarousel CTA pair, where flex squeeze wrapped both labels.

API

PropTypeDescription
variantenumVisual style of the button.
leadingIconReactNodeRendered aria-hidden at 1.25em.
trailingIconReactNodeIcon rendered after the label.
loadingbooleanImplies disabled + aria-busy; label moves to a visually hidden span.
...restButtonHTMLAttributesForwarded to the native button.

Design specifications

radiusradius.full
heightspace.44
padding-xspace.20
gapspace.4
labeltype.label-lg
fill-primary-restcolor.accent.base
fill-primary-hovercolor.accent.hover
fill-primary-pressedcolor.accent.pressed
text-primarycolor.accent.on-accent
fill-secondarycolor.bg.inverse
text-secondarycolor.text.on-inverse
border-outlinecolor.border.subtle
wash-hovercolor.bg.secondary
wash-pressedcolor.bg.pressed
border-brandcolor.accent.base
text-brandcolor.accent.text
text-brand-pressedcolor.accent.text-pressed
wash-brandcolor.accent.wash
border-disabledcolor.border.subtle
text-disabledcolor.text.disabled

Accessibility

Enter / Space โ€” Activates the button.

Tab โ€” Moves focus; focus-visible draws a 2px ring offset 3px in color.text.primary.

color.accent.on-accent on color.accent.base โ€” Contracted at โ‰ฅ 4.5:1 by the build gate.

color.accent.on-accent on color.accent.hover โ€” Contracted at โ‰ฅ 4.5:1 by the build gate.

color.accent.on-accent on color.accent.pressed โ€” Contracted at โ‰ฅ 4.5:1 by the build gate.

color.text.on-inverse on color.bg.inverse โ€” Contracted at โ‰ฅ 4.5:1 by the build gate.

color.accent.text on color.accent.wash โ€” Contracted at โ‰ฅ 4.5:1 by the build gate.

The focus ring deliberately deviates from the reference's brand-orange ring: orange on cream is 2.7:1, below the 3:1 focus-appearance floor, so the ring binds color.text.primary in both themes. Loading announces via aria-busy with the label preserved in a visually hidden span. Disabled buttons stay visible at the declared muted contrast; the reference's #BBBBBB (1.7:1) was replaced by the lifted disabled token.