Overlay
A sheet over the whole page that blocks interaction. Use it while the user has to wait with nothing to answer: a save, a load, a replacement in progress.
import { Overlay, ProgressCircular } from 'neba';
<Overlay open={saving} tone="blur" label="Saving">
<ProgressCircular size="lg" />
</Overlay>;Props
| Prop | Type | Default | Description |
|---|---|---|---|
| locale | string | — | BCP 47 tag naming the overlay in that language |
| open | boolean | — | The overlay is shown. Use with onOpenChange for a controlled overlay |
| defaultOpen | boolean | false | Whether the overlay starts shown |
| onOpenChange | (open: boolean) => void | — | Called when the overlay opens or closes |
| tone | 'scrim' | 'blur' | 'solid' | 'clear' | 'scrim' | How much of the page is taken away. Four steps on one axis, tuned with the blur radius as much as with the alpha |
| dismissible | boolean | false | Whether a click or Escape closes it. Off, the other way round from Dialog: an overlay is not asking anything, and a save dismissed by a stray click is a save the user will believe finished |
| modal | boolean | 'trap-focus' | true | Whether 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 |
| label | string | — | The accessible name. It has a default rather than being optional: an overlay that holds nothing readable still has to say what it is |
| children | ReactNode | — | What sits on top of the scrim: a spinner, a line of text, a small card |
Native <div> attributes pass through to the sheet. Only color and children are excluded, since the table above spells them differently.
An Overlay has no surface, no border, no title and no actions. If there is a decision to make, use Dialog instead.
Examples
tone
Four steps deciding how legible the page behind stays.
| tone | The page behind |
|---|---|
scrim | Stays readable; only interaction is blocked. Same value as Dialog's backdrop, so the two never show a seam. |
blur | Present as shape and colour, gone as words. For content being replaced. |
solid | Hidden entirely, covered opaquely in the page surface colour. |
clear | Nothing is drawn; only the pointer is blocked. |
import { useState } from 'react';
import { Button, Overlay, Typography } from 'neba';
import type { OverlayTone } from 'neba';
const TONES: OverlayTone[] = ['scrim', 'blur', 'solid', 'clear'];
export default function OverlayTones() {
const [tone, setTone] = useState<OverlayTone | null>(null);
return (
<>
<div className="flex flex-wrap gap-2">
{TONES.map((name) => (
<Button key={name} variant="outline" onClick={() => setTone(name)}>
{name}
</Button>
))}
</div>
<Overlay
open={tone !== null}
onOpenChange={(next) => !next && setTone(null)}
tone={tone ?? 'scrim'}
dismissible
label={`${tone} overlay`}
>
<div className="flex flex-col items-center gap-3">
<Typography level="h4">tone=“{tone}”</Typography>
<Button onClick={() => setTone(null)}>Close</Button>
</div>
</Overlay>
</>
);
}dismissible
Off by default, which is the other way round from Dialog. An Overlay is not asking for an answer, it is saying wait, so Escape and a click on the scrim are both refused. Turn it on for an overlay whose job is to catch a click outside something.
import { useState } from 'react';
import { Button, Card, Overlay } from 'neba';
export default function OverlayDismissible() {
const [held, setHeld] = useState(false);
const [loose, setLoose] = useState(false);
return (
<>
<div className="flex flex-wrap gap-2">
<Button variant="outline" onClick={() => setHeld(true)}>
Cannot be dismissed
</Button>
<Button variant="outline" onClick={() => setLoose(true)}>
Click anywhere to close
</Button>
</div>
<Overlay open={held} label="Working">
<Card
title="No way past this"
subtitle="Escape and a click outside are both refused."
footer={<Button onClick={() => setHeld(false)}>Let me out</Button>}
/>
</Overlay>
<Overlay
open={loose}
onOpenChange={setLoose}
dismissible
tone="scrim"
label="Dismissible overlay"
>
<p className="m-0 text-(--neba-fg)">Click the scrim, or press Escape.</p>
</Overlay>
</>
);
}modal
modal="trap-focus" leaves the page scrollable and clickable while holding focus inside the overlay: a good fit with the clear tone.
Accessibility
- Renders with
role="dialog", andlabelis its accessible name.labelhas a default because an overlay holding only a spinner, or aclearone, still has to say what it is. - The portal, the scroll lock, focus held inside, the page behind going inert, and focus returning on close are all handled.
- The entrance animates opacity only.
localedecides the overlay's accessible name;labelwrites it out instead.