Overlay
페이지 전체를 덮어 조작을 막는 한 겹입니다. 저장이나 로딩처럼 사용자가 답할 것 없이 기다려야 하는 동안 씁니다.
tsx
import { Overlay, ProgressCircular } from 'neba';
<Overlay open={saving} tone="blur" label="저장 중">
<ProgressCircular size="lg" />
</Overlay>;Props
| Prop | 타입 | 기본값 | 설명 |
|---|---|---|---|
| locale | string | — | BCP 47 태그. overlay의 접근성 이름을 이 언어로 씁니다 |
| open | boolean | — | 오버레이가 보입니다. onOpenChange와 함께 제어 컴포넌트로 씁니다 |
| defaultOpen | boolean | false | 처음부터 보일지 |
| onOpenChange | (open: boolean) => void | — | 열리거나 닫힐 때 |
| tone | 'scrim' | 'blur' | 'solid' | 'clear' | 'scrim' | 뒤 페이지를 얼마나 가져가는지. 하나의 축 위의 네 단계이고, 알파만큼이나 블러 반경으로 조율되어 있습니다 |
| dismissible | boolean | false | 클릭이나 Escape로 닫히는지. Dialog와 반대로 꺼져 있습니다. 오버레이는 묻지 않고 기다리라고 말하며, 빗나간 클릭으로 사라지는 저장은 끝났다고 믿게 되는 저장입니다 |
| modal | boolean | 'trap-focus' | true | 뒤 페이지를 키보드에서도 가져가는지. trap-focus는 페이지를 스크롤·클릭할 수 있게 두고 포커스만 붙잡습니다 |
| align공통 | 'start' | 'center' | 'end' | 'center' | 내용이 화면 세로에서 앉는 자리 |
| size공통 | 'xs' | 'sm' | 'md' | 'lg' | 'xl' | 'md' | 내용 둘레 여백의 스케일 |
| color공통 | 'primary' | 'secondary' | 'success' | 'warning' | 'danger' | 'info' | 'primary' | 의미론적 색 역할. 포커스 링과, 내용이 읽어 가는 슬롯에 닿습니다 |
| label | string | — | 오버레이의 접근성 이름. 읽을 것이 없는 오버레이도 자기가 무엇인지는 말해야 하므로 선택이 아니라 기본값입니다 |
| children | ReactNode | — | 스크림 위에 앉는 것, 스피너, 한 줄, 작은 카드 |
<div>의 native 속성은 sheet로 전달됩니다. color와 children만 위 표와 이름이 겹쳐 제외됩니다.
Overlay에는 표면도, 테두리도, 제목도, 액션도 없습니다. 사용자가 결정할 것이 있다면 Dialog를 쓰세요.
예시
tone
뒤 페이지가 얼마나 읽히는지를 정하는 네 단계입니다.
| tone | 뒤 페이지 |
|---|---|
scrim | 읽을 수는 있고 조작만 막힙니다. Dialog의 backdrop과 같은 값이라 두 컴포넌트가 겹쳐도 이음매가 보이지 않습니다. |
blur | 형태와 색은 남고 글자는 읽히지 않습니다. 내용이 교체되는 중일 때 씁니다. |
solid | 완전히 가립니다. 페이지 표면 색으로 불투명하게 덮습니다. |
clear | 아무것도 그리지 않고 포인터만 막습니다. |
tsx
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
기본값이 꺼짐이라는 점이 Dialog와 반대입니다. Overlay는 답을 요구하지 않고 기다리라고 말하는 것이므로, Esc와 scrim 클릭이 모두 거절됩니다. 무언가의 바깥 클릭을 받아 내는 것이 목적인 Overlay라면 켜세요.
tsx
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"는 페이지 스크롤과 클릭을 허용하면서 focus만 Overlay 안에 붙잡아 둡니다. clear tone과 함께 쓰기에 적합합니다.
접근성
role="dialog"로 렌더링되고label이 accessible name이 됩니다. 내용이 스피너뿐이거나clear여도 이름은 필요하므로label에는 기본값이 있습니다.- portal, scroll lock, focus 유지, 뒤 페이지 inert 처리, 닫을 때 focus 복귀가 모두 적용됩니다.
- 등장 효과는 opacity만 사용합니다.
- overlay의 접근성 이름은
locale이 정합니다.label로 직접 쓸 수도 있습니다.