02Tiện ích entrance tw-animate-css
Dùng nguyên tổ hợp `animate-in fade-in slide-in-from-top-8 duration-500` của plugin `tw-animate-css`, card bắt đầu ở vị trí cao hơn 32px và trong suốt rồi hạ dần về chỗ cũ trong lúc rõ nét lên.
Dùng choModal hoặc toast xuất hiện lần đầu, danh sách card vào trang
@keyframes slide-intranslateY(-32px)500ms

<main class="stage">
<div class="card is-demo">
<p class="card__title">Đã triển khai</p>
<p class="card__sub">Phiên bản mới vừa lên</p>
</div>
</main>@use "../../../../site/static/demo-tokens" as *;
// ── 변수 3개 ──────────────────────────────────────────
// Tailwind 「animate-in fade-in slide-in-from-top-8 duration-500」 의 500ms 를
// 리터럴로 재현 (이 편은 Tailwind 유틸 값을 그대로 옮기는 게 목적 — 사실.md 참고).
$duration: 500ms;
$easing: ease-out;
$color: $subject-cream;
// ── 데모 무대 ──────────────────────────────────────────
@include stage($stage-yellow);
.card {
box-sizing: border-box;
width: min(260px, 84vw);
padding: $sp-6 $sp-6;
border-radius: $r-card;
background: $subject-ink;
box-shadow: $shadow-raised;
opacity: 0;
transform: translateY(-32px); // slide-in-from-top-8 = spacing 8 × 4px = 32px
}
.card__title { margin: 0 0 $sp-1; color: $color; font-weight: 700; font-size: clamp(15px, 4vw, 18px); line-height: 1.3; font-family: inherit; }
.card__sub { margin: 0; color: rgba($subject-cream, .7); font-weight: 400; font-size: clamp(13px, 3.4vw, 14px); line-height: 1.4; font-family: inherit; }
// 데모: 카드가 위에서 내려와 멈춰 있다가, 2초마다 다시 위로 리셋해 반복
.card.is-demo {
animation: slide-in-loop 2s $easing infinite;
}
@keyframes slide-in-loop {
0% { opacity: 0; transform: translateY(-32px); }
25% { opacity: 1; transform: translateY(0); }
85% { opacity: 1; transform: translateY(0); }
100% { opacity: 0; transform: translateY(-32px); }
}
// 실사용: 한 번만 재생 (JS 가 mount 시점에 건다)
.card.is-appear {
animation: slide-in $duration $easing both;
}
@keyframes slide-in {
from { opacity: 0; transform: translateY(-32px); }
to { opacity: 1; transform: translateY(0); }
}
// ── 접근성 ──────────────────────────────────────────
@media (prefers-reduced-motion: reduce) {
.card.is-demo, .card.is-appear { animation: none; opacity: 1; transform: none; }
}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);
}
.card {
box-sizing: border-box;
width: min(260px, 84vw);
padding: 24px 24px;
border-radius: 16px;
background: #17141a;
box-shadow: 0 14px 24px rgba(23, 20, 26, 0.32);
opacity: 0;
transform: translateY(-32px);
}
.card__title {
margin: 0 0 4px;
color: #fff7e6;
font-weight: 700;
font-size: clamp(15px, 4vw, 18px);
line-height: 1.3;
font-family: inherit;
}
.card__sub {
margin: 0;
color: rgba(255, 247, 230, 0.7);
font-weight: 400;
font-size: clamp(13px, 3.4vw, 14px);
line-height: 1.4;
font-family: inherit;
}
.card.is-demo {
animation: slide-in-loop 2s ease-out infinite;
}
@keyframes slide-in-loop {
0% {
opacity: 0;
transform: translateY(-32px);
}
25% {
opacity: 1;
transform: translateY(0);
}
85% {
opacity: 1;
transform: translateY(0);
}
100% {
opacity: 0;
transform: translateY(-32px);
}
}
.card.is-appear {
animation: slide-in 500ms ease-out both;
}
@keyframes slide-in {
from {
opacity: 0;
transform: translateY(-32px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
@media (prefers-reduced-motion: reduce) {
.card.is-demo, .card.is-appear {
animation: none;
opacity: 1;
transform: none;
}
}import type { CSSProperties } from "react";
import styles from "./TwAnimateInCard.module.scss";
export type TwAnimateInCardProps = {
title?: string;
subtitle?: string;
/** tw-animate-css 의 duration-500 에 대응, ms */
duration?: number;
easing?: string;
color?: string;
};
type Vars = CSSProperties & { "--duration"?: string; "--easing"?: string; "--color"?: string };
export default function TwAnimateInCard({
title = "Deployed",
subtitle = "The new version just went live",
duration = 500,
easing = "ease-out",
color = "#fff7e6",
}: TwAnimateInCardProps) {
const vars: Vars = { "--duration": `${duration}ms`, "--easing": easing, "--color": color };
return (
<div className={styles.card} style={vars}>
<p className={styles.card__title}>{title}</p>
<p className={styles.card__sub}>{subtitle}</p>
</div>
);
}02 Tiện ích entrance tw-animate-css — Diện tích đo được 27.637% · khung hình chuyển động 9/23Nguyên tác GODRICH · SCSS và React nằm trong tệp zip · 9 Tailwind v4 Animations — dán 1 dòng là chạy
Bài viết chứa demo này
9 Tailwind v4 Animations — dán 1 dòng là chạyTailwind v4 animations là các animate- được khai báo ngay trong khối @theme của CSS, không cần tailwind.config.js. Chín Xem tất cả demo trong bài này


