Sakuga Back to Sakuga
Theme
Sound
Jump to a component

Core · Navigation

TopNav

TopNav is the desktop header: the AniCanon™ mark and wordmark, the main sections as text links, and search and account on the right.

@anicanon/sakuga/uiv1.0Used onWeb

Live, from @anicanon/sakuga 2.0.0

Sakuga for iOS and Android is coming

iOS

Web only

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

Android

Web only

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

Use it for
The desktop header: the AniCanon™ mark, up to five main sections (Home, Series, Shorts, Manga, Creators), search and the account.
Not for

Guidelines

  • Five links at most: Home, Series, Shorts, Manga, Creators. Mark the current one with current; it gets aria-current="page" and the topnav-indicator underline, which slides to a newly current link on motion-base.
  • It sits on the page or over the hero's top scrim, never in a filled bar.
  • Phones never show TopNav; they use TabBar.
  • actions (a prop, or children with slot="actions") sit before search and the account: the notifications bell, an IconButton with attention while something is unread.
  • onSearch runs when Search is pressed and account.onClick when the account is; search={false} drops Search.
  • search as an object searches in the bar: SearchField's props plus open, onOpen and onClose. Search calls onOpen; while open, the field takes the links' place, with Close search after it. Escape on an empty field calls onClose, and focus goes back to Search.
TopNavExample.jsxReact
import { useState } from 'react';
import { IconButton, TopNav } from '@anicanon/sakuga/ui';

export default function TopNavExample() {
  const [value, setValue] = useState('Series');
  return (
    <TopNav
      brand={{ logo: '/art/anicanon-mark.png', name: 'AniCanon' }}
      links={['Home', 'Series', 'Shorts', 'Manga', 'Creators'].map((l) => ({
        label: l,
        current: value === l,
        onClick: (e) => {
          e.preventDefault();
          setValue(l);
        },
      }))}
      actions={[
        <IconButton key="s" icon="search" label="Search" variant="plain" />,
        <img
          key="a"
          src="/art/eba-cast-lisa.webp"
          alt="Your account"
          style={{
            width: 34,
            height: 34,
            borderRadius: '50%',
            objectFit: 'cover',
            objectPosition: 'top',
          }}
        />,
      ]}
    />
  );
}

PropsTopNavProps

linksrequired
NavLink[]
brand
{ logo?: string; name?: string; href?: string; }
actions
ReactNode
Or children with slot="actions": they sit before search and the account.
label
stringDefault 'Main'
className
string
account
{ name: string; image?: string; onClick?: MouseEventHandler; }
The account button; onClick runs when it is pressed.
create
{ label?: string; href?: string; onClick?: MouseEventHandler; }
With href the Create button is a link; without, a button that runs onClick.
search
boolean | (SearchFieldProps & { open?: boolean; onOpen?: () => void; })
false drops the Search button, which shows with an account or create. An object holds the bar's SearchField: Search calls onOpen, and while open the field takes the links' place with Close search (onClose) after it.
onSearch
MouseEventHandler
Runs when Search is pressed.
children
ReactNode

Types

interface NavLink {
    label: string;
    href?: string;
    current?: boolean;
    onClick?: MouseEventHandler;
}
interface SearchFieldProps {
    /** Names the search and its field; 'Search' when left out. Not drawn. */
    label?: string;
    /** The starting words; the field follows when it changes. */
    value?: string;
    placeholder?: string;
    /** The words as they are typed. */
    onChange?: (text: string) => void;
    /** Enter with no result active. */
    onSubmit?: (text: string) => void;
    /** Picker's groups; leave out for a search that only submits. */
    results?: {
        title?: string;
        options: PickerOption[];
    }[];
    onSelect?: (option: PickerOption) => void;
    busy?: boolean;
    /** Said while busy; 'Searching' when left out. */
    busyText?: string;
    /** Said when results has no options; 'Nothing matches' when left out. */
    emptyText?: string;
    onClear?: () => void;
    /** Escape with nothing left to close or clear. */
    onClose?: () => void;
    /** Starts the results open or closed. */
    open?: boolean;
    autoFocus?: boolean;
    maxWidth?: number;
    id?: string;
    className?: string;
}
TopNavExample.swiftiOS · coming soon
TopNavExample.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

topnav-link-fgTopNav: idle links.
text-secondaryneutral-800#2e2d36
text-secondaryneutral-200#dcdcde
topnav-link-active-fgTopNav: the current link.
text-primaryneutral-925#0b0a10
text-primaryneutral-0#ffffff
topnav-indicatorTopNav: underline under the current link.
accentcamellia-600#c2335f
accentcamellia-400#f4779f
topnav-brand-fgTopNav: the AniCanon™ wordmark.
text-primaryneutral-925#0b0a10
text-primaryneutral-0#ffffff

Space and shape

topnav-gap
gap-3xl
40px
topnav-height
its own value
80px
topnav-brand-gap
gap-md
8px
topnav-brand-img-size
its own value
28px
topnav-links-gap
gap-2xl
24px
topnav-links-a-padding-top
inset-sm
8px
topnav-links-a-padding-bottom
inset-xs
4px
topnav-links-a-page-border-bottom-width
line-strong
2px
topnav-actions-gap
gap-xl
16px
topnav-account-size
its own value
36px
topnav-account-ring-width
line-strong
2px
topnav-account-outer-ring-width
ring-outer
calc(2px + 2px)
topnav-account-active-scale
scale-press-sm
.94
topnav-links-glide-border-bottom-width
line-strong
2px
topnav-forced-current-width
line-strong
2px

Type

topnav-font-family
font-sans
Funnel Sans
topnav-brand-font-family
font-display
Funnel Display
topnav-brand-font-weight
text-title-3-weight
700
topnav-brand-font-size
text-title-3-size
20px
Aa
topnav-links-a-font-size
text-body-lg-size
15px
Aa
topnav-links-a-font-weight
text-body-lg-weight
400
topnav-account-font
text-caption-strong
600 13px/18px "Funnel Sans", system-ui, -apple-system, "Segoe UI", sans-serif

Motion

topnav-links-a-duration
motion-fast
160ms
topnav-links-a-easing
ease-standard
cubic-bezier(0.2, 0, 0, 1)
topnav-account-duration
motion-fast
160ms
topnav-account-easing
ease-standard
cubic-bezier(0.2, 0, 0, 1)
topnav-account-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 header whose brand link reads the name (the logo is hidden; without brand.href the brand is the name alone, not a link), then a navigation named by label (default "Main") whose links mark the current page (a link without href is a button running its onClick, never a link to #); Tab moves through them, then the actions, then, when there is an account or Create, "Search" (running onSearch, or search.onOpen; while searching, the search landmark and "Close search" take the links' place and Search is gone), then Create and the account button ("Your account, Aiko Mori", running account.onClick).
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.