Popover
A composable floating panel anchored to a trigger element. Closes on outside click or Escape. All visual styling is supplied through Tailwind utility classes, giving you full control over the panel's appearance.
Overview
MPopover is a behavioral primitive composed of three parts: MPopover (state owner),
MPopoverTrigger (toggle button), and MPopoverContent (the panel).
No default styles are applied — the consumer provides all visual treatment through Class and OverlayClass.
Key Behaviors:
- Internal open/close/toggle state cascaded via
PopoverContext - Trigger exposes
aria-haspopup="dialog"andaria-expanded - Panel renders with
role="dialog",aria-modal="false", andtabindex="-1" - Placement stored as
data-placementattribute for CSS targeting - Outside-click dismissal via a consumer-styled overlay
- Escape key dismissal
- No built-in positioning, sizing, color, or JS interop
Usage
Add the namespace import:
@using Marai.UI.Components.MPopoverBasic Usage
Wrap MPopoverTrigger and MPopoverContent inside MPopover.
Supply relative positioning on MPopover and absolute placement classes on MPopoverContent.
Provide OverlayClass to enable outside-click dismissal.
<MPopover Class="relative inline-block">
<MPopoverTrigger Class="inline-flex items-center rounded-md bg-primary px-4 py-2 text-sm font-medium text-primary-foreground hover:bg-primary/90">
Open Popover
</MPopoverTrigger>
<MPopoverContent Class="absolute top-full left-1/2 -translate-x-1/2 mt-2 z-50 w-64 rounded-md border bg-background p-4 text-foreground shadow-md"
OverlayClass="fixed inset-0 z-40">
<p class="text-sm">This is popover content. You can place anything here including forms, lists, or rich text.</p>
</MPopoverContent>
</MPopover>
Examples
Placement
Pass the Placement enum to MPopoverContent to record behavioral intent as a data-placement attribute.
Apply the matching Tailwind positioning classes on Class to achieve the visual placement.
<div class="flex flex-wrap gap-8 items-center justify-center py-8">
<MPopover Class="relative inline-block">
<MPopoverTrigger Class="inline-flex items-center rounded-md border px-3 py-1.5 text-sm hover:bg-accent">Top</MPopoverTrigger>
<MPopoverContent Placement="PopoverPlacement.Top"
Class="absolute bottom-full left-1/2 -translate-x-1/2 mb-2 z-50 w-48 rounded-md border bg-background p-3 text-foreground shadow-md"
OverlayClass="fixed inset-0 z-40">
<p class="text-sm">Appears above the trigger</p>
</MPopoverContent>
</MPopover>
<MPopover Class="relative inline-block">
<MPopoverTrigger Class="inline-flex items-center rounded-md border px-3 py-1.5 text-sm hover:bg-accent">Bottom</MPopoverTrigger>
<MPopoverContent Placement="PopoverPlacement.Bottom"
Class="absolute top-full left-1/2 -translate-x-1/2 mt-2 z-50 w-48 rounded-md border bg-background p-3 text-foreground shadow-md"
OverlayClass="fixed inset-0 z-40">
<p class="text-sm">Appears below the trigger (default)</p>
</MPopoverContent>
</MPopover>
<MPopover Class="relative inline-block">
<MPopoverTrigger Class="inline-flex items-center rounded-md border px-3 py-1.5 text-sm hover:bg-accent">Left</MPopoverTrigger>
<MPopoverContent Placement="PopoverPlacement.Left"
Class="absolute right-full top-1/2 -translate-y-1/2 mr-2 z-50 w-48 rounded-md border bg-background p-3 text-foreground shadow-md"
OverlayClass="fixed inset-0 z-40">
<p class="text-sm">Appears to the left</p>
</MPopoverContent>
</MPopover>
<MPopover Class="relative inline-block">
<MPopoverTrigger Class="inline-flex items-center rounded-md border px-3 py-1.5 text-sm hover:bg-accent">Right</MPopoverTrigger>
<MPopoverContent Placement="PopoverPlacement.Right"
Class="absolute left-full top-1/2 -translate-y-1/2 ml-2 z-50 w-48 rounded-md border bg-background p-3 text-foreground shadow-md"
OverlayClass="fixed inset-0 z-40">
<p class="text-sm">Appears to the right</p>
</MPopoverContent>
</MPopover>
</div>
With Form Content
Popovers can contain forms, inputs, and interactive elements.
<MPopover Class="relative inline-block">
<MPopoverTrigger Class="inline-flex items-center rounded-md bg-primary px-4 py-2 text-sm font-medium text-primary-foreground hover:bg-primary/90">
Edit Profile
</MPopoverTrigger>
<MPopoverContent Class="absolute top-full left-1/2 -translate-x-1/2 mt-2 z-50 w-72 rounded-md border bg-background p-4 text-foreground shadow-md"
OverlayClass="fixed inset-0 z-40">
<div class="flex flex-col gap-3">
<div>
<MLabel For="popover-name">Display Name</MLabel>
<MInput id="popover-name" Class="mt-1 w-full rounded-md border px-3 py-1.5 text-sm" Placeholder="Enter your name" />
</div>
<div>
<MLabel For="popover-email">Email</MLabel>
<MInput id="popover-email" Type="email" Class="mt-1 w-full rounded-md border px-3 py-1.5 text-sm" Placeholder="email@example.com" />
</div>
<MButton Class="inline-flex items-center justify-center rounded-md bg-primary px-3 py-1.5 text-sm font-medium text-primary-foreground hover:bg-primary/90">Save Changes</MButton>
</div>
</MPopoverContent>
</MPopover>
Icon Trigger
Supply icon markup directly inside MPopoverTrigger and size the button through Class.
<MPopover Class="relative inline-block">
<MPopoverTrigger Class="inline-flex h-9 w-9 items-center justify-center rounded-full border hover:bg-accent" aria-label="More options">
<svg width="16" height="16" fill="none" stroke="currentColor" stroke-width="2"
stroke-linecap="round" viewBox="0 0 24 24" aria-hidden="true">
<circle cx="12" cy="12" r="1"/>
<circle cx="12" cy="5" r="1"/>
<circle cx="12" cy="19" r="1"/>
</svg>
</MPopoverTrigger>
<MPopoverContent Class="absolute top-full left-1/2 -translate-x-1/2 mt-2 z-50 w-48 rounded-md border bg-background p-3 text-foreground shadow-md"
OverlayClass="fixed inset-0 z-40">
<p class="text-sm">More options menu</p>
</MPopoverContent>
</MPopover>
Fully Custom Visual Treatment
Any visual design is achievable — gradient panels, colored borders, custom shadows — all through Class and OverlayClass.
<MPopover Class="relative inline-block">
<MPopoverTrigger Class="inline-flex items-center gap-2 rounded-lg border-2 border-violet-500 px-4 py-2 text-sm font-semibold text-violet-600 hover:bg-violet-50 dark:hover:bg-violet-950">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true">
<path d="M12 2L2 7l10 5 10-5-10-5z"/>
<path d="M2 17l10 5 10-5"/>
<path d="M2 12l10 5 10-5"/>
</svg>
Custom Popover
</MPopoverTrigger>
<MPopoverContent Class="absolute top-full left-0 mt-2 z-50 w-72 rounded-xl border-2 border-violet-200 bg-gradient-to-br from-violet-50 to-purple-50 p-5 shadow-xl dark:from-violet-950 dark:to-purple-950"
OverlayClass="fixed inset-0 z-40">
<p class="text-sm font-semibold text-violet-700 dark:text-violet-300">Fully Custom Treatment</p>
<p class="mt-1 text-sm text-violet-600 dark:text-violet-400">All styling is supplied via Tailwind classes on <code class="rounded bg-violet-100 px-1 text-xs dark:bg-violet-900">Class</code> and <code class="rounded bg-violet-100 px-1 text-xs dark:bg-violet-900">OverlayClass</code>. The component provides only behavioral primitives.</p>
</MPopoverContent>
</MPopover>
API Reference
MPopover
| Parameter | Type | Default | Description |
|---|---|---|---|
| ChildContent | RenderFragment? | null |
Slot content — typically MPopoverTrigger and MPopoverContent. |
| Class | string? | null |
CSS classes applied to the root container. Use relative to establish a positioning context. |
| AdditionalAttributes | Dictionary<string, object>? | null |
Arbitrary HTML attributes passed through to the root element. |
MPopoverTrigger
| Parameter | Type | Default | Description |
|---|---|---|---|
| ChildContent | RenderFragment? | null |
Button content (text, icons, etc.). |
| Class | string? | null |
CSS classes applied to the trigger button. |
| AdditionalAttributes | Dictionary<string, object>? | null |
Arbitrary HTML attributes (e.g., aria-label) passed through to the button element. |
MPopoverContent
| Parameter | Type | Default | Description |
|---|---|---|---|
| ChildContent | RenderFragment? | null |
Panel content (text, forms, lists, etc.). |
| Placement | PopoverPlacement | Bottom |
Placement intent recorded as data-placement. Does not inject CSS — pair with matching Tailwind classes on Class. |
| Class | string? | null |
CSS classes for the panel element. Supply positioning, sizing, color, and shadow here. |
| OverlayClass | string? | null |
CSS classes for the backdrop overlay. Use fixed inset-0 z-40 to enable outside-click dismissal across the viewport. |
| AdditionalAttributes | Dictionary<string, object>? | null |
Arbitrary HTML attributes passed through to the panel element. |
PopoverPlacement Enum
| Value | data-placement | Tailwind classes to pair |
|---|---|---|
Top |
top |
bottom-full left-1/2 -translate-x-1/2 mb-2 |
Bottom |
bottom |
top-full left-1/2 -translate-x-1/2 mt-2 |
Left |
left |
right-full top-1/2 -translate-y-1/2 mr-2 |
Right |
right |
left-full top-1/2 -translate-y-1/2 ml-2 |
The popover closes when:
- Outside click: Clicking the overlay div (rendered via
OverlayClass) triggers close. IfOverlayClassis omitted, outside-click dismissal is disabled. - Escape key: Pressing Escape dismisses the popover from either the overlay or the content panel.
Accessibility
MPopoverTriggerrenders witharia-haspopup="dialog",aria-expanded, andaria-controlspointing to the panel ID — all wired automaticallyMPopoverContentrenders withrole="dialog"and a stable generatedid(notaria-modal, since popover is non-modal)- Escape closes the popover and returns focus to the trigger
- Tab movement is unrestricted — focus can leave the popover naturally (non-modal behavior)
- Always provide
aria-labelonMPopoverTriggerfor icon-only triggers - For blocking modal interactions, use
MDialoginstead
| Key | Action |
|---|---|
| Enter / Space | Toggle popover (native button behavior) |
| Escape | Close popover and return focus to trigger |
| Tab | Move focus naturally — popover does not trap focus |