02Weather icons that never sit still — sun, cloud, rain, snow
The sun's rays rotate, clouds drift sideways, raindrops fall at their own speeds, and lightning rests then flashes — the icons each keep their own period inside one grid.
Best forCondition icons in forecast lists, a weather app's first screen
animation-delaystepsinfinite
<main class="stage stage--yellow">
<section class="wi is-demo" id="wi" aria-label="Animated weather icons">
<button class="wi__tile" type="button" aria-label="Replay the sunny icon">
<span class="wi__fx wi__sun"><svg viewBox="0 0 256 256" width="34" height="34" fill="currentColor" aria-hidden="true"><path d="M184,128a56,56,0,1,1-56-56A56,56,0,0,1,184,128Z" opacity="0.2"/><path d="M120,40V16a8,8,0,0,1,16,0V40a8,8,0,0,1-16,0Zm72,88a64,64,0,1,1-64-64A64.07,64.07,0,0,1,192,128Zm-16,0a48,48,0,1,0-48,48A48.05,48.05,0,0,0,176,128ZM58.34,69.66A8,8,0,0,0,69.66,58.34l-16-16A8,8,0,0,0,42.34,53.66Zm0,116.68-16,16a8,8,0,0,0,11.32,11.32l16-16a8,8,0,0,0-11.32-11.32ZM192,72a8,8,0,0,0,5.66-2.34l16-16a8,8,0,0,0-11.32-11.32l-16,16A8,8,0,0,0,192,72Zm5.66,114.34a8,8,0,0,0-11.32,11.32l16,16a8,8,0,0,0,11.32-11.32ZM48,128a8,8,0,0,0-8-8H16a8,8,0,0,0,0,16H40A8,8,0,0,0,48,128Zm80,80a8,8,0,0,0-8,8v24a8,8,0,0,0,16,0V216A8,8,0,0,0,128,208Zm112-88H216a8,8,0,0,0,0,16h24a8,8,0,0,0,0-16Z"/></svg></span>
<span class="wi__name">Sunny</span>
</button>
<button class="wi__tile" type="button" aria-label="Replay the cloudy icon">
<span class="wi__fx wi__drift"><svg viewBox="0 0 256 256" width="34" height="34" fill="currentColor" aria-hidden="true"><path d="M240,128a80,80,0,0,1-80,80H72A56,56,0,1,1,85.92,97.74l0,.1A80,80,0,0,1,240,128Z" opacity="0.2"/><path d="M160,40A88.09,88.09,0,0,0,81.29,88.67,64,64,0,1,0,72,216h88a88,88,0,0,0,0-176Zm0,160H72a48,48,0,0,1,0-96c1.1,0,2.2,0,3.29.11A88,88,0,0,0,72,128a8,8,0,0,0,16,0,72,72,0,1,1,72,72Z"/></svg></span>
<span class="wi__name">Cloudy</span>
</button>
<button class="wi__tile" type="button" aria-label="Replay the rain icon">
<span class="wi__scene">
<span class="wi__fx wi__drift wi__cloud-s"><svg viewBox="0 0 256 256" width="28" height="28" fill="currentColor" aria-hidden="true"><path d="M240,128a80,80,0,0,1-80,80H72A56,56,0,1,1,85.92,97.74l0,.1A80,80,0,0,1,240,128Z" opacity="0.2"/><path d="M160,40A88.09,88.09,0,0,0,81.29,88.67,64,64,0,1,0,72,216h88a88,88,0,0,0,0-176Zm0,160H72a48,48,0,0,1,0-96c1.1,0,2.2,0,3.29.11A88,88,0,0,0,72,128a8,8,0,0,0,16,0,72,72,0,1,1,72,72Z"/></svg></span>
<span class="wi__fx wi__drop wi__drop--1"><svg viewBox="0 0 256 256" width="9" height="9" fill="currentColor" aria-hidden="true"><path d="M208,144a80,80,0,0,1-160,0c0-72,80-128,80-128S208,72,208,144Z" opacity="0.2"/><path d="M174,47.75a254.19,254.19,0,0,0-41.45-38.3,8,8,0,0,0-9.18,0A254.19,254.19,0,0,0,82,47.75C54.51,79.32,40,112.6,40,144a88,88,0,0,0,176,0C216,112.6,201.49,79.32,174,47.75ZM128,216a72.08,72.08,0,0,1-72-72c0-57.23,55.47-105,72-118,16.53,13,72,60.75,72,118A72.08,72.08,0,0,1,128,216Zm55.89-62.66a57.6,57.6,0,0,1-46.56,46.55A8.75,8.75,0,0,1,136,200a8,8,0,0,1-1.32-15.89c16.57-2.79,30.63-16.85,33.44-33.45a8,8,0,0,1,15.78,2.68Z"/></svg></span>
<span class="wi__fx wi__drop wi__drop--2"><svg viewBox="0 0 256 256" width="9" height="9" fill="currentColor" aria-hidden="true"><path d="M208,144a80,80,0,0,1-160,0c0-72,80-128,80-128S208,72,208,144Z" opacity="0.2"/><path d="M174,47.75a254.19,254.19,0,0,0-41.45-38.3,8,8,0,0,0-9.18,0A254.19,254.19,0,0,0,82,47.75C54.51,79.32,40,112.6,40,144a88,88,0,0,0,176,0C216,112.6,201.49,79.32,174,47.75ZM128,216a72.08,72.08,0,0,1-72-72c0-57.23,55.47-105,72-118,16.53,13,72,60.75,72,118A72.08,72.08,0,0,1,128,216Zm55.89-62.66a57.6,57.6,0,0,1-46.56,46.55A8.75,8.75,0,0,1,136,200a8,8,0,0,1-1.32-15.89c16.57-2.79,30.63-16.85,33.44-33.45a8,8,0,0,1,15.78,2.68Z"/></svg></span>
<span class="wi__fx wi__drop wi__drop--3"><svg viewBox="0 0 256 256" width="9" height="9" fill="currentColor" aria-hidden="true"><path d="M208,144a80,80,0,0,1-160,0c0-72,80-128,80-128S208,72,208,144Z" opacity="0.2"/><path d="M174,47.75a254.19,254.19,0,0,0-41.45-38.3,8,8,0,0,0-9.18,0A254.19,254.19,0,0,0,82,47.75C54.51,79.32,40,112.6,40,144a88,88,0,0,0,176,0C216,112.6,201.49,79.32,174,47.75ZM128,216a72.08,72.08,0,0,1-72-72c0-57.23,55.47-105,72-118,16.53,13,72,60.75,72,118A72.08,72.08,0,0,1,128,216Zm55.89-62.66a57.6,57.6,0,0,1-46.56,46.55A8.75,8.75,0,0,1,136,200a8,8,0,0,1-1.32-15.89c16.57-2.79,30.63-16.85,33.44-33.45a8,8,0,0,1,15.78,2.68Z"/></svg></span>
</span>
<span class="wi__name">Rain</span>
</button>
<button class="wi__tile" type="button" aria-label="Replay the snow icon">
<span class="wi__scene">
<span class="wi__fx wi__drift wi__cloud-s"><svg viewBox="0 0 256 256" width="28" height="28" fill="currentColor" aria-hidden="true"><path d="M240,128a80,80,0,0,1-80,80H72A56,56,0,1,1,85.92,97.74l0,.1A80,80,0,0,1,240,128Z" opacity="0.2"/><path d="M160,40A88.09,88.09,0,0,0,81.29,88.67,64,64,0,1,0,72,216h88a88,88,0,0,0,0-176Zm0,160H72a48,48,0,0,1,0-96c1.1,0,2.2,0,3.29.11A88,88,0,0,0,72,128a8,8,0,0,0,16,0,72,72,0,1,1,72,72Z"/></svg></span>
<span class="wi__fx wi__flake wi__flake--1"><svg viewBox="0 0 256 256" width="10" height="10" fill="currentColor" aria-hidden="true"><path d="M220,128a92,92,0,1,1-92-92A92.1,92.1,0,0,1,220,128Z" opacity="0.2"/><path d="M223.77,150.09a8,8,0,0,1-5.86,9.68l-24.64,6,6.46,24.11a8,8,0,0,1-5.66,9.8A8.25,8.25,0,0,1,192,200a8,8,0,0,1-7.72-5.93l-7.72-28.8L136,141.86v46.83l21.66,21.65a8,8,0,0,1-11.32,11.32L128,203.31l-18.34,18.35a8,8,0,0,1-11.32-11.32L120,188.69V141.86L79.45,165.27l-7.72,28.8A8,8,0,0,1,64,200a8.25,8.25,0,0,1-2.08-.27,8,8,0,0,1-5.66-9.8l6.46-24.11-24.64-6a8,8,0,0,1,3.82-15.54l29.45,7.23L112,128,71.36,104.54l-29.45,7.23A7.85,7.85,0,0,1,40,112a8,8,0,0,1-1.91-15.77l24.64-6L56.27,66.07a8,8,0,0,1,15.46-4.14l7.72,28.8L120,114.14V67.31L98.34,45.66a8,8,0,0,1,11.32-11.32L128,52.69l18.34-18.35a8,8,0,0,1,11.32,11.32L136,67.31v46.83l40.55-23.41,7.72-28.8a8,8,0,0,1,15.46,4.14l-6.46,24.11,24.64,6A8,8,0,0,1,216,112a7.85,7.85,0,0,1-1.91-.23l-29.45-7.23L144,128l40.64,23.46,29.45-7.23A8,8,0,0,1,223.77,150.09Z"/></svg></span>
<span class="wi__fx wi__flake wi__flake--2"><svg viewBox="0 0 256 256" width="10" height="10" fill="currentColor" aria-hidden="true"><path d="M220,128a92,92,0,1,1-92-92A92.1,92.1,0,0,1,220,128Z" opacity="0.2"/><path d="M223.77,150.09a8,8,0,0,1-5.86,9.68l-24.64,6,6.46,24.11a8,8,0,0,1-5.66,9.8A8.25,8.25,0,0,1,192,200a8,8,0,0,1-7.72-5.93l-7.72-28.8L136,141.86v46.83l21.66,21.65a8,8,0,0,1-11.32,11.32L128,203.31l-18.34,18.35a8,8,0,0,1-11.32-11.32L120,188.69V141.86L79.45,165.27l-7.72,28.8A8,8,0,0,1,64,200a8.25,8.25,0,0,1-2.08-.27,8,8,0,0,1-5.66-9.8l6.46-24.11-24.64-6a8,8,0,0,1,3.82-15.54l29.45,7.23L112,128,71.36,104.54l-29.45,7.23A7.85,7.85,0,0,1,40,112a8,8,0,0,1-1.91-15.77l24.64-6L56.27,66.07a8,8,0,0,1,15.46-4.14l7.72,28.8L120,114.14V67.31L98.34,45.66a8,8,0,0,1,11.32-11.32L128,52.69l18.34-18.35a8,8,0,0,1,11.32,11.32L136,67.31v46.83l40.55-23.41,7.72-28.8a8,8,0,0,1,15.46,4.14l-6.46,24.11,24.64,6A8,8,0,0,1,216,112a7.85,7.85,0,0,1-1.91-.23l-29.45-7.23L144,128l40.64,23.46,29.45-7.23A8,8,0,0,1,223.77,150.09Z"/></svg></span>
</span>
<span class="wi__name">Snow</span>
</button>
<button class="wi__tile" type="button" aria-label="Replay the lightning icon">
<span class="wi__scene">
<span class="wi__fx wi__drift wi__cloud-s"><svg viewBox="0 0 256 256" width="28" height="28" fill="currentColor" aria-hidden="true"><path d="M240,128a80,80,0,0,1-80,80H72A56,56,0,1,1,85.92,97.74l0,.1A80,80,0,0,1,240,128Z" opacity="0.2"/><path d="M160,40A88.09,88.09,0,0,0,81.29,88.67,64,64,0,1,0,72,216h88a88,88,0,0,0,0-176Zm0,160H72a48,48,0,0,1,0-96c1.1,0,2.2,0,3.29.11A88,88,0,0,0,72,128a8,8,0,0,0,16,0,72,72,0,1,1,72,72Z"/></svg></span>
<span class="wi__fx wi__bolt"><svg viewBox="0 0 256 256" width="14" height="14" fill="currentColor" aria-hidden="true"><path d="M96,240l16-80L48,136,160,16,144,96l64,24Z" opacity="0.2"/><path d="M215.79,118.17a8,8,0,0,0-5-5.66L153.18,90.9l14.66-73.33a8,8,0,0,0-13.69-7l-112,120a8,8,0,0,0,3,13l57.63,21.61L88.16,238.43a8,8,0,0,0,13.69,7l112-120A8,8,0,0,0,215.79,118.17ZM109.37,214l10.47-52.38a8,8,0,0,0-5-9.06L62,132.71l84.62-90.66L136.16,94.43a8,8,0,0,0,5,9.06l52.8,19.8Z"/></svg></span>
</span>
<span class="wi__name">Lightning</span>
</button>
<button class="wi__tile" type="button" aria-label="Replay the wind icon">
<span class="wi__fx wi__windfx"><svg viewBox="0 0 256 256" width="34" height="34" fill="currentColor" aria-hidden="true"><path d="M120,96a24,24,0,1,1,24-24A24,24,0,0,1,120,96Zm88-16a24,24,0,1,0,24,24A24,24,0,0,0,208,80Zm-56,80a24,24,0,1,0,24,24A24,24,0,0,0,152,160Z" opacity="0.2"/><path d="M184,184a32,32,0,0,1-32,32c-13.7,0-26.95-8.93-31.5-21.22a8,8,0,0,1,15-5.56C137.74,195.27,145,200,152,200a16,16,0,0,0,0-32H40a8,8,0,0,1,0-16H152A32,32,0,0,1,184,184Zm-64-80a32,32,0,0,0,0-64c-13.7,0-26.95,8.93-31.5,21.22a8,8,0,0,0,15,5.56C105.74,60.73,113,56,120,56a16,16,0,0,1,0,32H24a8,8,0,0,0,0,16Zm88-32c-13.7,0-26.95,8.93-31.5,21.22a8,8,0,0,0,15,5.56C193.74,92.73,201,88,208,88a16,16,0,0,1,0,32H32a8,8,0,0,0,0,16H208a32,32,0,0,0,0-64Z"/></svg></span>
<span class="wi__name">Wind</span>
</button>
</section>
</main>@use "../../../../site/static/demo-tokens" as *;
// 날씨 아이콘 여섯 개가 한 격자 안에서 각자의 주기로 움직인다.
// 주기는 전부 설정 초(2s)의 약수 — 2초 캡처 창 안에 한 바퀴가 온전히 담긴다(271 실측).
// 딜레이가 걸린 루프에는 animation-fill-mode: backwards 를 붙인다(252 실측: 첫 프레임 번쩍임).
$duration: $dur-loop;
$easing: $ease-out;
$color: $subject-blue;
@include stage($stage-yellow);
.wi {
box-sizing: border-box;
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: $sp-2;
width: min(300px, 92vw);
}
.wi * { box-sizing: border-box; }
.wi__tile {
display: grid;
justify-items: center;
gap: $sp-1;
padding: $sp-2 $sp-1;
border: 0;
border-radius: $r-card;
background: $stage-paper;
color: $subject-ink;
box-shadow: $shadow-press;
cursor: pointer;
&:focus-visible { outline: 2px solid $color; outline-offset: 2px; }
}
.wi__name {
font-size: 10px;
font-weight: 700;
line-height: 1.3;
}
.wi__tile > .wi__fx,
.wi__scene {
height: 36px;
display: grid;
place-items: center;
}
.wi__scene { position: relative; width: 64px; }
.wi__cloud-s { color: #5b6478; }
.wi__drift { color: #5b6478; }
.wi__sun { color: #ba9f72; }
.wi__windfx { color: #4a89c9; }
// ── 여섯 개의 움직임 ──
// 해: 광선이 제자리에서 한 바퀴 — 원은 대칭이라 아이콘 전체를 돌려도 어색하지 않다
@keyframes wi-spin {
from { transform: rotate(0deg); }
to { transform: rotate(360deg); }
}
.wi__sun { animation-name: wi-spin; animation-duration: $duration; animation-timing-function: linear; animation-iteration-count: infinite; }
// 구름·장면의 구름: 좌우로 살랑(2초 한 바퀴)
@keyframes wi-drift {
0%, 100% { transform: translateX(-3px); }
50% { transform: translateX(3px); }
}
.wi__drift { animation-name: wi-drift; animation-duration: $duration; animation-timing-function: $easing; animation-iteration-count: infinite; }
// 빗방울 세 개: 1초 주기, 딜레이로 엇갈린다 — 끝에서 옅어져 사라지는 것까지 포함
@keyframes wi-dropfall {
0% { transform: translateY(0); opacity: 0; }
18% { opacity: 1; }
72% { opacity: 1; }
100% { transform: translateY(12px); opacity: 0; }
}
.wi__drop {
position: absolute;
top: 20px;
color: $color;
animation-name: wi-dropfall;
animation-duration: 1s;
animation-timing-function: $easing;
animation-iteration-count: infinite;
animation-fill-mode: backwards;
}
.wi__drop--1 { left: 14px; }
.wi__drop--2 { left: 27px; animation-delay: .33s; }
.wi__drop--3 { left: 40px; animation-delay: .66s; }
// 눈송이 두 개: 떨어지며 반 바퀴 돈다
@keyframes wi-flakefall {
0% { transform: translateY(0) rotate(0deg); opacity: 0; }
18% { opacity: 1; }
72% { opacity: 1; }
100% { transform: translateY(12px) rotate(160deg); opacity: 0; }
}
.wi__flake {
position: absolute;
top: 20px;
color: #4a89c9;
animation-name: wi-flakefall;
animation-duration: 1s;
animation-timing-function: $easing;
animation-iteration-count: infinite;
animation-fill-mode: backwards;
}
.wi__flake--1 { left: 18px; }
.wi__flake--2 { left: 36px; animation-delay: .5s; }
// 번개: 쉬었다 두 번 번쩍 — steps 컷으로 중간 밝기가 없게 한다. 쉴 때도 흐릿하게 남아 있어 번개 칸임이 보인다
@keyframes wi-flash {
0%, 20% { opacity: .35; }
22%, 28% { opacity: 1; }
30%, 44% { opacity: .35; }
46%, 52% { opacity: 1; }
54%, 100% { opacity: .35; }
}
.wi__bolt {
position: absolute;
top: 18px;
left: 50%;
margin-left: -7px;
color: #ba9f72;
animation-name: wi-flash;
animation-duration: $duration;
animation-timing-function: steps(1, end);
animation-iteration-count: infinite;
animation-fill-mode: backwards;
}
// 바람: 오른쪽으로 밀렸다 돌아오는 흐름
@keyframes wi-gust {
0%, 100% { transform: translateX(-4px); }
45% { transform: translateX(5px); }
}
.wi__windfx { animation-name: wi-gust; animation-duration: $duration; animation-timing-function: $easing; animation-iteration-count: infinite; }
@media (max-width: 340px) {
.wi { gap: $sp-1; }
.wi__tile { padding: $sp-1 0; border-radius: $r-control; }
.wi__scene { width: 56px; }
}
@media (prefers-reduced-motion: reduce) {
.wi__fx { animation: none !important; transform: none !important; }
// 정지 상태에서도 아이콘과 장면 전체가 보인다 — 빗방울·눈송이·번개만 바닥에 얹어 둔다
.wi__drop, .wi__flake { opacity: 1 !important; }
.wi__bolt { opacity: 1 !important; }
}html, body {
margin: 0;
overflow: hidden;
}
.stage {
box-sizing: border-box;
min-height: 100vh;
display: grid;
place-items: center;
margin: 0;
background: #ffd23f;
font-family: -apple-system, "Segoe UI", "Noto Sans KR", sans-serif;
padding: clamp(8px, 4vw, 24px);
}
.wi {
box-sizing: border-box;
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 8px;
width: min(300px, 92vw);
}
.wi * {
box-sizing: border-box;
}
.wi__tile {
display: grid;
justify-items: center;
gap: 4px;
padding: 8px 4px;
border: 0;
border-radius: 16px;
background: #ffffff;
color: #17141a;
box-shadow: 0 6px 12px rgba(23, 20, 26, 0.32);
cursor: pointer;
}
.wi__tile:focus-visible {
outline: 2px solid #2f6df6;
outline-offset: 2px;
}
.wi__name {
font-size: 10px;
font-weight: 700;
line-height: 1.3;
}
.wi__tile > .wi__fx,
.wi__scene {
height: 36px;
display: grid;
place-items: center;
}
.wi__scene {
position: relative;
width: 64px;
}
.wi__cloud-s {
color: #5b6478;
}
.wi__drift {
color: #5b6478;
}
.wi__sun {
color: #ba9f72;
}
.wi__windfx {
color: #4a89c9;
}
@keyframes wi-spin {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
.wi__sun {
animation-name: wi-spin;
animation-duration: 2s;
animation-timing-function: linear;
animation-iteration-count: infinite;
}
@keyframes wi-drift {
0%, 100% {
transform: translateX(-3px);
}
50% {
transform: translateX(3px);
}
}
.wi__drift {
animation-name: wi-drift;
animation-duration: 2s;
animation-timing-function: ease-out;
animation-iteration-count: infinite;
}
@keyframes wi-dropfall {
0% {
transform: translateY(0);
opacity: 0;
}
18% {
opacity: 1;
}
72% {
opacity: 1;
}
100% {
transform: translateY(12px);
opacity: 0;
}
}
.wi__drop {
position: absolute;
top: 20px;
color: #2f6df6;
animation-name: wi-dropfall;
animation-duration: 1s;
animation-timing-function: ease-out;
animation-iteration-count: infinite;
animation-fill-mode: backwards;
}
.wi__drop--1 {
left: 14px;
}
.wi__drop--2 {
left: 27px;
animation-delay: 0.33s;
}
.wi__drop--3 {
left: 40px;
animation-delay: 0.66s;
}
@keyframes wi-flakefall {
0% {
transform: translateY(0) rotate(0deg);
opacity: 0;
}
18% {
opacity: 1;
}
72% {
opacity: 1;
}
100% {
transform: translateY(12px) rotate(160deg);
opacity: 0;
}
}
.wi__flake {
position: absolute;
top: 20px;
color: #4a89c9;
animation-name: wi-flakefall;
animation-duration: 1s;
animation-timing-function: ease-out;
animation-iteration-count: infinite;
animation-fill-mode: backwards;
}
.wi__flake--1 {
left: 18px;
}
.wi__flake--2 {
left: 36px;
animation-delay: 0.5s;
}
@keyframes wi-flash {
0%, 20% {
opacity: 0.35;
}
22%, 28% {
opacity: 1;
}
30%, 44% {
opacity: 0.35;
}
46%, 52% {
opacity: 1;
}
54%, 100% {
opacity: 0.35;
}
}
.wi__bolt {
position: absolute;
top: 18px;
left: 50%;
margin-left: -7px;
color: #ba9f72;
animation-name: wi-flash;
animation-duration: 2s;
animation-timing-function: steps(1, end);
animation-iteration-count: infinite;
animation-fill-mode: backwards;
}
@keyframes wi-gust {
0%, 100% {
transform: translateX(-4px);
}
45% {
transform: translateX(5px);
}
}
.wi__windfx {
animation-name: wi-gust;
animation-duration: 2s;
animation-timing-function: ease-out;
animation-iteration-count: infinite;
}
@media (max-width: 340px) {
.wi {
gap: 4px;
}
.wi__tile {
padding: 4px 0;
border-radius: 12px;
}
.wi__scene {
width: 56px;
}
}
@media (prefers-reduced-motion: reduce) {
.wi__fx {
animation: none !important;
transform: none !important;
}
.wi__drop, .wi__flake {
opacity: 1 !important;
}
.wi__bolt {
opacity: 1 !important;
}
}// 아이콘 여섯 개가 각자의 주기로 움직인다 — 누르면 그 아이콘만 처음부터 다시 재생된다.
var wiRoot = document.getElementById('wi');
wiRoot.addEventListener('click', function (e) {
var tile = e.target.closest('.wi__tile');
if (!tile) return;
var fxs = tile.querySelectorAll('.wi__fx');
for (var i = 0; i < fxs.length; i++) {
fxs[i].style.animation = 'none';
}
void tile.offsetWidth;
for (var j = 0; j < fxs.length; j++) {
fxs[j].style.animation = '';
}
});import { useState } from "react";
import type { CSSProperties } from "react";
import styles from "./WeatherIconAnimations.module.scss";
// vanilla 판(run/281/demo/02-weather-icon-animations/index.html) 의 React 포트.
// 아이콘 여섯 개가 각자의 주기로 움직인다 — 누르면 그 타일만 key 를 바꿔 처음부터 다시 재생된다.
export type WiTile = {
id: string;
/** 타일 아래 붙는 낱말 — 언어팩이 통째로 바꾼다 */
name: string;
};
export type WeatherIconAnimationsProps = {
tiles?: WiTile[];
groupLabel?: string;
/** 자동 재생 표시(이 데모의 움직임은 루프가 아니라 상시 재생이라 화면 변화는 없다) */
demo?: boolean;
duration?: number;
easing?: string;
color?: string;
};
type Vars = CSSProperties & { "--duration"?: string; "--easing"?: string; "--color"?: string };
const DEFAULT_TILES: WiTile[] = [
{ id: "sun", name: "맑음" },
{ id: "cloud", name: "구름" },
{ id: "rain", name: "비" },
{ id: "snow", name: "눈" },
{ id: "storm", name: "번개" },
{ id: "wind", name: "바람" },
];
function SunIcon() {
return (
<svg viewBox="0 0 256 256" width="34" height="34" fill="currentColor" aria-hidden="true"><path d="M184,128a56,56,0,1,1-56-56A56,56,0,0,1,184,128Z" opacity="0.2"/><path d="M120,40V16a8,8,0,0,1,16,0V40a8,8,0,0,1-16,0Zm72,88a64,64,0,1,1-64-64A64.07,64.07,0,0,1,192,128Zm-16,0a48,48,0,1,0-48,48A48.05,48.05,0,0,0,176,128ZM58.34,69.66A8,8,0,0,0,69.66,58.34l-16-16A8,8,0,0,0,42.34,53.66Zm0,116.68-16,16a8,8,0,0,0,11.32,11.32l16-16a8,8,0,0,0-11.32-11.32ZM192,72a8,8,0,0,0,5.66-2.34l16-16a8,8,0,0,0-11.32-11.32l-16,16A8,8,0,0,0,192,72Zm5.66,114.34a8,8,0,0,0-11.32,11.32l16,16a8,8,0,0,0,11.32-11.32ZM48,128a8,8,0,0,0-8-8H16a8,8,0,0,0,0,16H40A8,8,0,0,0,48,128Zm80,80a8,8,0,0,0-8,8v24a8,8,0,0,0,16,0V216A8,8,0,0,0,128,208Zm112-88H216a8,8,0,0,0,0,16h24a8,8,0,0,0,0-16Z"/></svg>
);
}
function CloudIcon({ size = 34 }: { size?: number }) {
return (
<svg viewBox="0 0 256 256" width={size} height={size} fill="currentColor" aria-hidden="true"><path d="M240,128a80,80,0,0,1-80,80H72A56,56,0,1,1,85.92,97.74l0,.1A80,80,0,0,1,240,128Z" opacity="0.2"/><path d="M160,40A88.09,88.09,0,0,0,81.29,88.67,64,64,0,1,0,72,216h88a88,88,0,0,0,0-176Zm0,160H72a48,48,0,0,1,0-96c1.1,0,2.2,0,3.29.11A88,88,0,0,0,72,128a8,8,0,0,0,16,0,72,72,0,1,1,72,72Z"/></svg>
);
}
function DropIcon() {
return (
<svg viewBox="0 0 256 256" width="9" height="9" fill="currentColor" aria-hidden="true"><path d="M208,144a80,80,0,0,1-160,0c0-72,80-128,80-128S208,72,208,144Z" opacity="0.2"/><path d="M174,47.75a254.19,254.19,0,0,0-41.45-38.3,8,8,0,0,0-9.18,0A254.19,254.19,0,0,0,82,47.75C54.51,79.32,40,112.6,40,144a88,88,0,0,0,176,0C216,112.6,201.49,79.32,174,47.75ZM128,216a72.08,72.08,0,0,1-72-72c0-57.23,55.47-105,72-118,16.53,13,72,60.75,72,118A72.08,72.08,0,0,1,128,216Zm55.89-62.66a57.6,57.6,0,0,1-46.56,46.55A8.75,8.75,0,0,1,136,200a8,8,0,0,1-1.32-15.89c16.57-2.79,30.63-16.85,33.44-33.45a8,8,0,0,1,15.78,2.68Z"/></svg>
);
}
function FlakeIcon() {
return (
<svg viewBox="0 0 256 256" width="10" height="10" fill="currentColor" aria-hidden="true"><path d="M220,128a92,92,0,1,1-92-92A92.1,92.1,0,0,1,220,128Z" opacity="0.2"/><path d="M223.77,150.09a8,8,0,0,1-5.86,9.68l-24.64,6,6.46,24.11a8,8,0,0,1-5.66,9.8A8.25,8.25,0,0,1,192,200a8,8,0,0,1-7.72-5.93l-7.72-28.8L136,141.86v46.83l21.66,21.65a8,8,0,0,1-11.32,11.32L128,203.31l-18.34,18.35a8,8,0,0,1-11.32-11.32L120,188.69V141.86L79.45,165.27l-7.72,28.8A8,8,0,0,1,64,200a8.25,8.25,0,0,1-2.08-.27,8,8,0,0,1-5.66-9.8l6.46-24.11-24.64-6a8,8,0,0,1,3.82-15.54l29.45,7.23L112,128,71.36,104.54l-29.45,7.23A7.85,7.85,0,0,1,40,112a8,8,0,0,1-1.91-15.77l24.64-6L56.27,66.07a8,8,0,0,1,15.46-4.14l7.72,28.8L120,114.14V67.31L98.34,45.66a8,8,0,0,1,11.32-11.32L128,52.69l18.34-18.35a8,8,0,0,1,11.32,11.32L136,67.31v46.83l40.55-23.41,7.72-28.8a8,8,0,0,1,15.46,4.14l-6.46,24.11,24.64,6A8,8,0,0,1,216,112a7.85,7.85,0,0,1-1.91-.23l-29.45-7.23L144,128l40.64,23.46,29.45-7.23A8,8,0,0,1,223.77,150.09Z"/></svg>
);
}
function BoltIcon() {
return (
<svg viewBox="0 0 256 256" width="14" height="14" fill="currentColor" aria-hidden="true"><path d="M96,240l16-80L48,136,160,16,144,96l64,24Z" opacity="0.2"/><path d="M215.79,118.17a8,8,0,0,0-5-5.66L153.18,90.9l14.66-73.33a8,8,0,0,0-13.69-7l-112,120a8,8,0,0,0,3,13l57.63,21.61L88.16,238.43a8,8,0,0,0,13.69,7l112-120A8,8,0,0,0,215.79,118.17ZM109.37,214l10.47-52.38a8,8,0,0,0-5-9.06L62,132.71l84.62-90.66L136.16,94.43a8,8,0,0,0,5,9.06l52.8,19.8Z"/></svg>
);
}
function WindIcon() {
return (
<svg viewBox="0 0 256 256" width="34" height="34" fill="currentColor" aria-hidden="true"><path d="M120,96a24,24,0,1,1,24-24A24,24,0,0,1,120,96Zm88-16a24,24,0,1,0,24,24A24,24,0,0,0,208,80Zm-56,80a24,24,0,1,0,24,24A24,24,0,0,0,152,160Z" opacity="0.2"/><path d="M184,184a32,32,0,0,1-32,32c-13.7,0-26.95-8.93-31.5-21.22a8,8,0,0,1,15-5.56C137.74,195.27,145,200,152,200a16,16,0,0,0,0-32H40a8,8,0,0,1,0-16H152A32,32,0,0,1,184,184Zm-64-80a32,32,0,0,0,0-64c-13.7,0-26.95,8.93-31.5,21.22a8,8,0,0,0,15,5.56C105.74,60.73,113,56,120,56a16,16,0,0,1,0,32H24a8,8,0,0,0,0,16Zm88-32c-13.7,0-26.95,8.93-31.5,21.22a8,8,0,0,0,15,5.56C193.74,92.73,201,88,208,88a16,16,0,0,1,0,32H32a8,8,0,0,0,0,16H208a32,32,0,0,0,0-64Z"/></svg>
);
}
// 타일 한 개의 그림 — 아이디마다 장면이 다르다
function TileArt({ id }: { id: string }) {
if (id === "sun") return <span className={`${styles.fx} ${styles.sun}`}><SunIcon /></span>;
if (id === "cloud") return <span className={`${styles.fx} ${styles.drift}`}><CloudIcon /></span>;
if (id === "rain") {
return (
<span className={styles.scene}>
<span className={`${styles.fx} ${styles.drift} ${styles.cloudS}`}><CloudIcon size={28} /></span>
<span className={`${styles.fx} ${styles.drop} ${styles["drop--1"]}`}><DropIcon /></span>
<span className={`${styles.fx} ${styles.drop} ${styles["drop--2"]}`}><DropIcon /></span>
<span className={`${styles.fx} ${styles.drop} ${styles["drop--3"]}`}><DropIcon /></span>
</span>
);
}
if (id === "snow") {
return (
<span className={styles.scene}>
<span className={`${styles.fx} ${styles.drift} ${styles.cloudS}`}><CloudIcon size={28} /></span>
<span className={`${styles.fx} ${styles.flake} ${styles["flake--1"]}`}><FlakeIcon /></span>
<span className={`${styles.fx} ${styles.flake} ${styles["flake--2"]}`}><FlakeIcon /></span>
</span>
);
}
if (id === "storm") {
return (
<span className={styles.scene}>
<span className={`${styles.fx} ${styles.drift} ${styles.cloudS}`}><CloudIcon size={28} /></span>
<span className={`${styles.fx} ${styles.bolt}`}><BoltIcon /></span>
</span>
);
}
return <span className={`${styles.fx} ${styles.windfx}`}><WindIcon /></span>;
}
export default function WeatherIconAnimations({
tiles = DEFAULT_TILES,
groupLabel = "움직이는 날씨 아이콘",
demo = true,
duration = 2000,
easing = "ease-out",
color = "#2f6df6",
}: WeatherIconAnimationsProps) {
const [live, setLive] = useState(demo);
// 타일마다 재생 회수 — 누르면 그 타일의 key 가 바뀌어 애니메이션이 처음부터 다시 돈다
const [gens, setGens] = useState<number[]>(tiles.map(() => 0));
const vars: Vars = { "--duration": `${duration}ms`, "--easing": easing, "--color": color };
function replay(index: number): void {
setLive(false);
setGens((prev: number[]) => prev.map((g: number, k: number) => (k === index ? g + 1 : g)));
}
const cls = [styles.wi, live ? styles.isDemo : ""].filter(Boolean).join(" ");
return (
<main className={styles.stage}>
<section className={cls} style={vars} aria-label={groupLabel}>
{tiles.map((tile: WiTile, i: number) => (
<button
key={`${tile.id}-${gens[i]}`}
className={styles.tile}
type="button"
aria-label={`${tile.name} 아이콘 다시 재생`}
onClick={() => replay(i)}
>
<TileArt id={tile.id} />
<span className={styles.name}>{tile.name}</span>
</button>
))}
</section>
</main>
);
}02 Weather icons that never sit still — sun, cloud, rain, snow — Measured area 2.385% · moving frames 23/23Original by GODRICH · SCSS and React ship in the zip · Weather Widget Design — 9 Widgets, No Library
The post this demo lives in
Weather Widget Design — 9 Widgets, No LibraryWeather widget design is the sky card on a dashboard — temperature, hour strip, week bars. These nine are plain HTML, SC See all demos in this post


