Toast
스스로 도착해서, 이미 화면에 있는 것 위에 뜨는 메시지.
import { ToastProvider, useToast } from 'neba';
// 앱 전체를 한 번 감쌉니다
<ToastProvider position="bottom-end">{children}</ToastProvider>;
// 그 아래 어디서든
const toast = useToast();
toast.add({ color: 'success', title: '배포 완료', description: 'production · 4분 02초' });ToastProvider
| Prop | 타입 | 기본값 | 설명 |
|---|---|---|---|
| variant공통 | 'solid' | 'outline' | 'text' | 'outline' | 표면의 무게. 토스트 하나가 따로 덮어쓸 수 있습니다 |
| size공통 | 'xs' | 'sm' | 'md' | 'lg' | 'xl' | 'md' | 타입 스케일과 여백 |
| color공통 | 'primary' | 'secondary' | 'success' | 'warning' | 'danger' | 'info' | 'primary' | 기본 색 계열 |
| density공통 | 'default' | 'compact' | 'default' | 여백만 바꿉니다 |
| position | `top-${Align}` | `bottom-${Align}` | 'bottom-end' | 스택이 놓이는 자리. 위·아래 두 값과 공용 Align의 조합입니다 — 화면 한복판을 세로로 가르는 스택은 만들 수 없습니다 |
| timeout | number | 5000 | 기본 유지 시간(ms). 0이면 닫을 때까지 남습니다 |
| limit | number | 3 | 동시에 보이는 개수. 넘친 것은 버려지지 않고 스택이 빠지면 나타납니다 |
| width | number | string | 380 | 토스트 하나의 최대 너비 |
| closeLabel | string | 'Close' | × 버튼의 접근성 이름 |
useToast().add(options)
| Prop | 타입 | 기본값 | 설명 |
|---|---|---|---|
| title | ReactNode | — | 제목 |
| description | ReactNode | — | 아래 설명. 이것만 있으면 한 줄짜리 토스트입니다 |
| color공통 | 'primary' | 'secondary' | 'success' | 'warning' | 'danger' | 'info' | — | 이 토스트만 다른 색 계열로 |
| variant공통 | 'solid' | 'outline' | 'text' | — | 이 토스트만 다른 표면으로 |
| icon | ReactNode | false | — | 앞머리 글리프. 기본값은 color에 딸린 그림 |
| timeout | number | — | 이 토스트의 유지 시간(ms). 0은 읽고 나서 조치가 필요한 메시지에 씁니다 |
| priority | 'low' | 'high' | 'low' | high는 스크린 리더의 말을 끊습니다. 오류는 그럴 만하고 저장 완료는 아닙니다 |
| actionLabel | ReactNode | — | 액션 버튼의 라벨. 넘기면 버튼이 생깁니다 |
| onAction | (event) => void | — | 액션 버튼을 눌렀을 때 |
| id | string | — | 같은 id로 다시 부르면 그 토스트를 제자리에서 갱신하고 타이머를 다시 시작합니다 |
| onClose | () => void | — | 어떤 방식으로든 닫혔을 때 |
| onRemove | () => void | — | 애니메이션이 끝나고 DOM에서 빠졌을 때 |
add 외에 close(id?), update(id, options), promise(promise, { loading, success, error }), toasts를 함께 돌려줍니다.
예시
스택이 놓이는 자리
position이 side와 align의 조합이 아니라 두 단어인 이유는, 이 둘이 서로 독립적이지 않기 때문입니다. 토스트 스택은 위나 아래에 붙지 옆에 붙지 않습니다. 뒷부분은 NebaAlign, 다른 모든 컴포넌트가 쓰는 그 단어입니다.
액션, 그리고 promise 따라가기
읽고 나서 조치가 필요한 토스트는 읽히기 전에 사라지면 안 되므로 timeout: 0을 주세요. promise는 같은 생각의 나머지 절반입니다. 세 개가 쌓이는 대신 하나가 마음을 바꿉니다.
컴포넌트가 아니라 훅입니다
토스트가 필요해지는 순간 호출자가 손에 쥐고 있는 것은 클릭 핸들러지 트리 안의 자리가 아닙니다. 계속 마운트해 둬야 하는 <Toast open={…}/>, 그리고 메시지마다 하나씩 생기는 상태 — 이 컴포넌트는 바로 그 모양을 피하려고 존재합니다.
토스트가 어떻게 보일지는 provider에서 한 번에 정합니다. 스택의 자리, 너비, 표면, 유지 시간. 호출부에는 마땅히 남아야 할 것 하나만 남습니다 — 무슨 일이 일어났는지.
const toast = useToast();
const id = toast.add({
title: '삭제됨',
timeout: 0,
actionLabel: '실행 취소',
onAction: () => restore(id)
});
toast.update(id, { color: 'success', title: '복구됨' });같은 id로 다시 부르면 그 토스트를 제자리에서 갱신하고 타이머를 다시 시작합니다. "업로드 중… / 업로드 완료"가 원하는 동작입니다.
Toast인가 Alert인가?
Alert는 그 일이 벌어진 페이지에 속하고 그 자리에 남습니다. 토스트는 방금 다른 곳에서 일어난 일에 대한 것이고, 떠납니다. 1분 뒤에도 여전히 참인 메시지라면 그것은 Alert입니다.
접근성
잘 동작할 때 보이지 않는 부분은 전부 Base UI가 가집니다. 난데없이 나타난 메시지를 스크린 리더에 닿게 하는 live region, 호버와 창 비활성화에서 멈추는 타이머, 개수 제한, 스와이프, 스택으로 포커스를 옮기는 F6까지.
priority: 'high'는 스크린 리더의 말을 끊고, 기본값은 말이 끊길 때까지 기다립니다. 오류는 끊을 만하고 저장 완료는 그렇지 않습니다.
×는 스택이 호버되거나 포커스를 받기 전까지 일부러 접근성 트리에서 빠져 있습니다. 토스트가 "메시지 + 버튼"이 아니라 하나의 메시지로 읽히게 하기 위해서입니다.