Skip to content

PlOverlay

A sheet over the whole page that stops it being used. The scrim on its own, with whatever the caller puts on top of it. Most often a spinner and a line saying what is being waited for.

React
tsx
import { PlOverlay } from 'plass-ui';

<PlOverlay open={saving} label="Saving your changes">
  <Spinner />
</PlOverlay>;
dart
import 'package:plass_ui/plass_ui.dart';

PlOverlay(
  open: saving,
  label: 'Saving your changes',
  child: const Spinner(),
);

An overlay lifts itself out of the tree, so it needs an Overlay above it, WidgetsApp with a navigator and MaterialApp both provide one. Where it is written does not matter, and it takes up no room there.

Props

PropTypeDefaultDescription
openbooleanThe overlay is shown. Use with onOpenChange for a controlled overlay
defaultOpenbooleanWhether it starts shown, for an uncontrolled one
onOpenChange(open: boolean) => voidCalled when the open state changes
tone'scrim' | 'glass' | 'solid' | 'clear''scrim'How much of the page is taken away. scrim is PlModal’s own backdrop, glass is a lighter dim over a real blur, solid is opaque, and clear draws nothing while still blocking the pointer
dismissiblebooleanfalseWhether clicking the overlay or pressing Escape closes it. Off by default, the other way round from PlModal: a modal asks a question and Escape is the universal "no", while an overlay is saying *wait*
modalboolean | 'trap-focus'trueWhether the page behind is taken away for the keyboard too. trap-focus leaves it scrollable and clickable while still holding focus inside
alignshared'start' | 'center' | 'end''center'Where the content sits down the viewport
sizeshared'xs' | 'sm' | 'md' | 'lg' | 'xl''md'Scale of the padding around the content
colorshared'primary' | 'secondary' | 'success' | 'warning' | 'danger' | 'info''primary'Semantic colour role. Reaches the focus ring and whatever the content reads
labelstring'Overlay'The accessible name. Never drawn. An overlay that holds nothing readable still has to say what it is, which is why this has a default
childrenReactNodeWhat sits on top of the scrim — a spinner, a line of text, a small card
classNames{ backdrop?: string }Classes on the parts a className does not reach. backdrop is the scrim drawn behind the surface
PropTypeDefaultDescription
open * boolThe overlay is shown. Use with onOpenChange for a controlled overlay
onOpenChangedValueChanged<bool>?Called with false when the overlay asks to be closed — only ever when dismissible is on
childWidget?What sits on top of the scrim — a spinner, a line of text, a small card
tonePlOverlayTonePlOverlayTone.scrimHow much of the page is taken away. scrim is PlModal’s own backdrop, glass is a lighter dim over a real blur, solid is opaque, and clear draws nothing while still blocking the pointer
dismissibleboolfalseWhether clicking the overlay or pressing Escape closes it. Off by default, the other way round from PlModal: a modal asks a question and Escape is the universal "no", while an overlay is saying *wait*
modalbooltrueWhether the page behind is taken away for the pointer as well as the keyboard. false leaves it clickable while still holding focus inside
alignsharedPlassAlignPlassAlign.centerWhere the content sits down the viewport
sizesharedPlassSizePlassSize.mdScale of the padding around the content
labelString'Overlay'The accessible name. Never drawn. An overlay that holds nothing readable still has to say what it is, which is why this has a default

Every native <div> attribute passes straight through, onto the popup. color and children are excluded from the pass-through because both are Plass props here.

A className lands on the popup with them. The scrim underneath is what classNames.backdrop reaches.

Controlled: open and onOpenChanged are how an overlay is driven, and there is no uncontrolled mode. onOpenChanged is only ever called when dismissible is on, because nothing else can ask.

There is no color either. The one thing a colour family reached in the React build was the slots the content reads, and content in Flutter arrives with its own.

There is no variant. The three materials answer "how much does this surface assert itself against the page", and an overlay has already taken the page; tone is the question it actually has to answer. There is no elevation either: the overlay is the plane everything else floats above, and a scrim with a drop shadow is a scrim with an edge.

What the shared axes (size color align) mean across the library is in prop conventions.

Examples

tone

The four steps are one axis, how legible is what is behind, and they are tuned with the blur radius as much as with the alpha, because past about 16px a backdrop smears into flat colour and the scrim reads opaque no matter how low its alpha goes.

scrim matches PlModal's backdrop exactly. The two have to, or a modal opened over an overlay would show a seam.

clear draws nothing at all and still covers the viewport, which is the whole reason to use it: an invisible sheet that catches a click.

React

dismissible

Off by default, which is the other way round from PlModal and the one prop here worth reading twice. A modal asks a question and Escape is the universal "no"; an overlay says wait rather than asking anything, and a save that can be dismissed by a stray click is a save the user will think finished.

Turn it on for the overlay whose job is to catch a click outside something.

React

align

React

Accessibility

  • Base UI's Dialog owns the hard parts: the portal, the scroll lock, the focus held inside, the page behind going inert, and focus returning to wherever it came from when the overlay closes.
  • label has a default rather than being left empty, because an overlay that holds nothing readable (a bare spinner, a clear sheet) still has to say what it is.
  • modal="trap-focus" keeps the page scrollable and clickable while still holding focus inside, which is what a clear overlay usually wants.
  • The overlay animates opacity and nothing else. One that scaled or slid would drag whatever is written on it across the screen, and unlike a control this one is usually carrying a sentence.
  • Use a PlModal instead when there is a question to answer. An overlay has no title, no description and no actions, so a screen reader has nothing to work with beyond label.
  • Focus goes in and stays in: the layer is its own focus scope, and traversal is bounded by the nearest scope, so Tab inside the overlay cannot land on the page under it. When the overlay closes, focus goes back to whatever had it.
  • label has a default rather than being left empty, because an overlay that holds nothing readable (a bare spinner, a clear sheet) still has to say what it is. It names the layer as a route, which is how a screen reader knows the screen changed.
  • modal: false leaves the page clickable and scrollable while focus is still held inside, which is what a clear overlay usually wants.
  • The overlay animates opacity and nothing else. One that scaled or slid would drag whatever is written on it across the screen, and unlike a control this one is usually carrying a sentence. With animations turned off at the OS it appears at once.
  • Use a PlModal instead when there is a question to answer. An overlay has no title, no description and no actions, so a screen reader has nothing to work with beyond label.

Differences from the React build

ReactFlutterWhy
open / defaultOpen / onOpenChangeopen / onOpenChangedFlutter's own controls are controlled, and its name for the callback.
modal={true | 'trap-focus'}modal: boolThe two values were "does the pointer get through". A boolean says that in Flutter's words.
childrenchildFlutter's name.
colorThe only thing it reached was the slots the content read, and content here arrives with its own colours.
a portal to document.bodyan Overlay ancestorFlutter's portal goes to the nearest Overlay, which WidgetsApp with a navigator and MaterialApp both provide.
the scroll lockThere is no document to lock. The barrier already takes the pointer, and a scrollable behind it is not reachable.
className, style, native attributesThere is no class list and no style attribute to pass through.

Released under the MIT License