Components
Presence
An overlapping avatar stack with a +N overflow badge and an online/away/offline status dot. Purely presentational — no Morphos equivalent.
Installation
npx kosmesis add presencepnpm dlx kosmesis add presenceyarn dlx kosmesis add presencebunx kosmesis add presenceCopy and paste the following code into your project.
import { StatelessComponent } from "@praxisjs/core";import { Component } from "@praxisjs/decorators";import { cn } from "@/lib/utils";function getInitials(name: string): string { const parts = name.trim().split(/\s+/); return parts .slice(0, 2) .map((part) => part[0].toUpperCase()) .join("");}export interface PresenceUser { id: string; name: string; avatarUrl?: string;}export interface PresenceProps { users: PresenceUser[]; max?: number; size?: number; class?: string; id?: string;}// Doesn't reuse `Avatar`: Morphos's `Avatar` needs one instance per user, which doesn't compose with a flat `users` array prop.@Component()export class Presence extends StatelessComponent<PresenceProps> { render() { const { users, max = 5, size = 32, class: cls, id } = this.props; const visible = users.slice(0, max); const overflow = users.length - visible.length; return ( <div id={id} role="group" aria-label="Active users" class={cn("flex items-center", cls)}> {visible.map((user, i) => ( <span key={user.id} title={user.name} class="relative -ml-2.5 inline-flex shrink-0 items-center justify-center overflow-hidden rounded-full border-2 border-background bg-muted text-xs font-medium text-muted-foreground first:ml-0" style={{ width: `${String(size)}px`, height: `${String(size)}px`, zIndex: visible.length - i }} > {user.avatarUrl ? <img src={user.avatarUrl} alt={user.name} class="size-full object-cover" /> : getInitials(user.name)} </span> ))} {overflow > 0 && ( <span class="relative -ml-2.5 inline-flex shrink-0 items-center justify-center rounded-full border-2 border-background bg-secondary text-xs font-medium text-secondary-foreground" style={{ width: `${String(size)}px`, height: `${String(size)}px` }} > {`+${String(overflow)}`} </span> )} </div> ); }}export type PresenceStatus = "online" | "away" | "offline";export interface PresenceDotProps { status?: PresenceStatus; class?: string;}@Component()export class PresenceDot extends StatelessComponent<PresenceDotProps> { render() { const { status = "online", class: cls } = this.props; return ( <span data-status={status} class={cn( "inline-block size-2.5 rounded-full ring-2 ring-background", "data-[status=online]:bg-emerald-500 data-[status=away]:bg-yellow-500 data-[status=offline]:bg-muted-foreground/40", cls, )} /> ); }}Install the following dependencies:
npm install @praxisjs/csspnpm add @praxisjs/cssyarn add @praxisjs/cssbun add @praxisjs/cssCopy and paste the following code into your project.
import { StatelessComponent } from "@praxisjs/core";import { cx, Stylesheet, Styled, tokenVars } from "@praxisjs/css";import { Component } from "@praxisjs/decorators";import { KosmesisTokens } from "@/lib/kosmesis-theme";const t = tokenVars(KosmesisTokens);function getInitials(name: string): string { const parts = name.trim().split(/\s+/); return parts .slice(0, 2) .map((part) => part[0].toUpperCase()) .join("");}class PresenceStyles extends Stylesheet { $group = this.css({ display: "flex", alignItems: "center" }); $avatar = this.css({ position: "relative", marginLeft: "-0.625rem", display: "inline-flex", flexShrink: 0, alignItems: "center", justifyContent: "center", overflow: "hidden", borderRadius: "9999px", border: `2px solid ${t.background}`, backgroundColor: t.muted, fontSize: "0.75rem", fontWeight: 500, color: t.mutedForeground, }).on("&:first-child", { marginLeft: 0 }); $image = this.css({ width: "100%", height: "100%", objectFit: "cover" }); $overflow = this.css({ position: "relative", marginLeft: "-0.625rem", display: "inline-flex", flexShrink: 0, alignItems: "center", justifyContent: "center", borderRadius: "9999px", border: `2px solid ${t.background}`, backgroundColor: t.secondary, fontSize: "0.75rem", fontWeight: 500, color: t.secondaryForeground, }); $dot = this.css({ display: "inline-block", height: "0.625rem", width: "0.625rem", borderRadius: "9999px", boxShadow: `0 0 0 2px ${t.background}`, }) .on('&[data-status="online"]', { backgroundColor: "#10b981" }) .on('&[data-status="away"]', { backgroundColor: "#eab308" }) .on('&[data-status="offline"]', { backgroundColor: `color-mix(in oklab, ${t.mutedForeground} 40%, transparent)` });}export interface PresenceUser { id: string; name: string; avatarUrl?: string;}export interface PresenceProps { users: PresenceUser[]; max?: number; size?: number; class?: string; id?: string;}// Doesn't reuse `Avatar`: Morphos's `Avatar` needs one instance per user, which doesn't compose with a flat `users` array prop.@Component()export class Presence extends StatelessComponent<PresenceProps> { @Styled(PresenceStyles) $s!: PresenceStyles; render() { const { users, max = 5, size = 32, class: cls, id } = this.props; const visible = users.slice(0, max); const overflow = users.length - visible.length; return ( <div id={id} role="group" aria-label="Active users" class={cx(this.$s.$group, cls)}> {visible.map((user, i) => ( <span key={user.id} title={user.name} class={this.$s.$avatar} style={{ width: `${String(size)}px`, height: `${String(size)}px`, zIndex: visible.length - i }} > {user.avatarUrl ? <img src={user.avatarUrl} alt={user.name} class={this.$s.$image} /> : getInitials(user.name)} </span> ))} {overflow > 0 && ( <span class={this.$s.$overflow} style={{ width: `${String(size)}px`, height: `${String(size)}px` }}> {`+${String(overflow)}`} </span> )} </div> ); }}export type PresenceStatus = "online" | "away" | "offline";export interface PresenceDotProps { status?: PresenceStatus; class?: string;}@Component()export class PresenceDot extends StatelessComponent<PresenceDotProps> { @Styled(PresenceStyles) $s!: PresenceStyles; render() { const { status = "online", class: cls } = this.props; return <span data-status={status} class={cx(this.$s.$dot, cls)} />; }}Examples
About
Purely presentational — no Morphos equivalent. Deliberately doesn't reuse Avatar: Morphos's
Avatar needs one instance per user (to track image load/error state), which doesn't compose
cleanly with a flat users array prop, so image/initials fallback is handled locally instead.
PresenceDot is a separate small status indicator meant to sit on an Avatar's corner.
Usage
import { Presence, PresenceDot } from "@/components/ui/presence";
<Presence users={[{ id: "1", name: "Ada Lovelace" }, { id: "2", name: "Grace Hopper" }]} max={4} />
<PresenceDot status="online" class="absolute right-0 bottom-0" />Props
Presence
| Prop | Type | Default |
|---|---|---|
users | { id: string; name: string; avatarUrl?: string }[] | — |
max | number | 5 |
size | number | 32 |
class | string | — |
PresenceDot
| Prop | Type | Default |
|---|---|---|
status | "online" | "away" | "offline" | "online" |
class | string | — |