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

항목DefaultAction
min-width 286px / max-width 400pxDefault 와 같다
본문 말줄임2줄을 넘으면 말줄임 처리된다Default 와 같다
버튼없음 — typeaction 이 아니면 버튼 영역 자체가 렌더되지 않는다확인(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);
}