본문 바로가기

Components · 오버레이

Toast (토스트)

화면 가장자리에 잠깐 나타났다 스스로 사라지며 작업 결과를 알린다. Callout/Alert(페이지에 고정 노출되는 상시 안내)와 역할이 다르다 — 토스트는 발행·저장 완료 같은 순간 피드백 전용이다.

정의

화면 가장자리에 잠깐 나타났다 스스로 사라지며 작업 결과를 알리는 순간 피드백 컴포넌트다.

Anatomy

발행을 완료했습니다. ① 토스트 컨테이너 ② 상태 아이콘(선택) ③ 메시지
부위토큰
① 컨테이너bg --toast-bg(네이비) · fg --toast-fg · 모서리 --toast-radius · 패딩 --toast-pad-y/--toast-pad-x · 그림자 --toast-shadow · z-index: var(--z-toast)
② 상태 아이콘(선택)라인 SVG stroke="currentColor"
③ 메시지--text-body-sm · 1줄 권장

변형

변형용도 · 표시 시간
정보(기본)저장·발행 완료 알림 — 2~3초 후 자동 소멸
경고실패·주의 알림 — 3~4초 유지 + 좌측 상태 바 --color-state-warning-solid

상태

상태규칙
Enter아래에서 올라오며 fade-in(--duration-base --easing-out)
Visible정보 2~3초 / 경고 3~4초 유지(KRDS 기준)
Hover(선택)커서를 올리면 자동 소멸 타이머 일시정지(권장)
Exitfade-out 후 DOM에서 제거

Do / Don't

Do
  • 저장·발행 완료 같은 순간 피드백에만 쓰고 스스로 사라지게 한다.
  • 메시지는 결과를 한 줄로 담는다("카드뉴스를 저장했습니다").
  • 되돌리기 같은 액션이 붙으면 버튼 하나까지만, 아니면 표시 시간을 넉넉히 준다.
Don't
  • 폼 필드별 검증 오류에 토스트를 쓰지 말 것 — 인라인 에러(Input Error 상태)를 쓴다.
  • 상시 확인해야 할 안내를 토스트로 띄우지 말 것 — 그건 Callout·Alert(페이지 고정)다.
  • 토스트를 여러 개 쌓아 화면을 가리지 말 것 — 한 번에 1개, 순차 표시한다.

콘텐츠 규칙

  • 메시지는 결과 서술 1문장(≤25자), 완료형으로("발행했습니다").
  • 오류 토스트는 무엇이 실패했는지 + 다음 행동을 짧게("발행 실패 — 잠시 후 다시 시도").
  • 이모지·느낌표를 남발하지 않는다 — 상태는 아이콘·색으로 전달한다.

키보드 인터랙션

동작
Tab닫기·액션 버튼이 있을 때만 그 버튼으로 포커스 이동
Enter / Space포커스된 닫기·액션 버튼 실행
Esc액션형 토스트 닫기

순수 정보 토스트는 포커스를 받지 않는다 — 자동 소멸한다.

접근성

  • 컨테이너에 정보는 role="status" + aria-live="polite", 경고·오류는 role="alert" + aria-live="assertive" — 스크린리더가 자동 낭독한다.
  • 소멸 시간이 짧으면 못 읽는 사용자가 생긴다 — 중요한 내용을 토스트에만 의존하지 않는다.
  • 색만으로 성패를 구분하지 않는다(아이콘·텍스트 병기). 닫기 버튼은 aria-label="닫기".
  • 애니메이션은 prefers-reduced-motion에서 제거한다(즉시 표시·제거). 기준: ../guidelines/accessibility.md.

코드

html
<button class="pi-button" type="button" data-toast="발행을 완료했습니다.">토스트 띄우기</button>
<div class="pi-toast-region" role="status" aria-live="polite" aria-atomic="true"></div>
css
.pi-toast-region {
  position: fixed; left: 50%; bottom: var(--space-8); transform: translateX(-50%);
  display: flex; flex-direction: column; gap: var(--space-3); align-items: center;
  z-index: var(--z-toast); pointer-events: none;
}
.pi-toast {
  display: inline-flex; align-items: center; gap: var(--space-3);
  padding: var(--toast-pad-y) var(--toast-pad-x);
  border-radius: var(--toast-radius);
  background: var(--toast-bg);
  color: var(--toast-fg);
  box-shadow: var(--toast-shadow);
  font: var(--text-body-sm);
  word-break: keep-all; pointer-events: auto;
  opacity: 0; transform: translateY(var(--space-4));
  transition: opacity var(--duration-base) var(--easing-out),
              transform var(--duration-base) var(--easing-out);
}
.pi-toast.is-visible { opacity: 1; transform: translateY(0); }
.pi-toast--warning { border-left: var(--border-thick) solid var(--color-state-warning-solid); }
@media (prefers-reduced-motion: reduce) { .pi-toast { transition: none; } }
js
const region = document.querySelector(".pi-toast-region");
document.querySelectorAll("[data-toast]").forEach((trigger) => {
  trigger.addEventListener("click", () => {
    const toast = document.createElement("div");
    toast.className = "pi-toast";
    toast.textContent = trigger.dataset.toast;
    region.appendChild(toast);
    requestAnimationFrame(() => toast.classList.add("is-visible"));
    setTimeout(() => {
      toast.classList.remove("is-visible");
      toast.addEventListener("transitionend", () => toast.remove(), { once: true });
    }, 2500);
  });
});

Changelog

날짜변경
2026-07-15문서 계약(10섹션) 적용 — Anatomy 도해·정보/경고 변형·표시 시간·자동 소멸 실코드 신설. Callout/Alert와의 역할 구분 명시, KRDS 벤치마킹 규격 승계