All components
Persona
Avatar with name, secondary label, and a presence badge or ring, from 2xs to 2xl.
SJ
Sara JansenProduct Manager
12345678910111213141516171819202122232425import { Persona } from "#components/ui/base-ui/persona.tsx";import { Avatar } from "#components/ui/base-ui/avatar.tsx";
export function Example() { return ( // variant/size resolve once on Root; parts inherit them via context and // expose data-* attributes. Nest Persona.Presence inside your avatar — // the badge sizes from the avatar's --avatar-size and colors from // --persona-presence (set it to a semantic presence token). <Persona.Root variant="badge" size="md" style={styles.online}> <Avatar.Root shape="full"> <Avatar.Fallback>SJ</Avatar.Fallback> <Persona.Presence /> </Avatar.Root> <Persona.Details> <Persona.Label>Sara Jansen</Persona.Label> <Persona.SecondaryLabel>Product Manager</Persona.SecondaryLabel> <Persona.TertiaryLabel>Amsterdam · 09:41</Persona.TertiaryLabel> </Persona.Details> </Persona.Root> );}
// styles.online = { "--persona-presence": semanticColors.presenceOnline }// secondary/tertiary labels hide themselves at the smaller sizesVariants
SJ
badgeSara JansenProduct Manager
SJ
ringSara JansenProduct Manager
Sizes
SJ
2xsSara JansenProduct Manager
SJ
xsSara JansenProduct Manager
SJ
smSara JansenProduct Manager
SJ
mdSara JansenProduct Manager
SJ
lgSara JansenProduct Manager
SJ
xlSara JansenProduct Manager
SJ
2xlSara JansenProduct Manager
Install
1npx stylexblocks add personaSource
components/ui/base-ui/persona.tsx
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223import * as stylex from "@stylexjs/stylex";import { createSlotRecipeContext } from "#lib/base-ui/create-slot-recipe-context.tsx";import { defineSlotRecipe } from "#lib/recipe-props.ts";import { fontSizes } from '@stylexblocks/ui/tokens/font-sizes.stylex.ts'import { lineHeights } from '@stylexblocks/ui/tokens/line-heights.stylex.ts'import { radii } from '@stylexblocks/ui/tokens/radii.stylex.ts'import { semanticColors } from '@stylexblocks/ui/semantic-tokens/colors.stylex.ts'import { spacing } from '@stylexblocks/ui/tokens/spacing.stylex.ts'
const personaRoot = stylex.create({ base: { '--presence-border-color': 'var(--bg-currentcolor)', '--persona-presence': semanticColors.presenceOffline, display: 'flex', flexDirection: 'row', alignItems: 'center', },})
const personaDetails = stylex.create({ base: { display: 'flex', flexDirection: 'column', minWidth: 0, lineHeight: 1.375, },})
const personaDetailsSizes = stylex.create({ '2xs': { marginInlineStart: spacing._1, }, xs: { marginInlineStart: spacing._1_5, }, sm: { marginInlineStart: spacing._2, }, md: { marginInlineStart: spacing._2, }, lg: { marginInlineStart: spacing._3, }, xl: { marginInlineStart: spacing._4, }, '2xl': { marginInlineStart: spacing._4, },})
const personaLabel = stylex.create({ base: { overflow: 'hidden', whiteSpace: 'nowrap', textOverflow: 'ellipsis', minWidth: 0, color: semanticColors.fg, },})
const personaLabelSizes = stylex.create({ '2xs': { fontSize: fontSizes.xs, }, xs: { fontSize: fontSizes.xs, }, sm: { fontSize: fontSizes.sm, }, md: { fontSize: fontSizes.sm, }, lg: { fontSize: fontSizes.md, }, xl: { fontSize: fontSizes.lg, }, '2xl': { fontSize: fontSizes.xl, },})
const personaSecondaryLabel = stylex.create({ base: { overflow: 'hidden', whiteSpace: 'nowrap', textOverflow: 'ellipsis', minWidth: 0, color: `color-mix(in oklch, ${semanticColors.fg} 60%, transparent)`, },})
const personaSecondaryLabelSizes = stylex.create({ '2xs': { display: 'none', }, xs: { display: 'none', }, sm: { fontSize: fontSizes.xs, }, md: { fontSize: fontSizes.xs, }, lg: { fontSize: fontSizes.sm, }, xl: { fontSize: fontSizes.md, }, '2xl': { fontSize: fontSizes.lg, },})
const personaTertiaryLabel = stylex.create({ base: { overflow: 'hidden', whiteSpace: 'nowrap', textOverflow: 'ellipsis', minWidth: 0, color: `color-mix(in oklch, ${semanticColors.fg} 60%, transparent)`, },})
const personaTertiaryLabelSizes = stylex.create({ '2xs': { display: 'none', }, xs: { display: 'none', }, sm: { display: 'none', }, md: { display: 'none', }, lg: { fontSize: fontSizes.sm, }, xl: { fontSize: fontSizes.md, }, '2xl': { fontSize: fontSizes.lg, },})
const personaPresence = stylex.create({ base: {},})
const personaPresenceVariants = stylex.create({ badge: { display: 'flex', alignItems: 'center', justifyContent: 'center', position: 'absolute', bottom: 0, right: 0, width: 'calc(var(--avatar-size) / 3)', height: 'calc(var(--avatar-size) / 3)', transform: 'translate(12%, 12%)', borderWidth: '0.10em', borderRadius: '50%', borderColor: 'var(--presence-border-color)', backgroundColor: 'var(--persona-presence)', }, ring: { outlineWidth: '2px', outlineColor: 'var(--persona-presence)', outlineOffset: '2px', outlineStyle: 'solid', borderRadius: radii.full, position: 'absolute', inset: 0, },})
export type PersonaSize = keyof typeof personaDetailsSizes
export type PersonaVariant = keyof typeof personaPresenceVariants
// defineSlotRecipe validates the shape at compile time: default values must// exist in the axis maps, and slot keys must be `styles` or a declared axisconst personaSlotRecipe = defineSlotRecipe({ slots: { root: { styles: personaRoot }, details: { styles: personaDetails, sizes: personaDetailsSizes }, label: { styles: personaLabel, sizes: personaLabelSizes }, secondaryLabel: { styles: personaSecondaryLabel, sizes: personaSecondaryLabelSizes }, tertiaryLabel: { styles: personaTertiaryLabel, sizes: personaTertiaryLabelSizes }, presence: { styles: personaPresence, variants: personaPresenceVariants }, }, defaultVariants: { size: "md", variant: "badge" },});
/** * Persona — recipe and component colocated in one file. `<Persona.Root * variant size>` resolves the recipe once and every part inherits it through * context; each part composes via `render`. Place your avatar (any * positioned element) as the first child and nest `Persona.Presence` inside * it — the badge sizes off the avatar's `--avatar-size` and both variants * color from `--persona-presence` (set it to a semantic presence token). * Secondary/tertiary labels hide themselves at the smaller sizes. */const { withProvider, withContext } = createSlotRecipeContext(personaSlotRecipe);
export const Persona = { Root: withProvider("div", "root", "PersonaRoot"), Details: withContext("div", "details", "PersonaDetails"), Label: withContext("span", "label", "PersonaLabel"), SecondaryLabel: withContext("span", "secondaryLabel", "PersonaSecondaryLabel"), TertiaryLabel: withContext("span", "tertiaryLabel", "PersonaTertiaryLabel"), Presence: withContext("span", "presence", "PersonaPresence"),};