Skip to content

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.

dart
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:

AxisRule
PrimaryAnchored to trigger edge with a 2 px gap. If preferred direction lacks room, flips (down → up).
CrossAnchored 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.
WidthFloors at the trigger width; caps at the viewport (minus a 16 px safety inset) — or at overlayMaxWidth if smaller.
HeightHard 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:

dart
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.

dart
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:

StateTokenResolver
HoverCdxConfig.hoverTintprimary @ 8% (default)
Selected fillCdxConfig.selectedTintprimary @ 16% (default)
Splash / pressCdxInteractionOverlay.splashprimary @ 8%
HighlightCdxInteractionOverlay.highlightprimary @ 4%
Focus ringMaterial defaulttheme.focusColor + InkWell

Tokens — CdxInteractionOverlay

Static helpers used by every CDX interactive surface:

dart
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 → primary

TapRegion 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.

  • DropdownsDropdown, MultiSelectDropdown, ActionMenu
  • PickersDateTimePicker, CdxDateRangePicker
  • TheminghoverTint, selectedTint