Tooltip
Contextual hints on hover and focus with positioning, collision handling, and safe pointer paths to interactive content.
<pe-tooltip>
<button type="button" class="Trigger" data-tooltip-trigger>Save</button>
<span class="Popup" data-tooltip-content hidden>
<span class="Arrow" data-tooltip-arrow></span>
Saves this draft without publishing.
</span>
</pe-tooltip>.Trigger {
min-height: 2rem;
padding: 0 0.75rem;
border: 1px solid oklch(14.5% 0 0);
border-radius: 0;
background: transparent;
color: inherit;
cursor: pointer;
@media (prefers-color-scheme: dark) {
border-color: white;
}
@media (hover: hover) {
&:hover {
background: oklch(92% 0 0);
@media (prefers-color-scheme: dark) {
background: oklch(26.9% 0 0);
}
}
}
}
pe-tooltip {
display: contents;
}
.Popup {
max-width: min(18rem, calc(100vw - 1rem));
padding: 0.375rem 0.5rem;
border: 1px solid oklch(14.5% 0 0);
border-radius: 0;
background: oklch(14.5% 0 0);
color: white;
font-size: 0.875rem;
line-height: 1.25rem;
box-shadow: 0 0.5rem 1.25rem oklch(0% 0 0 / 18%);
z-index: 20;
@media (prefers-color-scheme: dark) {
border-color: white;
background: white;
color: oklch(14.5% 0 0);
box-shadow: 0 0.5rem 1.25rem oklch(0% 0 0 / 35%);
}
&:not([data-state="open"]) {
display: none;
}
}
.Arrow {
display: block;
width: 12px;
height: 6px;
overflow: clip;
pointer-events: none;
&::before {
position: absolute;
bottom: 0;
left: 50%;
box-sizing: border-box;
width: calc(6px * sqrt(2));
height: calc(6px * sqrt(2));
content: "";
transform: translate(-50%, 50%) rotate(45deg);
border: 1px solid oklch(14.5% 0 0);
background: oklch(14.5% 0 0);
@media (prefers-color-scheme: dark) {
border-color: white;
background: white;
}
}
&[data-side="top"] {
bottom: -6px;
rotate: 180deg;
}
&[data-side="bottom"] {
top: -6px;
rotate: 0deg;
}
&[data-side="left"] {
right: -9px;
rotate: 90deg;
}
&[data-side="right"] {
left: -9px;
rotate: -90deg;
}
}
Tooltip shows brief supplementary information on hover or focus. Keep tooltip content short and non-interactive; it must not replace a visible label. Keep hidden on authored content.
Anatomy
Section titled “Anatomy”Give the host an id to use external or shared triggers:
<button type="button" data-tooltip-trigger="publish-tip">Publish</button>
<pe-tooltip id="publish-tip" data-tooltip-side="bottom"> <span data-tooltip-content hidden> Makes the current version visible to visitors. </span></pe-tooltip>| Part | Selector | Role |
|---|---|---|
| Host | <pe-tooltip> |
Lifecycle host for one content node |
| Trigger | [data-tooltip-trigger] |
Shows tooltip on hover/focus; closes on click while open |
| Content | [data-tooltip-content] |
Tooltip surface; gets role="tooltip" |
| Arrow | [data-tooltip-arrow] |
Optional decorative direct child of content; receives positioning state and coordinates |
Delay group
Section titled “Delay group”Use the same data-tooltip-delay-group on sibling hosts to skip the opening delay while moving between them.
<div class="Toolbar">
<button class="Trigger" data-tooltip-trigger="bold-tip">Bold</button>
<button class="Trigger" data-tooltip-trigger="italic-tip">Italic</button>
</div>
<pe-tooltip
id="bold-tip"
data-tooltip-delay="400"
data-tooltip-delay-group="formatting"
data-tooltip-skip-delay="300"
>
<span class="Popup" data-tooltip-content hidden>Bold text</span>
</pe-tooltip>
<pe-tooltip
id="italic-tip"
data-tooltip-delay="400"
data-tooltip-delay-group="formatting"
data-tooltip-skip-delay="300"
>
<span class="Popup" data-tooltip-content hidden>Italic text</span>
</pe-tooltip>.Toolbar {
display: flex;
gap: 0.5rem;
}
.Trigger {
min-height: 2rem;
padding: 0 0.75rem;
border: 1px solid oklch(14.5% 0 0);
border-radius: 0;
background: transparent;
color: inherit;
cursor: pointer;
@media (prefers-color-scheme: dark) {
border-color: white;
}
@media (hover: hover) {
&:hover {
background: oklch(92% 0 0);
@media (prefers-color-scheme: dark) {
background: oklch(26.9% 0 0);
}
}
}
}
pe-tooltip {
display: contents;
}
.Popup {
max-width: min(18rem, calc(100vw - 1rem));
padding: 0.375rem 0.5rem;
border: 1px solid oklch(14.5% 0 0);
border-radius: 0;
background: oklch(14.5% 0 0);
color: white;
font-size: 0.875rem;
line-height: 1.25rem;
box-shadow: 0 0.5rem 1.25rem oklch(0% 0 0 / 18%);
z-index: 20;
@media (prefers-color-scheme: dark) {
border-color: white;
background: white;
color: oklch(14.5% 0 0);
box-shadow: 0 0.5rem 1.25rem oklch(0% 0 0 / 35%);
}
&:not([data-state="open"]) {
display: none;
}
}
.Arrow {
display: block;
width: 12px;
height: 6px;
overflow: clip;
pointer-events: none;
&::before {
position: absolute;
bottom: 0;
left: 50%;
box-sizing: border-box;
width: calc(6px * sqrt(2));
height: calc(6px * sqrt(2));
content: "";
transform: translate(-50%, 50%) rotate(45deg);
border: 1px solid oklch(14.5% 0 0);
background: oklch(14.5% 0 0);
@media (prefers-color-scheme: dark) {
border-color: white;
background: white;
}
}
&[data-side="top"] {
bottom: -6px;
rotate: 180deg;
}
&[data-side="bottom"] {
top: -6px;
rotate: 0deg;
}
&[data-side="left"] {
right: -9px;
rotate: 90deg;
}
&[data-side="right"] {
left: -9px;
rotate: -90deg;
}
}
API Reference
Section titled “API Reference”Host API
Section titled “Host API”| Name | Type | Description |
|---|---|---|
isOpen | boolean (readonly) | Reads whether the tooltip is currently open. |
open(trigger?) | void | Opens the tooltip for the given trigger or the active trigger. |
close() | void | Closes the tooltip. |
toggle(trigger?) | void | Shows or hides the tooltip. |
Host attributes
Section titled “Host attributes”| Name | Description |
|---|---|
data-tooltip-arrow | Marks the optional decorative arrow. Must be a direct child of data-tooltip-content. |
data-tooltip-side | Preferred side: top | right | bottom | left. Default: top. |
data-tooltip-align | Alignment along the side: start | center | end. Default: center. |
data-tooltip-offset | Distance from the trigger along the side axis in pixels. Default: 8. |
data-tooltip-align-offset | Offset along the alignment axis in pixels. Default: 0. |
data-tooltip-collision-padding | Viewport padding used for flip/clamp calculations. Default: 4. |
data-tooltip-arrow-padding | Minimum distance between an authored arrow and the content edges. Default: 5. |
data-tooltip-delay | Milliseconds to wait before opening on hover. Default: 0. Focus and programmatic open are immediate. |
data-tooltip-close-delay | Milliseconds to wait before closing after hover leave, content leave, or blur. Default: 0. |
data-tooltip-delay-group | Shared group id for sibling tooltips. Only one tooltip in a group stays open at a time. |
data-tooltip-skip-delay | Milliseconds after a group tooltip opened during which sibling hover opens skip data-tooltip-delay. In a delay group, leave also waits at least this long unless data-tooltip-close-delay is larger. Default: 0. |
data-state | On host/content, arrow, and triggers: "open" or "closed". With shared triggers, only the active trigger is open. |
data-side / data-align | Resolved side and align on content and arrow after collision handling — useful for styling. |
data-uncentered | Present on the arrow when edge padding prevents it from centering on the trigger. |
Events
Section titled “Events”| Name | Description |
|---|---|
pe-tooltip:open | Fired when the tooltip opens. detail includes content, trigger, and reason. |
pe-tooltip:close | Fired when the tooltip closes. detail includes content, trigger, and reason. |
Event reasons in detail.reason: hover, focus, trigger, escape, programmatic.
Styling
Section titled “Styling”Hide closed content in your base CSS:
.Popup:not([data-state="open"]) { display: none;}Positioned tooltips use position: fixed on the content element while open. Style .Popup directly — borders, typography, and shadows are entirely yours.
An optional [data-tooltip-arrow] must be a decorative direct child of the content. Set its size, shape, and side styles in CSS; include its size in data-tooltip-offset.
While open, the content surface also receives positioning CSS variables:
| Name | Description |
|---|---|
--anchor-width / --anchor-height |
Active trigger dimensions |
--available-width / --available-height |
Remaining viewport space on the resolved side |
--transform-origin |
Anchor-relative origin for enter/exit transitions |
Content also receives data-starting-style and data-ending-style for enter and exit animations.