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,
width: const CdxPopoverWidth.content(min: 200), // trigger | content({min, max}) | fixed(w)
child: CdxMenuSurface(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 | CdxPopoverWidth.trigger() pins to the trigger width (at least the 180 px menu minimum); content({min, max}) takes the content width, no narrower than min (the trigger width when null) and no wider than max (the viewport, minus a 16 px safety inset, when null); fixed(w) is exactly w, within the viewport. width is the only width knob. |
| 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.
One floating surface — CdxMenuSurface
Every floating panel (dropdown and multi-select lists, action menus and their submenus, split-button menus, date and colour popovers) is drawn on CdxMenuSurface, which paints CdxMenuSurfaceStyle.decoration(context):
CdxMenuSurface(child: yourBody) // fill + 1 px outlineVariant border + CdxShadows.menu| Part | Token |
|---|---|
| Fill | CdxMenuSurfaceStyle.background (white on light, surfaceContainerLowest on dark, or CdxConfig.surface) |
| Border | 1 px CdxMenuSurfaceStyle.borderColor (outlineVariant a third of the way to outline), light and dark |
| Radius | CdxMenuSurfaceStyle.borderRadius |
| Shadow | CdxShadows.menu: shadow @ 0.14, blur 12, y 4 plus shadow @ 0.08, blur 2, y 1 |
The hairline carries the panel's edge where a shadow is lost (dark pages, a panel over surfaceContainerLow). A panel never draws its own chrome. Toasts and dialogs keep their own tokens (CdxShadows.popover, CdxShadows.modal).
Theme-provided MenuAnchor, DropdownMenu and PopupMenuButton panels are painted by Flutter with Material elevation, which draws one shadow, so they share the border, fill and radius and approximate the shadow. So does the SearchView suggestions popup.
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 |
Drop targets — outline only
A drop target is shown by its outline alone. While a compatible drag is over a container, lane, cell, row, slot or zone, only its border changes: the primary colour, the standard width and the box's own radius. Its background, tint, overlay and opacity stay exactly as at rest.
- A box with no border at rest gains its outline without moving anything: draw it in the foreground (
DecoratedBox(position: DecorationPosition.foreground),Container.foregroundDecoration) or inside the box, so neighbours do not shift. A box that has a border keeps its width and changes the colour. - A landing preview, a placeholder drawn where the item will land, is a dashed or solid outline with no fill.
- The hover tint is not drawn on a target that a drag is over either.
- An insertion line or caret that shows where the item lands, the dragged proxy, the gap it leaves, a label or badge that appears during the drag, and resize handles are not drop targets and keep their own look. A target that shows both a line and a fill keeps the line and drops the fill.
A test drags a compatible item over the target and checks that its fill equals the one at rest, that its outline differs, and that a neighbour is unchanged.
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.