Profile
A compact avatar + name combo for the trailing slot of a <x-wirekit:: or the leading slot of a user menu. Profile is structural — it lays out an avatar image and a name side-by-side without imposing a link target or a click behavior. Wrap it in an <a>, a <button>, or a <x-wirekit:: when you need interaction.
Basic Usage
Pass avatar for an image, name for text, or both for a combined mark. The avatar is decorative (alt="") — the name text provides the accessible label when present.
Custom Content
If you need more than an avatar and name (e.g. a role badge, a status indicator, a chevron), pass arbitrary children. Children render after the avatar and name — they are additive, not replacements.
Owner
An account trigger that is only the avatar
Leave the name off and the control hugs the avatar. There is no row left to describe, so the radius you set does not apply — the shape follows the content and the control rounds fully, hover surface and focus ring together. Anything else puts a rounded rectangle around a circle, which is what a starter kit reported: a 2px square ring, 35px across a 32px avatar, at the top of a phone layout.
Give it an aria-label, because with no name there is no text for the control to be named by.
Props
| Prop | Type | Default | Description |
|---|---|---|---|
avatar |
string|array|null |
null |
URL of an avatar image, or ['src' => …, 'initials' => …, 'alt' => …]. Rendered at --size-wk-sm (2rem square) with rounded-full object-cover. Decorative — alt="" unless the array supplies one. With no src, the initials render in a token-colored circle instead. |
name |
string|null |
null |
Profile name text. Rendered at --text-wk-sm with body-weight font. |
as |
'div' | 'button' |
'div' |
The element to render. button gives you a real control: the browser supplies Enter and Space activation and the button role, with no JavaScript involved. Prefer it over interactive whenever the profile is itself the thing being clicked. Keep div when the profile sits inside something that is already a control — a button inside a button is invalid HTML. |
interactive |
bool |
false |
Turns a div profile into a focusable button-like element with role="button" + tabindex="0" plus Enter / Space keyboard handlers that synthesize a click. Use it when the surrounding markup rules out a real <button>; otherwise reach for as="button", which gets the same behavior from the browser instead of from Alpine. Adds a focus-visible ring matching the canonical button focus state. Has no effect on as="button", which is already a control. Default false preserves the presentational <div> byte-for-byte. |
radius |
'none' | 'sm' | 'md' | 'lg' | 'xl' | 'full' | 'nav-item' |
'sm' |
Corner rounding of the control branch — the one with a background and a focus ring. nav-item is on the list because it is not a rung: --radius-wk-nav-item is contextual (the shell derives it from the panel radius), which is what an account trigger sitting under a column of navigation entries needs in order to round like them. Has no visible effect on a plain div profile, which has no surface to round. |
tone |
'default' | 'muted' |
'default' |
How loud the name is. default is the color this component has always emitted. muted is what a navigation column asks for: a sidebar's own entries stand muted at rest and go full on hover, so an account trigger baked to the full color leaves exactly one row in the column permanently brighter than every other. muted reproduces both halves, hover included. Has no effect where there is no name — the slot's color is the caller's. |
padding |
'none' | 'xs' | 'sm' | 'md' | 'lg' | 'xl' |
'none' |
Inline padding on the page-edge spine ladder. none is the shape this component has always emitted. It exists for the same place radius does — an account trigger closing a navigation list: without padding the hover background hugs the name instead of drawing a row, so the entry that closes the list looks unlike every entry in it. Pair it with radius="nav-item" and the row matches its neighbors exactly. |
gap |
'none' | 'xs' | 'sm' | 'md' | 'lg' | 'xl' | '2xl' |
'sm' |
Space between the avatar and the name, on the --gap-wk-* ladder — the tighter of the two spacing ladders, and the one this component already stood on. none removes the gap entirely. |
scope |
string|null |
null |
Named personalization scope for block-level class overrides. |
When both avatar and name are omitted, the component renders an empty flex container — pass arbitrary children via the default slot for full control.
Interactive profile inside a dropdown trigger
The most common interactive use case — profile menu in a navbar. Without interactive, the <div> has no focusable child, so keyboard users can Tab past it without opening the menu.
The same menu with a real button
as="button" replaces the synthesized keyboard model with the browser's own. The
markup is one attribute different and the rendering is identical; what changes is that
the control is a button in the served HTML, before any JavaScript runs.
Accessibility
- The avatar
<img>is decorative:alt=""so the accessible name comes from thenametext or surrounding interactive element - By default
Profileis not interactive — it renders a plain<div>. Give itas="button"when the profile is the control, or wrap it in an<a>/<button>/ dropdown trigger when the surrounding element is as="button"is the accessible default for a clickable profile: Space and Enter both activate it, and it is a button to assistive tech from the first paint.interactivesynthesizes the same two keys from Alpine handlers, which means they do nothing until Alpine has booted — use it only where a real<button>is not possible, such as inside another control- When the profile is used inside an interactive wrapper (link/button/trigger), ensure the wrapper has a clear accessible name — either from the visible
nametext or via an explicitaria-labelon the wrapper
<x-wirekit::dropdown.trigger>
<x-wirekit::profile avatar="/me.jpg" name="Jane Doe" />
</x-wirekit::dropdown.trigger>
Keyboard Interaction
The default <div> is presentational and takes no focus, so there is nothing to
operate.
| Shape | Focus | Keys |
|---|---|---|
| default | not focusable | — |
as="button" |
in the tab order | Enter and Space activate it (from the browser) |
interactive |
in the tab order via tabindex="0" |
Enter and Space activate it (from Alpine, so only once Alpine has booted) |
Pitfalls
- Don't reach for
interactivewhenas="button"will do. Both make the profile clickable, but only the button is one before Alpine has loaded, and only the button is announced with its role in the served HTML. - Don't put a link or another button in the slot of an
as="button"profile. A control nested inside a control is invalid HTML and unreachable by keyboard in practice. Keep the defaultdivfor that layout and make the inner element the control. - Pass
avataras an array when you want an initials fallback. A bare string is treated as an image URL;['src' => …, 'initials' => …]lets the component fall back to initials when the image is absent.
Design Tokens
| Token | Used for |
|---|---|
--color-wk-text |
Name text color |
--gap-wk-sm |
Gap between avatar and name — the gap prop moves this to any other rung of the same ladder |
--radius-wk-sm |
Corner rounding when the profile is a control — the radius prop moves this |
--radius-wk-nav-item |
Corner rounding with radius="nav-item", so the row matches the navigation entries around it |
--text-wk-sm |
Name font size |
--font-wk-body-weight |
Name font weight |