Chat Header
- Stable
- WCAG 2.2 evidence
- RTL
The top bar of a conversation: avatar, the name as a heading, a status line and actions such as
Search and More. Accessibility: the name is a heading (WCAG 1.3.1, 2.4.6) and the status is text (1.4.1); the avatar is decorative
(1.1.1); text wraps instead of truncating (1.4.10, 1.4.12); colours meet 4.5:1 in Light and Dark (1.4.3). Logical
layout, so it mirrors in right-to-left. It is not a landmark: put it inside the conversation's section or main.
Presentational: renders in React Server Components; the actions are client islands. Consumer duties: translated, specific aria-labels on the icon buttons (and a Tooltip with the same text); a heading
level that fits the page; use a top app bar for page headers on mobile.
import { ChatHeader } from "@nexera-ui/react";- 5
- examples
- 5
- props
- 0
- live controls
- 1
- platform
- 7
- WCAG criteria
- 3
- blocks use it
Try every prop. Copy the code.
Change the props and the code updates. Check light and dark, LTR and RTL, and phone width.
import { ChatHeader } from "@nexera-ui/react";
<ChatHeader />Examples 4
The same examples as Storybook, rendered live. Open Code to copy one.
With menu
The More button opens a Menu.
import { Avatar, ChatHeader, IconButton, Menu, MenuItem, Presence } from "@nexera-ui/react";
import { LuEllipsis, LuSearch } from "react-icons/lu";
<ChatHeader
name="Sara Malik"
status="Head of People · Online"
avatar={<Avatar size="md" initials="SM" isDecorative presence={<Presence isDecorative />} />}
actions={
<>
<IconButton variant="ghost" aria-label="Search in conversation" icon={<LuSearch />} />
<Menu
trigger={<IconButton variant="ghost" aria-label="More options" icon={<LuEllipsis />} />}
>
<MenuItem id="mute" label="Mute notifications" />
<MenuItem id="profile" label="View profile" />
</Menu>
</>
}
/>;
Long content
A long name and status wrap; the actions keep their place.
import { Avatar, ChatHeader, IconButton, Presence } from "@nexera-ui/react";
import { LuEllipsis, LuSearch } from "react-icons/lu";
const actions = (
<>
<IconButton variant="ghost" aria-label="Search in conversation" icon={<LuSearch />} />
<IconButton variant="ghost" aria-label="More options" icon={<LuEllipsis />} />
</>
);
<div className="w-full max-w-xs">
<ChatHeader
name="Sara Malik-Rahman, Head of People Operations"
status="Head of People · Online · Lahore office · 9:00 to 17:00 PKT"
avatar={<Avatar size="md" initials="SM" isDecorative presence={<Presence isDecorative />} />}
actions={actions}
/>
</div>;
Right to left
Right-to-left: the avatar is on the right, the actions on the left.
import { Avatar, ChatHeader, IconButton, Presence } from "@nexera-ui/react";
import { LuEllipsis, LuSearch } from "react-icons/lu";
const actions = (
<>
<IconButton variant="ghost" aria-label="Search in conversation" icon={<LuSearch />} />
<IconButton variant="ghost" aria-label="More options" icon={<LuEllipsis />} />
</>
);
<ChatHeader
name="سارة مالك"
status="رئيسة شؤون الموظفين · متصلة"
avatar={<Avatar size="md" initials="SM" isDecorative presence={<Presence isDecorative />} />}
actions={actions}
/>;
Full HR helpdesk
import { useState } from "react";
import {
Avatar,
Badge,
Button,
ChatHeader,
Composer,
ConversationItem,
DateSeparator,
IconButton,
Menu,
MenuItem,
MessageBubble,
Presence,
SearchField,
SystemMessage,
Tooltip,
TooltipTrigger,
TypingIndicator,
} from "@nexera-ui/react";
import { LuEllipsis, LuSearch } from "react-icons/lu";
function HelpdeskExample() {
const [person, setPerson] = useState("Ayesha Khan");
const [query, setQuery] = useState("");
const [resolved, setResolved] = useState(false);
const [messages, setMessages] = useState<Record<string, string[]>>({});
const conversations = [
{
name: "Ayesha Khan",
initials: "AK",
preview: "Can I take Friday off for a family event?",
time: "10:24",
unread: 0,
},
{
name: "Bilal Ahmed",
initials: "BA",
preview: "My payslip shows the wrong overtime",
time: "09:58",
unread: 2,
},
{
name: "Hina Raza",
initials: "HR",
preview: "Thanks, the laptop arrived.",
time: "Yesterday",
unread: 0,
},
{
name: "#people-ops",
initials: "#",
preview: "Zainab: interview panel updated",
time: "Yesterday",
unread: 0,
},
];
const current = conversations.find((c) => c.name === person) ?? {
name: "Ayesha Khan",
initials: "AK",
preview: "",
time: "",
unread: 0,
};
return (
<section
aria-label="Full HR helpdesk"
className="box-border flex w-[1280px] flex-col gap-4 rounded-xl border border-default bg-surface p-6 text-primary"
>
<h3 className="m-0 text-base font-semibold leading-6">HR helpdesk</h3>
<p className="m-0 text-[13px] leading-[18px] text-muted">
Inbox, conversation and ticket details side by side. System messages track status changes.
</p>
<div className="flex h-[634px] overflow-hidden rounded-xl border border-default">
<aside
aria-label="Inbox"
className="box-border flex w-[320px] shrink-0 flex-col gap-1 border-e border-default p-3"
>
<SearchField
aria-label="Search conversations"
placeholder="Search conversations"
value={query}
onChange={setQuery}
shortcut="Ctrl K"
/>
{conversations
.filter((c) => c.name.toLowerCase().includes(query.toLowerCase()))
.map((c) => (
<ConversationItem
key={c.name}
name={c.name}
preview={c.preview}
time={c.time}
unread={c.unread}
isCurrent={person === c.name}
avatar={
<Avatar
size="lg"
initials={c.initials}
isDecorative
presence={<Presence isDecorative />}
/>
}
onPress={() => {
setPerson(c.name);
}}
/>
))}
</aside>
<section
aria-label={`Conversation with ${person}`}
className="flex min-w-0 flex-1 flex-col"
>
<ChatHeader
name={person}
status={`Ticket #1042 · Leave · ${resolved ? "Resolved" : "Open"}`}
avatar={
<Avatar
size="md"
initials={current.initials}
isDecorative
presence={<Presence isDecorative />}
/>
}
actions={
<>
<TooltipTrigger delay={200}>
<IconButton
variant="ghost"
aria-label="Search in conversation"
icon={<LuSearch />}
onPress={() => {
setQuery(person);
}}
/>
<Tooltip>Search in conversation</Tooltip>
</TooltipTrigger>
<Menu
trigger={
<IconButton variant="ghost" aria-label="More options" icon={<LuEllipsis />} />
}
>
<MenuItem
id="resolve"
label={resolved ? "Reopen ticket" : "Resolve ticket"}
onAction={() => {
setResolved((v) => !v);
}}
/>
</Menu>
</>
}
/>
<div
role="log"
aria-label="Messages"
className="flex min-h-0 flex-1 flex-col gap-[6px] overflow-auto p-4"
>
<DateSeparator label="Today" />
{person === "Ayesha Khan" ? (
<>
<MessageBubble
name="Ayesha Khan"
className="[&>div:last-child]:max-w-[360px]"
time="10:24"
avatar={<Avatar size="md" initials="AK" isDecorative />}
>
Hi Sara, can I take Friday 17 Oct off for a family event?
</MessageBubble>
<MessageBubble className="ps-10" group="continued" name="Ayesha Khan" time="10:24">
I have 12 days of annual leave left.
</MessageBubble>
<SystemMessage className="self-start">
Sara Malik assigned this ticket to herself
</SystemMessage>
<MessageBubble
direction="outgoing"
type="reply"
className="[&>div:last-child]:max-w-[225px]"
quote="Can I take Friday 17 Oct off?"
quoteAuthor="Sara Malik"
name="Sara Malik"
time="10:31"
status="read"
>
Approved. Enjoy the long weekend.
</MessageBubble>
<MessageBubble
direction="outgoing"
group="continued"
name="Sara Malik"
time="10:31"
status="read"
>
I have added it to the team calendar.
</MessageBubble>
</>
) : (
<MessageBubble
name={person}
time={current.time}
avatar={<Avatar size="md" initials={current.initials} isDecorative />}
>
{current.preview}
</MessageBubble>
)}
{(messages[person] ?? []).map((message, i) => (
<MessageBubble
key={i}
direction="outgoing"
name="Sara Malik"
time="Now"
status="sent"
>
{message}
</MessageBubble>
))}
{!resolved && <TypingIndicator>{person.split(" ")[0]} is typing</TypingIndicator>}
</div>
<div className="border-t border-default">
<Composer
key={person}
aria-label={`Reply to ${person}`}
placeholder={`Reply to ${person}`}
onAttach={() => {
setMessages((previous) => ({
...previous,
[person]: [...(previous[person] ?? []), "Attachment: Leave request.pdf"],
}));
}}
onEmoji={() => {
setMessages((previous) => ({
...previous,
[person]: [...(previous[person] ?? []), "👍"],
}));
}}
onRecord={() => {
setMessages((previous) => ({
...previous,
[person]: [...(previous[person] ?? []), "Voice message · 0:03"],
}));
}}
onSend={(message) => {
setMessages((previous) => ({
...previous,
[person]: [...(previous[person] ?? []), message],
}));
}}
/>
</div>
</section>
<aside
aria-label="Ticket details"
className="box-border flex w-[280px] shrink-0 flex-col gap-4 border-s border-default p-4"
>
<div className="flex items-center justify-between">
<h4 className="m-0 text-sm font-semibold">Ticket #1042</h4>
</div>
<dl className="m-0 flex flex-col gap-3 text-[13px]">
<div className="flex items-center gap-4">
<dt className="w-[72px] text-muted">Status</dt>
<dd className="m-0">
<Badge tone="success" label={resolved ? "Resolved" : "Open"} />
</dd>
</div>
{[
["Category", "Leave"],
["Priority", "Normal"],
["Assignee", "Sara Malik"],
["SLA", "Reply within 4h"],
].map(([label, value]) => (
<div key={label} className="flex items-center gap-4">
<dt className="w-[72px] text-muted">{label}</dt>
<dd className="m-0 font-medium">{value}</dd>
</div>
))}
</dl>
<Button
variant="secondary"
onPress={() => {
setResolved((v) => !v);
}}
>
{resolved ? "Reopen ticket" : "Resolve ticket"}
</Button>
</aside>
</div>
</section>
);
}
export function FullHRHelpdesk() {
return <HelpdeskExample />;
}
Props 5
5 props shown
name*NexeraReactNodeThe person or channel the conversation is with. Rendered as a heading so the conversation can be found by heading navigation (WCAG 1.3.1, 2.4.6). Wraps; never truncated.
statusNexeraReactNodeSecondary line: role, availability or member count. Write availability here as text: the avatar's presence dot is colour only (WCAG 1.4.1).
avatarNexeraReactNodeAvatar: pass
<Avatar size="md" isDecorative presence={<Presence isDecorative />}... />. Decorative because the name and the
status text are next to it.
actionsNexeraReactNodeActions at the inline end: pass named icon buttons, for example
<IconButton variant="ghost" aria-label="Search in conversation" icon={<IconSearch />} /> and a Menu trigger. They
are client components; the header itself stays a server component.
headingLevelNexera1 | 2 | 3 | 4 | 5 | 6Heading level of the name. Choose the level that fits the page outline (for example 2 under the page's h1).
* Required. React Aria props shown are the ones most apps use; the component accepts the rest of its React Aria props too.
Accessible by default.
Built on React Aria, and covered by the WCAG 2.2 evidence generated on every build.
WCAG 2.2 evidence
5 direct · 2 supporting- 1.1.1Non-text ContentLevel A · tested directly
- 1.3.1Info and RelationshipsLevel A · tested directly
- 1.4.10ReflowLevel AA · supporting test
- 1.4.12Text SpacingLevel AA · supporting test
- 2.1.1KeyboardLevel A · tested directly
- 2.4.6Headings and LabelsLevel AA · tested directly
- 4.1.2Name, Role, ValueLevel A · tested directly
Fits any width.
Nexera components respond to the space they are given. Drag the corner of the frame, or pick a width.
Styling hooks
Pass className to add Tailwind classes (merged last). State is exposed as data attributes, so you can style it with variants like data-pressed:.
<ChatHeader className="shadow-sm" />Used in blocks
- ChatMessaging and collaboration
- InboxMessaging and collaboration
- Helpdesk assistantMessaging and collaboration
Related components
- ComposerThe message box under a conversation: a growing textarea with attach, emoji and voice buttons, a Send button, a "replying to" bar, attachments and a voice recording row.
- ConversationItemOne conversation in a list: avatar, name, time, the last message and an unread count.
- DateSeparatorMarks where a new day starts in a conversation: a label between two lines.
- MessageBubbleOne chat message: `Direction` (incoming, outgoing) x `Type` (text, reply, file, image) x `Group` (single, continued), with sender name and avatar, time, an edited mark and read ticks.
- ReactionA reaction under a message, such as a thumbs up, that the user can add or remove.
- SystemMessageAn event in a conversation that nobody wrote, such as someone joining or a channel being renamed: a small muted pill with an optional icon.
- TypingIndicatorShows that someone is writing a reply: three dots in a bubble and a short text.