Toast
Feedback & Status
즉시 알려야 할 상태·결과를 짧게 전달하는 알림.
Usage
작업 결과나 주의사항을 화면 흐름을 막지 않고 잠깐 알릴 때 사용한다.
import
import
import { Toast } from '@mildang/design-system/Toast';예제를 복사해 쓸 때 필요한 준비
• @mildang/styled-system 은 이 저장소에서 Panda 가 생성하는 산출물이다. 저장소 안에서는 turbo run ship 이후 쓸 수 있고, 패키지 소비자는 자기 Panda 산출물이나 다른 레이아웃 수단으로 바꿔야 한다.
API Reference
공개 Props가 없습니다.
Default vs Action
| 항목 | Default | Action |
|---|---|---|
| 폭 | min-width 286px / max-width 400px | Default 와 같다 |
| 본문 말줄임 | 2줄을 넘으면 말줄임 처리된다 | Default 와 같다 |
| 버튼 | 없음 — type 이 action 이 아니면 버튼 영역 자체가 렌더되지 않는다 | 확인(onAction)· |
닫기(onClose) 버튼. 둘 다 값을 넘긴 것만 렌더되고, 안 넘기면 그 버튼은 생략된다 | ||
| 자동으로 닫힘 | 기본 3초(duration prop 으로 조정 가능) 후 자동으로 닫힌다 | Default 와 같은 |
| 기본 duration(3초)을 그대로 물려받는다 |
Action 이 "눌러야만 닫힌다" 는 아니다. type: "action" 이 실제로 바꾸는 건 버튼 렌더
여부뿐이고, 자동 닫힘 시간은 Default 와 동일한 기본값을 그대로 쓴다. 버튼을 누르기 전에는
절대 안 닫혀야 하면 toast(message, { type: "action", duration: Infinity, onAction, onClose })
처럼 duration 을 직접 지정해야 한다.
떴을 때 모습
떴을 때의 모습을 status 별로 고정해 보여줍니다 — 실제 토스트는 3초 뒤 사라져 문서에 담기지 않습니다.
코드
import { type NoticeStatus } from '@mildang/design-system/NoticeBase';
import { toast } from '@mildang/design-system/Toast';
import { Button } from '@mildang/design-system/Button';
type ToastPosition = 'top-right' | 'bottom-center' | 'bottom-right';
type BaseStoryArgs = {
status: NoticeStatus;
position: ToastPosition;
title?: string;
showIcon: boolean;
message: string;
};
const DEFAULT_ARGS: BaseStoryArgs = {
status: 'info',
position: 'top-right',
title: '타이틀',
showIcon: true,
message: '얼럿 내용을 입력해주세요',
};
const STORY_DEFAULT_ARGS = { ...({}), ...(DEFAULT_ARGS) } as BaseStoryArgs;
const ToastActionExampleRender = (args: BaseStoryArgs) => {
return (
<Button
variant="tertiary"
onClick={() =>
toast(args.message, {
type: 'action',
status: args.status,
position: args.position,
...(args.title && { title: args.title }),
showIcon: args.showIcon,
onAction: () => {
console.log('Action clicked');
},
})
}
>
Toast
</Button>
);
};
export default function ToastActionExample(props: Partial<BaseStoryArgs>) {
const mergedProps = { ...STORY_DEFAULT_ARGS, ...props } as BaseStoryArgs;
return ToastActionExampleRender(mergedProps);
}
상태별 의미 — status
상태에 따라 색·아이콘만 달라지고 레이아웃·액션 패턴은 동일하다.
| status | 의미 |
|---|---|
basic | 긍·부정 의미가 없는 일반 안내(상태 판단이 불필요하거나 아직 확정되지 않은 경우) |
success | 작업이 정상 완료됐음을 알린다(예: 저장 완료) |
info | 중립 정보·상태 안내(예: 업데이트, 공지) — 행동이 필수는 아니다 |
warning | 주의·검토가 필요하다(예: 만료 임박, 설정 미완) |
error | 진행을 막는 문제다(예: 검증 실패, 권한 없음) — 실패 원인과 해결 경로를 함께 전달한다 |
뜨는 위치 — placement
정렬은 3가지로 제한하며 top-right 를 기본으로 한다.
| position | 기본 | 적합한 상황 |
|---|---|---|
top-right | ✅ | 일반적인 비동기 알림·정보 제공(메시지 도착, 작업 완료, 시스템 알림 등) |
bottom-center | 모바일에서 특히 유용 — 사용자의 현재 작업을 방해하지 않으면서 정보 제공 | |
| (작업 진행 상황, 백그라운드 작업 알림 등) | ||
bottom-right | 가장 낮은 주목도가 필요한 자리 — 부가 정보·덜 중요한 알림(활동 로그, 추천 | |
| 내용 등) |
디자인 배열상 top-right 가 중요 요소를 가리거나 토스트가 눈에 띄지 않는 화면이면 다른 위치로
바꿀 수 있다. 다만 이 3가지는 Storybook 컨트롤이 강제하는 값이고(ToastPosition 유니온),
toast() 런타임 자체는 sonner 의 position 전체(상하좌우 6방향)를 그대로 받는다 — 코드가
3가지로 막지는 않는다.
예제
기본 사용
버튼을 눌러 실제 토스트를 띄웁니다. Toast 레이어는 화면당 한 번만 마운트하면 됩니다.
import { type NoticeStatus } from '@mildang/design-system/NoticeBase';
import { toast } from '@mildang/design-system/Toast';
import { Button } from '@mildang/design-system/Button';
type ToastPosition = 'top-right' | 'bottom-center' | 'bottom-right';
type BaseStoryArgs = {
status: NoticeStatus;
position: ToastPosition;
title?: string;
showIcon: boolean;
message: string;
};
const DEFAULT_ARGS: BaseStoryArgs = {
status: 'info',
position: 'top-right',
title: '타이틀',
showIcon: true,
message: '얼럿 내용을 입력해주세요',
};
const STORY_DEFAULT_ARGS = { ...({}), ...(DEFAULT_ARGS) } as BaseStoryArgs;
const ToastDefaultExampleRender = (args: BaseStoryArgs) => {
return (
<Button
variant="tertiary"
onClick={() =>
toast(args.message, {
type: 'default',
status: args.status,
position: args.position,
...(args.title && { title: args.title }),
showIcon: args.showIcon,
offset: '20vh',
})
}
>
Toast
</Button>
);
};
export default function ToastDefaultExample(props: Partial<BaseStoryArgs>) {
const mergedProps = { ...STORY_DEFAULT_ARGS, ...props } as BaseStoryArgs;
return ToastDefaultExampleRender(mergedProps);
}
All Statuses (Action)
import { VStack } from '@mildang/styled-system/jsx';
import { NoticeBase, type NoticeStatus } from '@mildang/design-system/NoticeBase';
const ALL_STATUSES: NoticeStatus[] = ['error', 'warning', 'success', 'info', 'basic'];
const ToastAllStatusesExample = () => (
<VStack gap="24" alignItems="flex-start">
<VStack gap="12" alignItems="flex-start">
{ALL_STATUSES.map((status) => (
<NoticeBase
key={`title-${status}`}
type="action"
status={status}
title="타이틀"
message="얼럿 내용을 입력해주세요"
showIcon
actionLabel="확인"
onAction={() => {}}
onClose={() => {}}
/>
))}
</VStack>
<VStack gap="12" alignItems="flex-start">
{ALL_STATUSES.map((status) => (
<NoticeBase
key={`no-title-${status}`}
type="action"
status={status}
message="얼럿 내용을 입력해주세요"
showIcon
actionLabel="확인"
onAction={() => {}}
onClose={() => {}}
/>
))}
</VStack>
</VStack>
);
export default ToastAllStatusesExample;
Container Offset
import { toast, getToastContainer } from '@mildang/design-system/Toast';
import type { NoticeStatus } from '@mildang/design-system/NoticeBase';
import { css } from '@mildang/styled-system/css';
import { Button } from '@mildang/design-system/Button';
type BaseStoryArgs = {
status: NoticeStatus;
position: 'top-right' | 'bottom-center' | 'bottom-right';
title?: string;
showIcon: boolean;
message: string;
};
const STORY_DEFAULT_ARGS = {} as BaseStoryArgs;
const ToastOffsetLimitExampleRender = (_args: BaseStoryArgs) => {
return (
<div className={css({})}>
<div className={css({ display: 'flex', p: '16' })}>
<Button
variant="tertiary"
onClick={() =>
toast('컨테이너 영역 기준 사방 offset으로 토스트가 표시됩니다.', {
type: 'default',
position: 'top-right',
duration: 100000,
})
}
>
토스트 표시
</Button>
</div>
<div
{...getToastContainer()}
className={css({
p: '16',
border: '1px dashed',
borderColor: 'neutral.border.high',
borderRadius: '8',
color: 'neutral.text.low',
height: '500px',
mt: '24',
})}
>
컨테이너 영역
</div>
</div>
);
};
export default function ToastOffsetLimitExample(props: Partial<BaseStoryArgs>) {
const mergedProps = { ...STORY_DEFAULT_ARGS, ...props } as BaseStoryArgs;
return ToastOffsetLimitExampleRender(mergedProps);
}
Per Card Toasters
import { css } from '@mildang/styled-system/css';
import { Toast, toast } from '@mildang/design-system/Toast';
import { Button } from '@mildang/design-system/Button';
const ToastCardContainersExample = () => {
const cards = [
{ id: 'card-1', label: '학습 현황', status: 'info' as const },
{ id: 'card-2', label: '출석 관리', status: 'success' as const },
{ id: 'card-3', label: '성적 분석', status: 'warning' as const },
{ id: 'card-4', label: '과제 제출', status: 'error' as const },
{ id: 'card-5', label: '알림 설정', status: 'basic' as const },
{ id: 'card-6', label: '커리큘럼', status: 'info' as const },
];
return (
<div
className={css({
display: 'grid',
gridTemplateColumns: 'repeat(3, 1fr)',
gap: '16px',
p: '16px',
})}
>
{cards.map((card) => (
<div
key={card.id}
className={css({
position: 'relative',
border: '1px solid',
borderColor: 'neutral.border.base',
borderRadius: '12px',
p: '24px',
height: '250px',
display: 'flex',
flexDirection: 'column',
justifyContent: 'space-between',
bg: 'white',
overflow: 'hidden',
})}
>
<Toast id={card.id} style={{ position: 'absolute', top: 0, left: 0, right: 0, bottom: 0 }} />
<p className={css({ fontWeight: 'bold', fontSize: '16px' })}>{card.label}</p>
<Button
variant="tertiary"
size="sm"
onClick={() => {
toast(`${card.label} 카드에서 발생한 토스트입니다.`, {
status: card.status,
position: 'top-right',
duration: 3000,
toasterId: card.id,
});
}}
>
토스트 표시
</Button>
</div>
))}
</div>
);
};
export default ToastCardContainersExample;
CustomStyle
import { type NoticeStatus } from '@mildang/design-system/NoticeBase';
import { toast } from '@mildang/design-system/Toast';
import { Button } from '@mildang/design-system/Button';
import { css } from '@mildang/styled-system/css';
type ToastPosition = 'top-right' | 'bottom-center' | 'bottom-right';
type BaseStoryArgs = {
status: NoticeStatus;
position: ToastPosition;
title?: string;
showIcon: boolean;
message: string;
};
const DEFAULT_ARGS: BaseStoryArgs = {
status: 'info',
position: 'top-right',
title: '타이틀',
showIcon: true,
message: '얼럿 내용을 입력해주세요',
};
const STORY_DEFAULT_ARGS = { ...({}), ...(DEFAULT_ARGS) } as BaseStoryArgs;
const ToastCustomStyleExampleRender = (args: BaseStoryArgs) => {
return (
<Button
variant="tertiary"
onClick={() =>
toast(args.message, {
type: 'default',
status: args.status,
position: args.position,
...(args.title && { title: args.title }),
showIcon: args.showIcon,
sx: css.raw({
bg: 'critical.surface.base',
borderColor: 'critical.border.base',
}),
})
}
>
Toast
</Button>
);
};
export default function ToastCustomStyleExample(props: Partial<BaseStoryArgs>) {
const mergedProps = { ...STORY_DEFAULT_ARGS, ...props } as BaseStoryArgs;
return ToastCustomStyleExampleRender(mergedProps);
}