Event Popover
- Stable
- WCAG 2.2 evidence
- RTL
The details of one event in a non-modal panel anchored to its trigger: title, time, join action,
location, guests and an RSVP row, with a close button and header actions. It follows Popover: React Aria
DialogTrigger and Popover (isNonModal) with its own role="dialog" panel, named by the title and described by
the time. The trigger exposes aria-expanded and aria-controls; focus moves into the panel when it opens and back to the
trigger when Escape, the close button or a slot="close" action closes it (WCAG 2.1.1, 2.4.3, 4.1.2). Nothing is trapped: Tab
past the last control leaves the panel for the element after the trigger and closes it, a click outside closes it, and the
rest of the page stays available (2.1.2). Placement flips and the panel stays inside the viewport and scrolls inside when it
is too tall (1.4.10, 2.4.11). The icon buttons (actions), the join action and the RSVP answers are the interactive parts; the rest is text. Guests'
answers are icons that also have a text alternative (1.1.1, 1.4.1), and the RSVP choice has a check mark besides the colour. Consumer duties: the wording and time formatting (when in the user's locale and time zone), accessible names of the action
buttons, what edit, delete and RSVP do, and not showing guest data to people who may not see it. To edit the event use a
Modal or Drawer, not this panel.
import { EventPopover } from "@nexera-ui/react";- 6
- examples
- 30
- props
- 4
- live controls
- 1
- platform
- 13
- 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 { EventPopover } from "@nexera-ui/react";
<EventPopover />Examples 5
The same examples as Storybook, rendered live. Open Code to copy one.
Minimal
Only what every event has: title, time and the close button. Rows appear when their props are given.
import { Button, CalendarEvent, EventPopover, IconButton } from "@nexera-ui/react";
import { LuEllipsis, LuPencil, LuTrash2 } from "react-icons/lu";
const zone = "Asia/Karachi";
const start = new Date("2026-10-16T05:30:00Z");
const end = new Date("2026-10-16T06:15:00Z");
const guests: EventPopoverGuests = {
summary: "4 guests · 2 yes, 1 awaiting, 1 no",
items: [
{ name: "Zainab Ali", role: "Organizer", response: "yes" },
{ name: "Bilal Ahmed", role: "Interviewer", response: "yes" },
{ name: "Zara Siddiqui", role: "Candidate", response: "awaiting" },
{ name: "Omar Farooq", role: "Optional", response: "no" },
],
};
const rsvp: EventPopoverRsvp = {
label: "Going?",
defaultValue: "yes",
options: [
{ value: "yes", label: "Yes" },
{ value: "maybe", label: "Maybe" },
{ value: "no", label: "No" },
],
};
const actions = (
<>
<IconButton variant="ghost" size="sm" aria-label="Edit event" icon={<LuPencil />} />
<IconButton
variant="ghost"
size="sm"
aria-label="Delete event"
icon={<LuTrash2 />}
slot="close"
/>
<IconButton variant="ghost" size="sm" aria-label="More actions" icon={<LuEllipsis />} />
</>
);
const trigger = (
<CalendarEvent title="Interview: Zara Siddiqui" start={start} end={end} timeZone={zone} />
);
<EventPopover
title="Interview: Zara Siddiqui"
when="Thursday 16 October · 10:30 to 11:15"
meta="Does not repeat · Pakistan Standard Time"
actions={actions}
join={<Button size="sm">Join with video</Button>}
location="Lahore HQ, Room Margalla (8 seats)"
guests={guests}
rsvp={rsvp}
defaultOpen
placement="bottom start"
>
{trigger}
</EventPopover>;
Calendar tone
calendarTone colours the swatch before the title; name the calendar in meta, because the colour alone does not.
import { Button, CalendarEvent, EventPopover, IconButton } from "@nexera-ui/react";
import { LuEllipsis, LuPencil, LuTrash2 } from "react-icons/lu";
const zone = "Asia/Karachi";
const start = new Date("2026-10-16T05:30:00Z");
const end = new Date("2026-10-16T06:15:00Z");
const guests: EventPopoverGuests = {
summary: "4 guests · 2 yes, 1 awaiting, 1 no",
items: [
{ name: "Zainab Ali", role: "Organizer", response: "yes" },
{ name: "Bilal Ahmed", role: "Interviewer", response: "yes" },
{ name: "Zara Siddiqui", role: "Candidate", response: "awaiting" },
{ name: "Omar Farooq", role: "Optional", response: "no" },
],
};
const rsvp: EventPopoverRsvp = {
label: "Going?",
defaultValue: "yes",
options: [
{ value: "yes", label: "Yes" },
{ value: "maybe", label: "Maybe" },
{ value: "no", label: "No" },
],
};
const actions = (
<>
<IconButton variant="ghost" size="sm" aria-label="Edit event" icon={<LuPencil />} />
<IconButton
variant="ghost"
size="sm"
aria-label="Delete event"
icon={<LuTrash2 />}
slot="close"
/>
<IconButton variant="ghost" size="sm" aria-label="More actions" icon={<LuEllipsis />} />
</>
);
const trigger = (
<CalendarEvent title="Interview: Zara Siddiqui" start={start} end={end} timeZone={zone} />
);
<EventPopover
title="Annual leave"
when="Thursday 16 October · 10:30 to 11:15"
meta="Personal calendar · All day"
actions={actions}
join={<Button size="sm">Join with video</Button>}
location="Lahore HQ, Room Margalla (8 seats)"
guests={guests}
rsvp={rsvp}
defaultOpen
placement="bottom start"
calendarTone="leave"
>
{trigger}
</EventPopover>;
Controlled rsvp
The RSVP row is a single-choice radio group; controlled here through value and onChange.
import { useState } from "react";
import { Button, EventPopover, IconButton } from "@nexera-ui/react";
import { LuEllipsis, LuPencil, LuTrash2 } from "react-icons/lu";
const start = new Date("2026-10-16T05:30:00Z");
const guests: EventPopoverGuests = {
summary: "4 guests · 2 yes, 1 awaiting, 1 no",
items: [
{ name: "Zainab Ali", role: "Organizer", response: "yes" },
{ name: "Bilal Ahmed", role: "Interviewer", response: "yes" },
{ name: "Zara Siddiqui", role: "Candidate", response: "awaiting" },
{ name: "Omar Farooq", role: "Optional", response: "no" },
],
};
const actions = (
<>
<IconButton variant="ghost" size="sm" aria-label="Edit event" icon={<LuPencil />} />
<IconButton
variant="ghost"
size="sm"
aria-label="Delete event"
icon={<LuTrash2 />}
slot="close"
/>
<IconButton variant="ghost" size="sm" aria-label="More actions" icon={<LuEllipsis />} />
</>
);
export function ControlledRsvp() {
const [answer, setAnswer] = useState("maybe");
return (
<div className="flex flex-col gap-3">
<EventPopover
title="Interview: Zara Siddiqui"
when="Thursday 16 October · 10:30 to 11:15"
meta="Does not repeat · Pakistan Standard Time"
actions={actions}
join={<Button size="sm">Join with video</Button>}
location="Lahore HQ, Room Margalla (8 seats)"
guests={guests}
defaultOpen
placement="bottom start"
rsvp={{ ...rsvp, defaultValue: undefined, value: answer, onChange: setAnswer }}
/>
<p className="m-0 text-body-small text-secondary" aria-live="polite">
Your answer: {answer}
</p>
</div>
);
}
Long content
Long titles, locations and guest names wrap; the panel scrolls inside itself when it is taller than the viewport.
import { Button, CalendarEvent, EventPopover, IconButton } from "@nexera-ui/react";
import { LuEllipsis, LuPencil, LuTrash2 } from "react-icons/lu";
const zone = "Asia/Karachi";
const start = new Date("2026-10-16T05:30:00Z");
const end = new Date("2026-10-16T06:15:00Z");
const guests: EventPopoverGuests = {
summary: "4 guests · 2 yes, 1 awaiting, 1 no",
items: [
{ name: "Zainab Ali", role: "Organizer", response: "yes" },
{ name: "Bilal Ahmed", role: "Interviewer", response: "yes" },
{ name: "Zara Siddiqui", role: "Candidate", response: "awaiting" },
{ name: "Omar Farooq", role: "Optional", response: "no" },
],
};
const rsvp: EventPopoverRsvp = {
label: "Going?",
defaultValue: "yes",
options: [
{ value: "yes", label: "Yes" },
{ value: "maybe", label: "Maybe" },
{ value: "no", label: "No" },
],
};
const actions = (
<>
<IconButton variant="ghost" size="sm" aria-label="Edit event" icon={<LuPencil />} />
<IconButton
variant="ghost"
size="sm"
aria-label="Delete event"
icon={<LuTrash2 />}
slot="close"
/>
<IconButton variant="ghost" size="sm" aria-label="More actions" icon={<LuEllipsis />} />
</>
);
const trigger = (
<CalendarEvent title="Interview: Zara Siddiqui" start={start} end={end} timeZone={zone} />
);
<EventPopover
title="Final-round panel interview with the head of payroll operations and two regional leads"
when="Thursday 16 October · 10:30 to 11:15"
meta="Does not repeat · Pakistan Standard Time"
actions={actions}
join={<Button size="sm">Join with video</Button>}
location="Lahore HQ, Room Margalla on the third floor of the east wing, next to the visitor reception and the cafeteria"
guests={{
summary: "3 guests",
items: [
{
name: "Muhammad Abdul Rahman bin Abdul Aziz Al-Fahad",
role: "Head of payroll operations",
response: "yes",
},
{ name: "Zara Siddiqui", role: "Candidate", response: "awaiting" },
{ name: "Omar Farooq", response: "no" },
],
}}
rsvp={rsvp}
defaultOpen
placement="bottom start"
>
{trigger}
</EventPopover>;
Right to left
Right-to-left (Arabic): the actions and close button move to the left, rows mirror, and the panel's placement follows the reading direction.
import { Button, CalendarEvent, EventPopover, IconButton } from "@nexera-ui/react";
import { LuEllipsis, LuPencil, LuTrash2 } from "react-icons/lu";
const start = new Date("2026-10-16T05:30:00Z");
const actions = (
<>
<IconButton variant="ghost" size="sm" aria-label="Edit event" icon={<LuPencil />} />
<IconButton
variant="ghost"
size="sm"
aria-label="Delete event"
icon={<LuTrash2 />}
slot="close"
/>
<IconButton variant="ghost" size="sm" aria-label="More actions" icon={<LuEllipsis />} />
</>
);
<EventPopover
title="مقابلة: زارا صديقي"
when="الخميس ١٦ أكتوبر · ١٠:٣٠ إلى ١١:١٥"
meta="لا يتكرر · توقيت باكستان"
actions={actions}
join={<Button size="sm">انضم إلى الاجتماع</Button>}
location="المقر الرئيسي في لاهور، قاعة مرگلہ"
guests={{
summary: "٣ ضيوف",
items: [
{ name: "زينب علي", role: "المنظم", response: "yes" },
{ name: "زارا صديقي", role: "المرشحة", response: "awaiting" },
{ name: "عمر فاروق", role: "اختياري", response: "no" },
],
}}
rsvp={{
label: "هل ستحضر؟",
defaultValue: "yes",
options: [
{ value: "yes", label: "نعم" },
{ value: "maybe", label: "ربما" },
{ value: "no", label: "لا" },
],
}}
defaultOpen
placement="bottom start"
closeLabel="إغلاق"
labels={{ yes: "قبل", awaiting: "في انتظار الرد", no: "رفض" }}
>
{<CalendarEvent title="مقابلة: زارا صديقي" />}
</EventPopover>;
Props 30
Press "Try it" on a card to load that prop into the playground.
30 props shown
children*NexeraReactNodeThe trigger: one pressable element, usually a CalendarEvent or a Nexera Button. It gets aria-expanded and
aria-controls, toggles the popover when pressed and receives focus back when the popover closes.
title*NexeraReactNodeEvent title. A level 2 heading that names the dialog; wraps.
when*NexeraReactNodeWhen the event happens. The dialog's description
(aria-describedby). Write it with Intl.DateTimeFormat for the user's locale, as CalendarEvent does.
metaNexeraReactNodeRecurrence, time zone or calendar name. Shown when provided. Name the calendar here: the swatch colour alone does not (WCAG 1.4.1).
calendarToneNexera"info" | "leave" | "success" | "warning" | "danger" | "remote" | "brand"Colour of the 14 px swatch before the title: the calendar's colour. Decorative.
actionsNexeraReactNodeIcon buttons before the close button: for example
IconButtons with variant="ghost" and size="sm", each with an aria-label. Give one slot="close" to close the
popover when it is pressed. The close button itself is always there.
closeLabelNexerastringAccessible name of the close button. Translate it for your locale.
joinNexeraReactNodeThe join action, for example a Button or a link.
The video icon is drawn before it. Shown when provided.
locationNexeraReactNodeWhere the event takes place (Figma Location, TEXT; sample "Lahore HQ, Room Margalla (8 seats)"). The pin icon is drawn
before it. Shown when provided.
guestsNexeraEventPopoverGuestsThe attendees: a summary line and the guests. Shown when provided.
rsvpNexeraEventPopoverRsvpThe RSVP row. A single-choice group (React Aria RadioGroup): one Tab
stop, arrows choose, with a check mark on the chosen answer. Shown when provided.
labelsNexeraPartial<EventPopoverLabels>Translate the words announced for each guest's answer.
isOpenNexerabooleanWhether the popover is open (controlled). Use with onOpenChange.
defaultOpenNexerabooleanWhether the popover is open on first render (uncontrolled).
onOpenChangeNexera(isOpen: boolean) => voidCalled when the popover opens or closes.
classNameNexerastringExtra classes for the surface, merged last so they win over the defaults.
styleNexeraCSSPropertiesInline style for the surface.
placementReact Aria"bottom" | "bottom left" | "bottom right" | "bottom start" | "bottom end" | "top" | "top left" | "top right" | "top start" | "top end" | "left" | "left top" | "left bottom" | "start" | "start top" | "start bottom" | "right" | "right top" | "right bottom" | "end" | "end top" | "end bottom"The placement of the element with respect to its anchor element.
containerPaddingReact ArianumberThe placement padding that should be applied between the element and its surrounding container.
offsetReact ArianumberThe additional offset applied along the main axis between the element and its anchor element.
crossOffsetReact ArianumberThe additional offset applied along the cross axis between the element and its anchor element.
shouldFlipReact AriabooleanWhether the element should flip its orientation (e.g. top to bottom or left to right) when there is insufficient room for it to render completely.
boundaryElementReact AriaElementElement that that serves as the positioning boundary.
scrollRefReact AriaRefObject<Element | null>A ref for the scrollable region within the overlay.
shouldUpdatePositionReact AriabooleanWhether the overlay should update its position automatically.
maxHeightReact ArianumberThe maxHeight specified for the overlay element. By default, it will take all space up to the current viewport height.
arrowBoundaryOffsetReact ArianumberThe minimum distance the arrow's edge should be from the edge of the overlay element.
getTargetRectReact Aria(target: Element) => DOMRect | nullOverrides the target element's bounding rectangle. Useful for positioning relative to a specific point such as the mouse cursor (e.g. context menus) or text selection. @param target - The target element.
isKeyboardDismissDisabledReact AriabooleanWhether pressing the escape key to close the popover should be disabled. Most popovers should not use this option. When set to true, an alternative way to close the popover with a keyboard must be provided.
shouldCloseOnInteractOutsideReact Aria(element: Element) => booleanWhen user interacts with the argument element outside of the popover ref, return true if onClose should be called. This gives you a chance to filter out interaction with elements that should not dismiss the popover. By default, onClose will always be called on interaction outside the popover ref.
* 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
7 direct · 6 supporting- 1.1.1Non-text ContentLevel A · tested directly
- 1.3.1Info and RelationshipsLevel A · tested directly
- 1.3.2Meaningful SequenceLevel A · supporting test
- 1.4.1Use of ColorLevel A · tested directly
- 1.4.10ReflowLevel AA · supporting test
- 1.4.12Text SpacingLevel AA · supporting test
- 2.1.1KeyboardLevel A · tested directly
- 2.1.2No Keyboard TrapLevel A · tested directly
- 2.3.3Animation from InteractionsLevel AAA · supporting test
- 2.4.3Focus OrderLevel A · tested directly
- 2.4.7Focus VisibleLevel AA · supporting test
- 2.5.8Target Size (Minimum)Level AA · supporting test
- 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:.
data-disableddata-hovereddata-presseddata-selected
<EventPopover className="data-disabled:opacity-90 shadow-sm" />Used in blocks
- Month calendarScheduling and calendar
- Week calendarScheduling and calendar
- Event detailsScheduling and calendar
Related components
- AccordionItemOne collapsible section of an FAQ or settings page.
- AreaChartA stacked area chart: layers piled up over time with the title, range switch, actions and legend of the card.
- AvailabilityRowOne person's availability across a window of the day: an avatar with initials and the name, and a timeline with the busy periods drawn as bars.
- AvatarA person or organisation shown as a photo, initials or an icon.
- AvatarGroupA row of overlapping avatars with a "+N" chip for the rest.
- AvatarLabelAn avatar with a name and an optional subtitle beside it, for a person in a list, a header or a mention.
- BadgeShort status or category label: `Tone` x `Variant` x `Size`, with an optional dot, leading icon and remove button.
- BadgeGroupA pill that pairs a small badge with a short message, for an announcement or a status line.