저장이 끝났다는 짧은 쪽지가 화면 구석에서 톡 올라왔다 스르르 사라지는 그 움직임, 토스트를 CSS만으로 어떻게 짜는지 코드로 보여드리겠습니다. 미끄러져 들어오고, 잠시 머물고, 스스로 흐려지는 세 박자를 애니메이션으로 하나씩 그려 봅니다.

82.1 미끄러져 들어오는 토스트

토스트는 정해진 구석에서 슬쩍 밀려 들어와야 사용자가 놀라지 않습니다. 오른쪽 바깥에서 제자리로 미끄러지는 등장을 @keyframes로 그려 봅니다.


@keyframes toast-in {
from {
opacity: 0;
transform: translateX(120%);
}
to {
opacity: 1;
transform: translateX(0);
}
}
.toast {
position: fixed;
right: 16px;
bottom: 16px;
padding: 12px 16px;
border-radius: 8px;
background: #111827;
color: #fff;
animation: toast-in .3s cubic-bezier(.2, .8, .2, 1);
}


바깥 120% 지점에서 투명하게 시작해 제자리로 미끄러지며 또렷해집니다. 위에서 내려오게 하고 싶다면 translateX(120%)translateY(-120%)로 바꾸고 자리를 top으로 옮기면 됩니다.

82.2 스스로 흐려지며 사라지기

토스트는 잠깐 머물다 스스로 물러나야 합니다. 나갈 때는 반대로 흐려지며 밀려나게 하고, 등장과 퇴장을 한 줄에 이어 붙여 알아서 사라지게 만듭니다.


@keyframes toast-out {
to {
opacity: 0;
transform: translateX(120%);
}
}
.toast.hide {
animation: toast-out .25s ease forwards;
}
.toast.auto {
animation:
toast-in .3s cubic-bezier(.2, .8, .2, 1),
toast-out .4s ease 3s forwards;
}


여기서 forwards가 핵심입니다. 이게 없으면 애니메이션이 끝나는 순간 opacity가 원래의 1로 되돌아가 사라졌던 토스트가 다시 튀어나옵니다. animation-fill-mode: forwards는 마지막 프레임의 모습을 그대로 붙잡아 두어, 흐려진 상태로 얌전히 남게 합니다.

82.3 차곡차곡 쌓기

소식이 몰리면 토스트가 여럿 뜨는데, 서로 겹치지 않게 세로로 줄을 세워야 합니다. 자리 고정을 감싸는 상자로 옮기고 안쪽은 gap으로 벌립니다.


.toast-stack {
position: fixed;
right: 16px;
bottom: 16px;
display: flex;
flex-direction: column-reverse;
gap: 10px;
}
.toast-stack .toast {
position: static;
right: auto;
bottom: auto;
}


column-reverse 덕분에 새로 온 토스트가 아래에 붙고 먼저 온 것들은 위로 밀려 올라갑니다. gap: 10px가 사이를 벌려 두어 쪽지들이 붙어 뭉개지지 않습니다.

82.4 성격을 색과 점으로 알리기

잘됐다는 소식과 잘못됐다는 소식은 다른 옷을 입어야 합니다. 다만 색만으로는 색을 구별하기 어려운 사용자를 놓치니, 왼쪽 띠와 앞의 점을 함께 물들입니다.


.toast {
display: flex;
align-items: center;
gap: 8px;
border-left: 4px solid #6b7280;
}
.toast::before {
content: "";
flex: none;
width: 8px;
height: 8px;
border-radius: 50%;
background: #9ca3af;
}
.toast--ok {
border-left-color: #22c55e;
}
.toast--ok::before {
background: #22c55e;
}
.toast--err {
border-left-color: #ef4444;
}
.toast--err::before {
background: #ef4444;
}


초록 띠와 초록 점은 성공, 붉은 띠와 붉은 점은 오류를 말합니다. 색과 위치라는 두 통로로 성격을 겹쳐 알리니, 색을 못 읽어도 왼쪽 모양의 차이로 뜻이 전해집니다.

82.5 알림 배지가 톡톡 뛰기

새 소식이 시스템에서 도착하면 배지가 스스로 시선을 끌어야 합니다. 숫자 배지 뒤로 붉은 물결이 퍼져 나가는 반응을 pulse 키프레임으로 만듭니다.


@keyframes badge-pulse {
0% {
transform: scale(1);
opacity: .7;
}
70%, 100% {
transform: scale(2.2);
opacity: 0;
}
}
.bell {
position: relative;
display: inline-block;
}
.bell .count {
position: absolute;
top: -6px;
right: -6px;
min-width: 18px;
height: 18px;
border-radius: 999px;
background: #ef4444;
color: #fff;
font-size: 11px;
line-height: 18px;
text-align: center;
}
.bell.ring .count::after {
content: "";
position: absolute;
inset: 0;
border-radius: 999px;
background: #ef4444;
animation: badge-pulse 1.4s ease-out infinite;
}


::after가 숫자와 똑같은 자리에 겹쳐 깔린 뒤, scale(1)에서 scale(2.2)로 부풀며 흐려집니다. 커지는 동안 opacity가 0으로 빠지니 물결이 밖으로 번지다 사라지는 것처럼 보입니다.

82.6 새 알림 점이 맺히기

급하지 않은 소식은 아이콘 위에 조용히 켜지는 점 하나로 알립니다. 없던 점이 방울처럼 맺히도록 살짝 튀었다가 자리를 잡는 등장을 넣습니다.


@keyframes dot-pop {
0% {
transform: scale(0);
}
60% {
transform: scale(1.3);
}
100% {
transform: scale(1);
}
}
.dot {
width: 8px;
height: 8px;
border-radius: 50%;
background: #ef4444;
transform: scale(0);
}
.dot.show {
animation: dot-pop .3s cubic-bezier(.34, 1.56, .64, 1) forwards;
}


처음엔 scale(0)으로 접혀 보이지 않다가, 60% 지점에서 scale(1.3)으로 한 번 살짝 넘겼다 제 크기로 돌아옵니다. 이 짧은 과함이 점이 톡 맺히는 손맛을 만들고, forwards가 그 크기를 붙잡아 둡니다.

82.7 움직임을 원하지 않는 사용자 존중하기

미끄러지고 부푸는 움직임이 어지럼을 부르는 사용자가 있습니다. 소식은 그대로 전하되 움직임만 걷어 내, 토스트가 즉시 나타나고 배지는 조용히 켜지게 합니다.


@media (prefers-reduced-motion: reduce) {
.toast,
.toast.hide,
.toast.auto,
.dot.show {
animation: none;
}
.bell.ring .count::after {
animation: none;
display: none;
}
}


움직임 줄이기를 켠 사용자에게는 슬라이드와 물결이 사라지고 상태만 바뀝니다. 토스트는 곧바로 떠서 제때 사라지고, 배지의 숫자는 남되 퍼지는 물결만 걷힙니다.

82.8 정리

토스트는 미끄러져 들어오는 toast-in, 흐려지며 나가는 toast-out, 그리고 끝 모습을 붙잡는 forwards 이 셋으로 완성됩니다. 여럿일 땐 gap으로 줄을 세우고, 배지는 pulse로 시선을 끌고, 점은 살짝 튀며 맺히게 하되, 움직임을 싫어하는 사용자를 위한 스위치를 늘 함께 답니다.