Flyout
import "@taprootio/espalier/flyout";
<esp-flyout>
A transient panel for help and more-info content that claims designed spare width before ever covering the page.
Place one esp-flyout in an esp-page's flyout slot. The panel
lives on the canvas after any persistent preview, outside the content
surface. It negotiates between --esp-page-flyout-min-width and
--esp-page-flyout-max-width; preview receives flexible room first. The page
decides where help goes, from widest viewport to narrowest:
- Gutter — the panel occupies the right canvas gutter. When the gutter already has room, it claims existing spare canvas and the main content does not move at all, on any alignment.
- Shift — when the gutter is not quite wide enough, the surface
slides toward
startby exactly the shortfall (no more) to free the difference. - Docked sidebar — once the gutter is gone, the panel competes for width with the main well; content narrows but is never covered.
- Overlay drawer — when the complete sidebar + main + help
minimums do not fit, or below
COMPACT_VIEWPORT_MEDIA_QUERY(the same breakpoint theesp-menudrawer uses), the panel becomes a fixed drawer over a vellum backdrop. Setmode="overlay"to get this presentation at every width.
The panel is styled as a tear-off piece: a dotted perforation on
its leading edge, rounded trailing corners, and no shadow — all
overridable through the --esp-flyout-* tokens below.
An anchored panel shown with preview keeps its caret at the main/preview
edge and receives a non-interactive dotted bridge across preview from the
page. A short, thick terminus on the preview/help seam completes that
connection; no consumer positioning is required.
A11y follows the presentation. In the in-grid modes (1–3) the
flyout is a non-modal complementary landmark named by its
heading, opening it never steals focus, and boundary scrolling chains
back to the main document. In the overlay-drawer mode (4) it is a true
modal: role="dialog" + aria-modal, the
background goes inert, scroll is locked, focus moves into the drawer
and is trapped, and focus is restored on close. Escape closes it
in every mode.
By default the panel is a tear-off — a dotted perforation on its
leading edge, rounded trailing corners, no shadow. Add
match-surface to lift the trailing edge with the page's surface
edge shadow, so the flyout reads as a raised peer of the content it
flies out from while staying attached at the leading perforation:
When the page has no spare width to claim — a kind="full" page,
or a capped page on a viewport at the cap — the open flyout docks
as a right sidebar and the content makes room:
Below COMPACT_VIEWPORT_MEDIA_QUERY every flyout becomes a fixed
overlay drawer over a vellum — there is no room for a side-by-side
split on a phone. mode="overlay" forces that presentation at any
width (try it here; Escape or the vellum closes it):
Components that cannot know where the flyout lives can request it
over the bus with showFlyout() — the same pattern showToast()
uses. A second request swaps the content in place; it never
stacks. Content passed as a string renders as plain text; pass a
Node for rich content. Pass the triggering element as anchor to
align the in-grid panel with that control and keep both in the same
scroll flow. If the panel would cross the visible viewport's bottom,
it shifts up only far enough to fit and returns to its natural trigger
alignment as the page scrolls. Content taller than the viewport
scrolls inside the panel. Pass fullHeight: true (or set the
full-height attribute) when a reading surface should fill the visible
block-size even when its content is short. Overlay drawers stay
viewport-fixed.
Bus requests anchored inside nested pages are presented by the
outermost containing page's flyout by default. This keeps previews,
embedded tools, and nested layouts from trapping application-level
help or detail panels. Set scope="nearest" on a nested flyout when
that page should explicitly own requests originating inside it.
The demos above are each driven by their own toggle button, so they
are marked standalone to opt out of the bus entirely.
Slots
<esp-flyout> has a slot:
Default
The flyout's content.
Attributes
<esp-flyout> has the following attributes:
open
open has a default value of false.
Whether the flyout is open. Reflected so the initial state can
be declared in markup; esp-page mirrors it (via the lifecycle
events and the slot) onto its own flyout-open attribute to
drive the grid.
heading
heading has a default value of "".
Heading text for the panel's header row. Also becomes the
flyout's accessible name unless the author set an aria-label.
An open overlay without either uses the fallback name "Flyout" so
its dialog is never exposed without an accessible name.
mode
mode has a default value of "auto".
Placement behavior.
"auto"(default) — the page's placement ladder: gutter, docked sidebar, then overlay drawer when the complete in-grid minimums do not fit or belowCOMPACT_VIEWPORT_MEDIA_QUERY."overlay"— always the fixed overlay drawer, at any width. The page never reserves a grid track for an overlay-mode flyout.
full-height
full-height has a default value of false.
Fill the visible block-size in gutter and docked modes. The content region scrolls independently. Overlay drawers already fill the viewport.
match-surface
match-surface has a default value of false.
When set on a flyout slotted into an esp-page, the panel lifts
its trailing edge with the page's surface edge shadow, so it
reads as a raised peer of the content surface it flies out from
rather than the shadowless tear-off. The leading edge keeps
its dotted perforation and casts no shadow, so the panel stays
attached at the tear line instead of floating over the content.
The trailing-corner radius is still --esp-flyout-radius. No
effect on a standalone flyout — there is no page surface to match.
standalone
standalone has a default value of false.
Whether this flyout ignores the shared showFlyout() /
closeFlyout() bus and is driven only through its own
show() / close() / toggle() API.
By default a flyout services the bus, so a single flyout in a
page "just works" as the target of showFlyout() — the same
zero-config model as <esp-toaster> and showToast(). Anchored
requests resolve to one page scope according to scope; set
standalone on flyouts that should never answer the bus.
scope
scope has a default value of "outermost".
Which containing page owns anchored bus requests.
"outermost"(default) routes to the flyout in the outermostesp-pagecontaining the request trigger."nearest"explicitly gives this flyout precedence for requests originating in its own page or nested descendants.
A scope="nearest" flyout must be a light-DOM descendant of the
esp-page whose requests it owns so that page can discover it.
Requests without an anchor or returnFocusTo have no local context,
so only outermost page flyouts service them. Use the instance API for
a directly controlled nested flyout.
intent
intent has a default value of undefined.
The element's intent — its meaning: neutral
(no pin), or success, warning, danger, info — each pinned
to its fixed status family (blue for info), retunable per theme
via intents. On token-emitting controls a non-neutral intent pins the
filled-action pair to that family, derived over the governing
zone's theme; class-styled chrome (badges, callouts, status pills)
opts out of inline emission and renders its treatment from CSS
classes instead. An intent never repaints the surrounding zone,
which is what contexts are for. Removing the attribute restores
the element's own default intent.
context
context has a default value of undefined.
A theme-defined color zone. The selected context rebinds designer-facing roles and emits a complete, contrast-enforced semantic token table on this host for descendants to inherit.
Methods
<esp-flyout> has the following methods:
show
Open the flyout with its current content. In the in-grid modes it does not move focus; in the overlay-drawer mode it moves focus into the drawer as a modal.
close
Close the flyout. In the in-grid modes, if focus is inside the
flyout it is returned to returnFocusTo when one was provided; in
the overlay-drawer mode the modal treatment restores focus to
wherever it was when the drawer opened, unless returnFocusTo
names an explicit target — the explicit target wins.
toggle
Toggle the flyout between open and closed.
traverseToClosest
Traverse up the DOM tree to find the closest element that matches the selector. This method is aware of shadow DOM boundaries and will traverse through them to find the element.
Events
<esp-flyout> emits the following events:
esp-flyout-opened
esp-flyout-opened is of type CustomEvent<{}>.
Fired by show() when the flyout opens.
esp-flyout-closed
esp-flyout-closed is of type CustomEvent<{reason: FlyoutCloseReason}>.
Fired by close() when the flyout closes. reason is "escape", "vellum", "button", or "programmatic".
esp-flyout-state-changed
esp-flyout-state-changed is of type CustomEvent<{}>.
Fired on every open, mode, or anchor change, including direct property assignment (not just show()/close()). esp-page uses it to keep its layout in sync; most consumers want esp-flyout-opened/esp-flyout-closed instead.
CSS Parts
<esp-flyout> has the following CSS parts:
panel
The panel surface.
header
The heading/close-button row.
content
The scrollable content region wrapping the slot.
CSS Properties
<esp-flyout> has the following CSS properties:
--esp-flyout-background
The background color of the panel. Defaults to var(--esp-color-background).
--esp-flyout-border
The border on the panel's leading edge. Defaults to the tear-off perforation, 1px dotted var(--esp-color-border).
--esp-flyout-radius
The radius of the panel's trailing corners. Defaults to var(--esp-size-border-radius); the overlay drawer squares them.
--esp-flyout-shadow
The panel's shadow. Defaults to none in the gutter/docked modes (the perforation does the separating) and to a drawer shadow in overlay mode.
--esp-flyout-padding
Padding inside the panel's header and content regions. Defaults to var(--esp-size-padding).
--esp-flyout-z-index
Stack order of the overlay-drawer mode. Defaults to 3000 — above page chrome, below the dialog drop zone (4000). Set it on the esp-page (or an ancestor), not on the flyout itself: the page reads the same token to hoist the drawer's aside above its header.
--esp-field-background
Background color of the shared field shell. Defaults to var(--esp-color-layer-2).
--esp-field-border-color
Border color of the shared field shell. Defaults to var(--esp-color-border).
--esp-field-border-width
Border width of the shared field shell. Defaults to 1px.
--esp-field-text-color
Text color used inside the shared field shell. Defaults to var(--esp-color-text).
--esp-field-hover-bg
Hover background color of the shared field shell. Derived from --esp-field-background.
--esp-field-focus-bg
Focus background color of the shared field shell. Derived from --esp-field-background.
--esp-field-focus-shadow
Shadow color used for shared field focus treatment. Defaults to var(--esp-color-shadow).