Overlays
Every popover-style surface in CDX — dropdowns, action menus, multi-select panels, date pickers, range pickers, split-button menus — shares one positioning algorithm and one interaction-overlay contract. This page is the reference for both.
Positioning — buildOverlayFollower
buildOverlayFollower is the helper every CDX overlay uses to position itself relative to a trigger anchor. It composes a CompositedTransformFollower with viewport-aware flip + cross-axis alignment + content-driven width.
buildOverlayFollower(
layerLink: _layerLink,
anchorContext: context,
overlayMaxHeight: 300,
preferredAnchor: AxisDirection.down,
overlayMaxWidth: null, // null = intrinsic, capped at viewport
overlayConstraints: BoxConstraints(minWidth: 200),
child: buildOverlayMaterial(context, child: myMenuBody),
)Positioning rules:
| Axis | Rule |
|---|---|
| Primary | Anchored to trigger edge with a 2 px gap. If preferred direction lacks room, flips (down → up). |
| Cross | Anchored to the trigger's leading edge by default. If trigger is closer to the far edge, flips so the overlay grows toward the side with more room. Trigger overflow yields a small inward correction. |
| Width | Floors at the trigger width; caps at the viewport (minus a 16 px safety inset) — or at overlayMaxWidth if smaller. |
| Height | Hard cap at overlayMaxHeight — no flow beyond. |
Sizing is never reduced to fit — the overlay keeps its intrinsic width/height. If the viewport is too small, the far edge clips. This keeps long option labels readable instead of being squeezed.
Material shell — buildOverlayMaterial
Wraps overlay content in the standard CDX shell:
Material(
elevation: 2,
borderRadius: CdxConfig.of(context).controlBorderRadius,
clipBehavior: Clip.antiAlias,
color: scheme.surfaceContainerLow,
child: yourBody,
)The light elevation (2) keeps the overlay visually attached to the trigger rather than reading as a heavy modal sheet.
Interaction layer — CdxStateLayer
CdxStateLayer is the canonical wrapper for any interactive surface. It folds Material's hover / splash / highlight / focus / selection visuals into one composition, and pulls every color from CdxInteractionOverlay so a Material button and an adjacent InkWell answer the same visual contract.
CdxStateLayer(
onTap: () => selectItem(item),
selected: item == currentItem,
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
borderRadius: BorderRadius.circular(6), // optional — defaults to controlBorderRadius
tooltip: 'Select item',
child: Row(children: [Icon(item.icon), const SizedBox(width: 8), Text(item.label)]),
)Token chain:
| State | Token | Resolver |
|---|---|---|
| Hover | CdxConfig.hoverTint | primary @ 8% (default) |
| Selected fill | CdxConfig.selectedTint | primary @ 16% (default) |
| Splash / press | CdxInteractionOverlay.splash | primary @ 8% |
| Highlight | CdxInteractionOverlay.highlight | primary @ 4% |
| Focus ring | Material default | theme.focusColor + InkWell |
Tokens — CdxInteractionOverlay
Static helpers used by every CDX interactive surface:
CdxInteractionOverlay.hover(scheme, cdx) // hoverTint, optional CdxConfig override
CdxInteractionOverlay.splash(scheme) // primary @ 8%
CdxInteractionOverlay.highlight(scheme) // primary @ 4%
CdxInteractionOverlay.selectedFill(scheme, cdx) // selectedTint
CdxInteractionOverlay.mouseCursor(enabled: true) // SystemMouseCursors.click / .basic
// For Material ButtonStyle.overlayColor:
CdxInteractionOverlay.materialOverlay(scheme, cdx)
CdxInteractionOverlay.materialMouseCursor()
CdxInteractionOverlay.materialBorder(scheme) // outlineVariant → onSurfaceVariant → primaryTapRegion groups
Every CDX overlay scopes its dismiss-on-outside-tap with a UniqueKey TapRegion group ID. Tapping anywhere outside the trigger or overlay closes the overlay; tapping inside doesn't. Multiple overlays in the same screen don't interfere because each owns its own group ID.
RootOverlay targeting
CDX overlays that may be shown inside a sheet, dialog, or panel always target OverlayChildLocation.rootOverlay. Without this, the overlay gets clipped to the nearest ancestor Overlay's bounds — which often belongs to a smooth_sheets host or a scoped Navigator and is smaller than the window.