Skip to content

I9kProfileMenu ​

The signed-in account's control for a navigation bar: an avatar button that opens a small panel naming the account, its links and the caller's actions, such as signing out.

View as MarkdownView source
ts
import { I9kProfileMenu } from '@9klabs/design';

Examples ​

vue
<div style="display: flex; justify-content: flex-end; min-height: 18rem">
  <I9kProfileMenu
    label="Account menu"
    name="Ismail"
    detail="me@example.com"
    :avatar-src="src"
    :links="links"
  >
    <template #actions="{ close }">
      <I9kButton @click="close">Sign out</I9kButton>
    </template>
  </I9kProfileMenu>
</div>

Without a name ​

vue
<div style="display: flex; justify-content: flex-end; min-height: 14rem">
  <I9kProfileMenu label="Account menu" detail="me@example.com" :avatar-src="src" :links="links" />
</div>

Watch out ​

  • Pass linkComponent as an imported component (RouterLink, NuxtLink), not a string name; without it every link is a plain <a href> and a full page load.
  • The panel closes itself on a link click, but not on a router navigation it did not start (the Back button, a link elsewhere on the page) — bind v-model:open and close it from the consumer on every route change.
  • The avatar on the button is decorative (aria-hidden), so label is the whole accessible name of the button; make it name the control ("Account menu"), not the picture.
  • The panel is aligned to the inline end of the button and opens inward, so place the menu at the end of a bar (the right in left-to-right pages, the left in right-to-left ones).
  • The panel is not teleported, so a bar with overflow: hidden would clip it; I9kNavigation does not.
  • The closed panel is hidden with the hidden attribute, never an inline style, so it works under a style-src Content-Security-Policy without 'unsafe-inline'; do not restyle .i9k-profile-menu__panel with a display that beats [hidden].

API ​

Props ​

PropTypeDefaultRequired
labelstring—yes
namestring | nullnullno
detailstring | nullnullno
avatarSrcstring | nullnullno
linksI9kNavigationLink[]() => []no
linkComponentstring | object | nullnullno
openbooleanfalseno
size'sm' | 'md' | 'lg''md'no

Emits ​

EventPayload
update:open[open: boolean]
navigate[link: I9kNavigationLink, event: MouseEvent]

Slots ​

  • actions

Agent prompt ​

Paste this into an agent to have it use the component correctly. It is self-contained.

text
Use I9kProfileMenu from @9klabs/design for the signed-in account's control in a navigation bar.

import { I9kProfileMenu } from '@9klabs/design';

Props:
- label: string (required) — the button's accessible name, for example "Account menu". The avatar inside the button is decorative, so this is the button's whole name.
- name?: string | null (default null) — the account's display name, shown at the top of the panel. When it is null only the detail line shows.
- detail?: string | null (default null) — a second line under the name, such as the account's email address.
- avatarSrc?: string | null (default null) — any image URL for the account's picture, including a data: URI; shown on the button and in the panel.
- links?: I9kNavigationLink[] (default []) — the { id: string; label: string; href: string } shape I9kNavigation takes, listed in order in the panel. The type is not exported from the package; inline the object shape or declare your own local type.
- linkComponent?: string | object | null (default null) — render the links with this component instead of a plain <a>, receiving the destination as `to` rather than `href`. Pass an imported component (RouterLink, NuxtLink), not its name as a string.
- open?: boolean (default false) — optional external control, bound with v-model:open. The panel drives itself, so this can be left unbound.
- size?: 'sm' | 'md' | 'lg' (default 'md') — the size of the avatar on the button, on the shared control scale.

Emits:
- update:open — [open: boolean], fired on every open and close.
- navigate — [link: I9kNavigationLink, event: MouseEvent], fired when a panel link is clicked, just before the panel closes itself.

Slots:
- actions — the block at the bottom of the panel, such as a sign-out button. Receives one slot prop, close: () => void, which closes the panel and returns focus to the button.

Behavior: a disclosure, not an ARIA menu (no role="menu"). The button carries aria-expanded and aria-controls, and the panel follows it in DOM order, so Tab walks from the button into the links and actions. Escape closes the panel and returns focus to the button; it is heard on the whole document while the panel is open, because Safari and Firefox on macOS do not focus a clicked button. A press outside the menu, keyboard focus leaving it, focus moving into an iframe (a video player), following a link, or calling the actions slot's close closes it. The name and detail lines take dir="auto", so a Latin name or an email address keeps its order on a right-to-left page. The panel is not teleported: it opens under the button, inside the bar's own stacking context (z-index 150 inside a bar at 100). A closed panel is hidden with the `hidden` attribute, not an inline style, so it server-renders without one and stays closed before hydration under a strict style-src Content-Security-Policy.

Usage:
<I9kProfileMenu
  v-model:open="accountOpen"
  label="Account menu"
  :name="user.name"
  :detail="user.email"
  :avatar-src="user.image"
  :links="accountLinks"
  :link-component="NuxtLink"
>
  <template #actions="{ close }">
    <I9kButton @click="close(); signOut()">Sign out</I9kButton>
  </template>
</I9kProfileMenu>

Made with love 💚, caffeine, and Claude Code. Built with @9klabs/design itself.