Popup
화면을 덮는 대화상자 계열이다. 다섯 종류가 하나의 PopupBase 위에 올라가며 dim 과 포커스 트랩, Escape, 스크롤 잠금, 배경 inert 를 공유한다. 이 페이지는 공통 계약만 다루고, 각 종류는 자기 페이지에 있다.
import { PopupHost, PopupBase } from "@nui-kit/react";
// 서브패스로 좁힐 때
import { PopupHost } from "@nui-kit/react/popup";
import "@nui-kit/react/styles/popup.css"; // 온디맨드일 때다섯 종류
| 컴포넌트 | 무엇 | 여는 법 |
|---|---|---|
Alert | 알림. 확인 버튼 하나. dim·ESC 로 닫히지 않는다 | 명령형 useAlert() |
Confirm | 확인·취소. openAsync 로 결과를 Promise 로 받는다 | 명령형 useConfirm() |
LayerPopup | 가운데 대화상자. 크기 셋 | 선언형 · 명령형 useLayerPopup() |
BottomSheet | 아래에서 올라오는 시트 | 선언형 · 명령형 useBottomSheet() |
FullPopup | 화면 전체를 덮고 오른쪽에서 들어온다 | 선언형 · 명령형 useFullPopup() |
두 가지 사용 방식
| 방식 | 쓰는 법 | 적합한 경우 |
|---|---|---|
| 명령형 | useAlert() · useConfirm() · useLayerPopup() 등 | 코드 흐름 중간에 띄우고 결과를 받아야 할 때 |
| 선언형 | <LayerPopup open={state} /> | 열림 상태를 컴포넌트가 직접 소유할 때. Alert · Confirm 은 선언형이 없다 |
PopupHost — 명령형의 전제
명령형 훅은 PopupHost 가 렌더하는 자리에 팝업을 띄운다. 앱 루트에 한 번만 둔다. 선언형만 쓴다면 필요 없다.
// app/providers.tsx — PopupHost 는 클라이언트 컴포넌트다
"use client";
import { PopupHost } from "@nui-kit/react/popup";
export function Providers({ children }) {
return <PopupHost>{children}</PopupHost>;
}명령형 훅은 모두 같은 모양이다. open() 으로 열고, close(id?) 는 id 를 주지 않으면 가장 최근에 연 것을 닫고, closeAll() 은 그 종류를 전부 닫는다. 열려 있는 목록도 돌려준다(alerts · confirms · layerPopups · bottomSheets · fullPopups).
쌓임
팝업은 나중에 연 것이 위에 온다. 열린 BottomSheet 위에 Alert 을 띄우면 Alert 이 위다. 겹친 상태에서는 최상단 팝업만 ESC 와 포커스 트랩을 처리한다. 명령형은 PopupHost 가 isTopmost 를 넣어 주고, 선언형은 소비자가 넘긴다.
true 라 선언형에서 그냥 렌더해도 ESC 와 포커스 트랩이 동작한다. 선언형으로 팝업 둘을 겹쳐 띄울 때만 아래쪽에 isTopmost={false} 를 넘긴다. 그러지 않으면 ESC 한 번에 둘 다 닫힌다. 명령형은 PopupHost 가 알아서 넣으므로 신경 쓸 것이 없다.접근성 — 다섯 종류가 공유한다
- 패널은
role="dialog"+aria-modal="true". 제목이 있으면aria-labelledby, 없으면dialogLabel이aria-label로 붙는다. 종류마다 기본 라벨이 있다 - 열리면 패널 안 첫 포커스 요소로 이동하고, 닫히면 원래 위치로 복원한다
- 닫기 버튼은 마크업의 가장 마지막에 있다. 그래서 첫 포커스가 본문·푸터로 가고, 닫기는 Tab 을 끝까지 눌렀을 때 잡힌다. 보이는 자리는 그대로 오른쪽 위다
- Tab 이 패널 밖으로 나가지 않는다 (포커스 트랩)
- 열려 있는 동안 배경은
inert+aria-hidden이 되고 스크롤이 잠긴다. 토스트 · 툴팁 · 로딩 알림은 예외다 — 팝업 안에서 띄운 것도 눌리고 읽혀야 하므로 우리 portal 컨테이너는 격리에서 빠진다 - 닫기 버튼은 40px 로 보이지만 누르는 범위는 44px 이다. 모양은 그대로 두고 히트만 넓혔다
prefers-reduced-motion에서는 이동·확대 없이 페이드만 남는다
커스터마이징
색은 컴포넌트별로 열지 않는다. 한 곳만 바꾸려면 className 을, 화면 전체를 바꾸려면 브랜드 프리셋을 쓴다. 아래 훅은 다섯 종류가 함께 쓴다.
| 컴포넌트 | 변수 | 기본값 | 비고 |
|---|---|---|---|
| Popup 계열 | --nui-popup--border-width | var(--nui-border-width-1) | — · 3자리를 함께 움직인다 |
| --nui-popup--lg-width | min(100%, 40rem) | 크기 large | |
| --nui-popup--md-width | min(100%, 30rem) | 크기 medium (기본) | |
| --nui-popup--radius | var(--nui-radius-3) | — | |
| --nui-popup--sm-width | min(100%, 22.5rem) | 크기 small |
API
PopupBase
직접 쓰기보다 다섯 셸 컴포넌트를 쓴다. LayerPopup · BottomSheet · FullPopup 은 이 props 를 그대로 받고(variant 는 셸이 정한다), Alert · Confirm 은 내용에 필요한 것만 받는다.
| 이름 | 타입 | 기본값 | 설명 |
|---|---|---|---|
open * | boolean | — | — |
bodyClassName | string | — | — |
children | ReactNode | — | — |
className | string | — | — |
closeButtonLabel | string | — | — |
contentAlign | PopupContentAlign | — | — |
description | ReactNode | — | — |
dialogLabel | string | — | title 이 없을 때 dialog 에 붙일 접근 이름 |
footer | ReactNode | — | — |
footerClassName | string | — | — |
hasCloseButton | boolean | — | — |
icon | ReactNode | null | — | — |
id | string | — | — |
isTopmost | boolean | — | 스택 최상단인가 — 포커스 트랩과 ESC 를 이 팝업만 처리한다 |
onClickClose | () => void | — | — |
onExited | () => void | — | 닫힘 애니메이션까지 끝난 뒤 호출된다 |
onRequestClose | () => void | — | — |
panelClassName | string | — | — |
shouldCloseOnBackdrop | boolean | — | dim 클릭으로 닫히는가 |
shouldCloseOnEscape | boolean | — | — |
size | PopupSize | — | — |
title | ReactNode | — | — |
variant | PopupVariant | — | — |
PopupBaseProps 에서 자동 생성됨 (components/Popup/Popup.types.ts).