Toast
Toast
자동으로 사라지는 알림 메시지. 어디서든 호출할 수 있는 명령형 API입니다.
설치
npm install @woon-ui/toastimport { toast, Toaster } from '@woon-ui/toast'
import '@woon-ui/toast/css'개요
import { toast, Toaster } from '@woon-ui/toast'toast()는 어디서든 호출할 수 있는 모듈 레벨 함수입니다.
앱 루트에 Toaster를 한 번 렌더하면 됩니다.
설정
Toaster
import { Toaster } from '@woon-ui/toast'
<>
<App />
<Toaster position="bottom-right" />
</>| Prop | Type | Default |
|---|---|---|
| position | 'top-left'|'top-center'|'top-right'|'bottom-left'|'bottom-center'|'bottom-right' | 'bottom-right' |
| 토스트가 나타나는 위치입니다. | ||
| maxVisible | number | 3 |
| 동시에 표시할 최대 토스트 수. 초과 시 가장 오래된 토스트부터 제거됩니다. | ||
| zIndex | number | 9000 |
| 토스트 컨테이너의 z-index입니다. | ||
| render | ComponentType<ToastDefaultRenderProps> | 내장 UI |
| 기본 토스트 UI를 교체할 커스텀 컴포넌트입니다. | ||
단독 사용
<Toaster />는 toast runtime을 마운트하는 컴포넌트입니다.
import { Toaster } from '@woon-ui/toast'
function App() {
return (
<>
<YourApp />
<Toaster position="bottom-right" />
</>
)
}Toaster에 position, maxVisible, zIndex, render를 전달해 동작과 기본 UI를 설정할 수 있습니다.
기본 사용법
import { toast } from '@woon-ui/toast'
toast({ title: '저장되었습니다' })
toast({ title: '변경사항 저장됨', description: '모든 변경사항이 저장되었습니다.' })Preview
ToastContent
첫 번째 인자로 전달하는 콘텐츠입니다.
| Prop | Type | Default |
|---|---|---|
| title | ReactNode | — |
| 토스트 제목 | ||
| description | ReactNode | — |
| 부가 설명 | ||
| action | { label: ReactNode; onClick: () => void } | — |
| 액션 버튼. 클릭 시 onClick 실행 후 토스트가 자동으로 닫힙니다. | ||
옵션
두 번째 인자로 표시 동작을 제어합니다.
toast({ title: '오류 발생' }, { tone: 'danger', duration: 8000 })| Prop | Type | Default |
|---|---|---|
| tone | 'default'|'danger' | 'default' |
| 시각적 톤 | ||
| duration | number | 5000 |
| 자동 닫힘 시간(ms). 최솟값 2000ms. Infinity로 설정하면 수동으로 닫을 때까지 유지됩니다. | ||
Preview
액션 버튼
action으로 버튼을 추가할 수 있습니다. 클릭 시 onClick 실행 후 토스트가 자동으로 닫힙니다.
toast(
{
title: '항목이 삭제되었습니다',
action: {
label: '되돌리기',
onClick: () => restoreItem(),
},
},
{ duration: Infinity },
)Tip
액션이 있는 토스트는 duration: Infinity로 설정하는 것을 권장합니다.
사용자가 버튼을 누를 시간이 필요합니다.
Preview
토스트 업데이트
toast()는 핸들 객체를 반환합니다. update()로 내용을 바꾸고 close()로 수동으로 닫을 수 있습니다.
const handle = toast({ title: '업로드 중...' }, { duration: Infinity })
// 내용 업데이트
handle.update({ title: '업로드 완료!' })
// 수동 닫기
handle.close()ToastHandle
| Prop | Type | Default |
|---|---|---|
| id | string | — |
| 토스트 고유 ID | ||
| close | () => void | — |
| 토스트 수동 닫기 | ||
| update | (content: ToastContent) => void | — |
| 표시 중인 토스트의 내용을 업데이트합니다 | ||
Preview
스택 동작
maxVisible(기본 3)을 초과하면 가장 오래된 토스트가 사라집니다- 기본 상태에서 토스트는 스택으로 겹쳐 보입니다
- hover 시 전체 목록이 펼쳐집니다 (expanded)
- 큐 없이 overflow는 oldest-drop 방식입니다
Preview
위치
ToasterPosition은 6가지를 지원합니다.
| 위치 | 값 |
|---|---|
| 좌상단 | 'top-left' |
| 상단 중앙 | 'top-center' |
| 우상단 | 'top-right' |
| 좌하단 | 'bottom-left' |
| 하단 중앙 | 'bottom-center' |
| 우하단 | 'bottom-right' |
<Toaster position="top-center" />커스텀 렌더링
render prop (컴포넌트 등록)
<Toaster render={...} />로 기본 토스트 UI를 교체할 수 있습니다.
import { Toaster } from '@woon-ui/toast'
import { MyToast } from './ui/Toast'
<Toaster render={MyToast} />ToastDefaultRenderProps 타입을 구현하면 됩니다:
type ToastDefaultRenderProps = {
title: ReactNode
description?: ReactNode
action?: { label: ReactNode; onClick: () => void }
close: () => void
}함수 문법 (완전 커스텀)
toast()에 함수를 전달하면 렌더링을 완전히 제어할 수 있습니다.
toast((ctx) => (
<div>
<p>커스텀 UI</p>
<button onClick={ctx.close}>닫기</button>
</div>
))스타일
@woon-ui/toast/css로 기본 스타일이 적용됩니다. 특정 값만 바꾸려면 선택자로 override하세요.
[data-woon-toast] {
background: #1a1a1a;
color: #fff;
border-radius: 12px;
}완전히 교체하려면 @woon-ui/react/css import를 제거하고 아래 CSS를 복사해서 수정하세요.
/* Toaster 컨테이너 & 스택 애니메이션 */
[data-woon-toaster] {
position: fixed;
width: min(calc(100vw - 2rem), 24rem);
pointer-events: none;
}
[data-woon-toaster][data-position="top-left"] { top: 1rem; left: 1rem; }
[data-woon-toaster][data-position="top-right"] { top: 1rem; right: 1rem; }
[data-woon-toaster][data-position="top-center"] { top: 1rem; left: 50%; translate: -50% 0; }
[data-woon-toaster][data-position="bottom-left"] { bottom: 1rem; left: 1rem; }
[data-woon-toaster][data-position="bottom-right"] { bottom: 1rem; right: 1rem; }
[data-woon-toaster][data-position="bottom-center"] { bottom: 1rem; left: 50%; translate: -50% 0; }
[data-woon-toaster][data-position^="bottom"] { --lift: -1; --lift-amount: calc(var(--lift) * 14px); }
[data-woon-toaster][data-position^="top"] { --lift: 1; --lift-amount: calc(var(--lift) * 14px); }
[data-woon-toast-wrapper] {
position: absolute;
width: 100%;
opacity: 0;
--y: translateY(calc(var(--lift) * -100%));
transform: var(--y);
transition: transform 400ms, opacity 400ms, height 400ms;
}
[data-woon-toaster][data-position^="bottom"] [data-woon-toast-wrapper] { bottom: 0; transform-origin: bottom center; }
[data-woon-toaster][data-position^="top"] [data-woon-toast-wrapper] { top: 0; transform-origin: top center; }
[data-woon-toaster]:not([data-expanded]) [data-woon-toast-wrapper]:not([data-mounted]):not([data-front]) {
--y: translateY(calc(var(--lift-amount) * var(--toasts-before))) scale(calc(-1 * var(--toasts-before) * 0.05 + 1));
}
[data-woon-toaster] [data-woon-toast-wrapper][data-mounted] { --y: translateY(0); opacity: 1; }
[data-woon-toaster]:not([data-expanded]) [data-woon-toast-wrapper][data-mounted]:not([data-front]) {
--y: translateY(calc(var(--lift-amount) * var(--toasts-before))) scale(calc(-1 * var(--toasts-before) * 0.05 + 1));
}
[data-woon-toaster]:not([data-expanded]) [data-woon-toast-wrapper][data-mounted]:not([data-front]) [data-woon-toast] > * {
opacity: 0;
transition: opacity 400ms;
}
[data-woon-toaster][data-expanded] [data-woon-toast-wrapper][data-mounted] { --y: translateY(calc(var(--lift) * var(--offset))); opacity: 1; }
[data-woon-toaster][data-expanded] [data-woon-toast-wrapper][data-mounted] [data-woon-toast] > * { opacity: 1; transition: opacity 400ms; }
[data-woon-toaster] [data-woon-toast-wrapper][data-state="closed"][data-front] { --y: translateY(calc(var(--lift) * -100%)); opacity: 0; }
[data-woon-toaster][data-expanded] [data-woon-toast-wrapper][data-state="closed"]:not([data-front]) { --y: translateY(calc(var(--lift) * var(--offset) + var(--lift) * -100%)); opacity: 0; }
[data-woon-toaster]:not([data-expanded]) [data-woon-toast-wrapper][data-state="closed"]:not([data-front]) { opacity: 0; transition: opacity 250ms ease-in; }
/* Toast 아이템 */
[data-woon-toast],
[data-woon-toast] *,
[data-woon-toast] *::before,
[data-woon-toast] *::after {
box-sizing: border-box;
}
[data-woon-toast] {
padding: 0.75rem 1rem;
background: #fff;
color: #000;
border-radius: 8px;
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.2);
font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
font-size: 0.875rem;
line-height: 1.5;
display: flex;
align-items: center;
justify-content: space-between;
gap: 0.75rem;
}
[data-woon-toast][data-tone="danger"] { background: #c0392b; color: #fff; }
/* Toast 콘텐츠 */
[data-woon-toast-body] { display: flex; flex-direction: column; gap: 0.15rem; flex: 1; min-width: 0; }
[data-woon-toast-title] { font-weight: 500; line-height: 1.4; }
[data-woon-toast-description] { font-size: 0.8125rem; opacity: 0.65; line-height: 1.4; }
[data-woon-toast-actions] { display: flex; align-items: center; gap: 0.125rem; flex-shrink: 0; }
[data-woon-toast-action] {
-webkit-appearance: none; appearance: none; box-sizing: border-box;
font-size: 0.8125rem; font-weight: 600; font-family: inherit;
color: #3b82f6; background: none; border: none; cursor: pointer;
padding: 0.25rem 0.375rem; border-radius: 4px; line-height: 1; white-space: nowrap;
}
[data-woon-toast-action]:hover { background: rgba(59, 130, 246, 0.08); }
[data-woon-toast-close] {
-webkit-appearance: none; appearance: none; box-sizing: border-box;
display: flex; align-items: center; justify-content: center;
width: 1.5rem; height: 1.5rem; background: none; border: none; cursor: pointer;
border-radius: 4px; font-size: 0.6875rem; font-family: inherit; color: inherit; opacity: 0.4; line-height: 1;
}
[data-woon-toast-close]:hover { opacity: 0.8; background: rgba(0, 0, 0, 0.06); }
[data-tone='danger'] [data-woon-toast-action] { color: rgba(255, 255, 255, 0.9); }
[data-tone='danger'] [data-woon-toast-action]:hover { background: rgba(255, 255, 255, 0.12); }
[data-tone='danger'] [data-woon-toast-close]:hover { background: rgba(255, 255, 255, 0.12); }
@media (prefers-reduced-motion) {
[data-woon-toast-wrapper], [data-woon-toast-wrapper] > * { transition: none !important; }
}접근성
- 토스트 컨테이너에
aria-live="polite"자동 적용 - hover 또는 focus 시 auto-dismiss 타이머 일시정지
- 타이머 재개 시 duration 전체 리셋 (잔여 시간이 아닌 전체 시간)
duration최솟값 2000ms 강제 (너무 짧으면 접근성 위반)- ESC 키로 가장 앞의 토스트 닫기
data 속성
| 속성 | 대상 | 값 |
|---|---|---|
data-woon-toaster | Toaster 컨테이너 | — |
data-position | Toaster 컨테이너 | 위치값 |
data-expanded | Toaster 컨테이너 | hover 시 존재 |
data-woon-toast | 개별 토스트 | — |
data-tone | 개별 토스트 | "default" 또는 "danger" |
data-state | 토스트 래퍼 | "open" 또는 "closed" |
data-front | 토스트 래퍼 | 가장 최신 토스트에 존재 |