CSS キーフレーム アニメーション 9選 — 一行でコピペOK
CSS キーフレーム アニメーションは、@keyframesで定義した一連の変化をクラス名ひとつにまとめ、任意の要素へそのまま適用する書き方です。浮く・跳ねる・回る・脈打つ・揺れる・ぐらつく・点滅する・鼓動する・ぷるぷるする、9種類をtransformとopacityの2プロパティだけで組みました。
01 basic-keyframe-motions-float-updown
02 basic-keyframe-motions-bounce
03 basic-keyframe-motions-spin
04 basic-keyframe-motions-pulse
05 basic-keyframe-motions-shake
06 basic-keyframe-motions-wobble
07 basic-keyframe-motions-flash
08 basic-keyframe-motions-heartbeat
09 basic-keyframe-motions-jelly
自動再生 · タイルを押すとその項目へ · すべて1つのzip
9つは名前の五十音順ではなく、要素が画面でどんな役割を担うかという順序で並べました。まず存在をそっと伝え(01・02)、次に進行中だと示し(03・04)、続けて注意を引き(05・06)、最後に状態が変わったことを知らせます(07〜09)。下の表で「css アニメーション 基本」の形を見渡してから、各項目の下でコードを確認してください。ここの9つが要素そのものを動かすのに対し、枠線だけが動くパターンはCSS 枠線 アニメーション 9選に、入力欄にフォーカスが入った瞬間の反応はCSS 入力フォーム フォーカスアニメーション9選に、それぞれ別にまとめています。
| クラス | 動くプロパティ | duration | 繰り返し |
|---|---|---|---|
.a-float |
translateY | 3秒 | 無限 |
.a-bounce |
translateY | 1秒 | 無限 |
.a-spin |
rotate | 1秒 | 無限 |
.a-pulse |
scale・opacity | 2秒 | 無限 |
.a-shake |
translateX | 0.5秒 | 1回(トリガー) |
.a-wobble |
translateX・rotate | 1秒 | 無限 |
.a-flash |
opacity | 1秒 | 無限 |
.a-heartbeat |
scale | 1.2秒 | 無限 |
.a-jelly |
scaleX・scaleY | 0.6秒 | 1回(トリガー) |
01ふわふわ浮く
translateYが往復する幅
.a-floatを付けると、translateYが0から-16pxのあいだをease-in-outでゆっくり往復するので、要素が宙に浮いているように見えます。イラストやアイコンバッジのように常に表示され続ける要素に使い、3秒という長めの周期で目に留まりすぎないようにしています。

<main class="stage">
<div class="badge a-float">
<span class="badge__dot" aria-hidden="true"></span>
新着
</div>
</main>@use "../../../../site/static/demo-tokens" as *;
// ── 변수 3개. 변형은 이것만 바꾼다 ──────────────────────
$duration: 3s; // 한 번 오가는 데 걸리는 시간
$easing: ease-in-out; // 뜨고 가라앉는 리듬이라 스프링보다 사인 곡선에 가까운 이 값을 쓴다
$color: $subject-blue; // 점 색
// ── 데모 무대 ───────────────────────────────
@include stage($stage-ink);
// ── 배지: .a-float 하나만 붙이면 어떤 요소든 떠오른다 ─────
.badge {
display: inline-flex;
align-items: center;
gap: clamp(8px, 3vw, 12px);
padding: clamp(14px, 4vw, 18px) clamp(24px, 6vw, 32px);
border-radius: $r-pill;
background: $subject-cream;
color: $stage-ink;
font-weight: 700; font-size: clamp(16px, 4.5vw, 20px); line-height: 1; font-family: inherit;
box-shadow: $shadow-raised;
}
.badge__dot {
width: 10px;
height: 10px;
border-radius: 50%;
background: $color;
flex: none;
}
.a-float {
animation: float $duration $easing infinite;
}
@keyframes float {
0%, 100% { transform: translateY(0); }
50% { transform: translateY(-16px); }
}
// ── 접근성: 움직임 줄이기 설정이면 뜨지 않는다 ──────────────
@media (prefers-reduced-motion: reduce) {
.a-float { animation: none; }
}html, body {
margin: 0;
overflow: hidden;
}
.stage {
box-sizing: border-box;
min-height: 100vh;
display: grid;
place-items: center;
margin: 0;
background: #17141a;
font-family: -apple-system, "Segoe UI", "Noto Sans KR", sans-serif;
padding: clamp(8px, 4vw, 24px);
}
.badge {
display: inline-flex;
align-items: center;
gap: clamp(8px, 3vw, 12px);
padding: clamp(14px, 4vw, 18px) clamp(24px, 6vw, 32px);
border-radius: 999px;
background: #fff7e6;
color: #17141a;
font-weight: 700;
font-size: clamp(16px, 4.5vw, 20px);
line-height: 1;
font-family: inherit;
box-shadow: 0 14px 24px rgba(23, 20, 26, 0.32);
}
.badge__dot {
width: 10px;
height: 10px;
border-radius: 50%;
background: #2f6df6;
flex: none;
}
.a-float {
animation: float 3s ease-in-out infinite;
}
@keyframes float {
0%, 100% {
transform: translateY(0);
}
50% {
transform: translateY(-16px);
}
}
@media (prefers-reduced-motion: reduce) {
.a-float {
animation: none;
}
}import type { CSSProperties, ReactNode } from "react";
import styles from "./FloatBadge.module.scss";
export type FloatBadgeProps = {
children: ReactNode;
/** 한 번 오가는 데 걸리는 시간 (기본 3000ms) */
duration?: number;
/** CSS easing (기본 ease-in-out) */
easing?: string;
/** 점 색 */
color?: string;
};
type Vars = CSSProperties & {
"--duration"?: string;
"--easing"?: string;
"--color"?: string;
};
export default function FloatBadge({
children,
duration = 3000,
easing = "ease-in-out",
color = "#2f6df6",
}: FloatBadgeProps) {
const vars: Vars = {
"--duration": `${duration}ms`,
"--easing": easing,
"--color": color,
};
return (
<div className={styles.badge} style={vars}>
<span className={styles.dot} aria-hidden="true" />
{children}
</div>
);
}01 · ふわふわ浮く — 計測 累積面積 4.311% · 動いたフレーム 23/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る
常に表示される要素に
$duration: 3s;
$easing: ease-in-out;
.a-float { animation: float $duration $easing infinite; }
@keyframes float {
0%, 100% { transform: translateY(0); }
50% { transform: translateY(-16px); }
}
02跳ねる
床で弾むようなイージング
.a-bounceはtranslateYを短く沈めてから大きく跳ね上げるcubic-bezierイージングで動き、床でボールが弾むような緩急を作ります。スクロール誘導の矢印や「もっと見る」アイコンのように、押せる場所だと動きで示したいときに向いています。

<main class="stage">
<div class="hint a-bounce">
<span class="hint__arrow" aria-hidden="true"></span>
もっと見る
</div>
</main>@use "../../../../site/static/demo-tokens" as *;
// ── 변수 3개 ──────────────────────────────────────────
$duration: 1s;
$easing: cubic-bezier(.28, .84, .42, 1); // 떨어질 때 빠르고 튀어 오를 때 감속 — 중력을 흉내낸다
$color: $stage-ink;
// ── 데모 무대 ───────────────────────────────
@include stage($stage-yellow);
.hint {
display: inline-flex;
flex-direction: column;
align-items: center;
gap: 6px;
color: $color;
font-weight: 700; font-size: clamp(14px, 4vw, 17px); line-height: 1; font-family: inherit;
}
.hint__arrow {
width: 14px;
height: 14px;
border-right: 3px solid $color;
border-bottom: 3px solid $color;
transform: rotate(45deg);
}
// ── .a-bounce 하나로 바닥에 튕기는 리듬을 만든다 ──────────
.a-bounce {
animation: bounce $duration $easing infinite;
}
@keyframes bounce {
0%, 100% { transform: translateY(0); }
30% { transform: translateY(-4px); }
55% { transform: translateY(10px); }
75% { transform: translateY(-2px); }
90% { transform: translateY(4px); }
}
@media (prefers-reduced-motion: reduce) {
.a-bounce { animation: 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);
}
.hint {
display: inline-flex;
flex-direction: column;
align-items: center;
gap: 6px;
color: #17141a;
font-weight: 700;
font-size: clamp(14px, 4vw, 17px);
line-height: 1;
font-family: inherit;
}
.hint__arrow {
width: 14px;
height: 14px;
border-right: 3px solid #17141a;
border-bottom: 3px solid #17141a;
transform: rotate(45deg);
}
.a-bounce {
animation: bounce 1s cubic-bezier(0.28, 0.84, 0.42, 1) infinite;
}
@keyframes bounce {
0%, 100% {
transform: translateY(0);
}
30% {
transform: translateY(-4px);
}
55% {
transform: translateY(10px);
}
75% {
transform: translateY(-2px);
}
90% {
transform: translateY(4px);
}
}
@media (prefers-reduced-motion: reduce) {
.a-bounce {
animation: none;
}
}import type { CSSProperties, ReactNode } from "react";
import styles from "./BounceHint.module.scss";
export type BounceHintProps = {
children: ReactNode;
/** 한 번 튀는 데 걸리는 시간 (기본 1000ms) */
duration?: number;
/** CSS easing (기본 중력을 흉내낸 커스텀 베지어) */
easing?: string;
/** 화살표·글자 색 */
color?: string;
};
type Vars = CSSProperties & {
"--duration"?: string;
"--easing"?: string;
"--color"?: string;
};
export default function BounceHint({
children,
duration = 1000,
easing = "cubic-bezier(.28,.84,.42,1)",
color = "#17141a",
}: BounceHintProps) {
const vars: Vars = {
"--duration": `${duration}ms`,
"--easing": easing,
"--color": color,
};
return (
<div className={styles.hint} style={vars}>
<span className={styles.arrow} aria-hidden="true" />
{children}
</div>
);
}02 · 跳ねる — 計測 累積面積 0.997% · 動いたフレーム 23/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る
押せる場所だと動きで示す
$duration: 1s;
$easing: cubic-bezier(.28, .84, .42, 1);
.a-bounce { animation: bounce $duration $easing infinite; }
@keyframes bounce {
0%, 100% { transform: translateY(0); }
30% { transform: translateY(-4px); }
55% { transform: translateY(10px); }
75% { transform: translateY(-2px); }
90% { transform: translateY(4px); }
}
03回転
「css アニメーション 無限ループ」の基本形
.a-spinはrotateを0度から360度までlinearで回すだけの汎用クラスで、ローディングスピナーに限らず更新ボタンや同期アイコンにもそのまま付けられます。イージングを挟まず一定速度で回すので、一周ごとの継ぎ目が目立ちません。

<main class="stage">
<button class="btn" type="button">
<span class="icon a-spin" aria-hidden="true"></span>
更新
</button>
</main>@use "../../../../site/static/demo-tokens" as *;
// ── 변수 3개 ──────────────────────────────────────────
$duration: 1s;
$easing: linear; // 가감속 없이 일정한 속도라야 회전이 끊겨 보이지 않는다
$color: $subject-cream;
// ── 데모 무대 ───────────────────────────────
@include stage($stage-orange);
.btn {
display: inline-flex;
align-items: center;
gap: 10px;
border: 0;
border-radius: $r-control;
padding: 14px 26px;
background: rgba(255, 255, 255, .14);
color: $subject-ink; // AA: 주황 면(흰 14% 겹침 #d2ada4) 위 6.28:1 — 스피너 링은 크림 유지
font-weight: 700; font-size: clamp(15px, 4vw, 18px); line-height: 1; font-family: inherit;
cursor: pointer;
-webkit-tap-highlight-color: transparent;
}
.icon {
width: 20px;
height: 20px;
border-radius: 50%;
border: 3px solid rgba(255, 255, 255, .3);
border-top-color: $color; // 이 한 변만 다른 색이라 회전이 보인다
flex: none;
}
// ── .a-spin 은 로딩뿐 아니라 새로고침·동기화 아이콘에도 그대로 쓴다 ─
.a-spin {
animation: spin $duration $easing infinite;
}
@keyframes spin {
to { transform: rotate(360deg); }
}
@media (prefers-reduced-motion: reduce) {
.a-spin { animation: none; }
}html, body {
margin: 0;
overflow: hidden;
}
.stage {
box-sizing: border-box;
min-height: 100vh;
display: grid;
place-items: center;
margin: 0;
background: #ff4d1f;
font-family: -apple-system, "Segoe UI", "Noto Sans KR", sans-serif;
padding: clamp(8px, 4vw, 24px);
}
.btn {
display: inline-flex;
align-items: center;
gap: 10px;
border: 0;
border-radius: 12px;
padding: 14px 26px;
background: rgba(255, 255, 255, 0.14);
color: #17141a;
font-weight: 700;
font-size: clamp(15px, 4vw, 18px);
line-height: 1;
font-family: inherit;
cursor: pointer;
-webkit-tap-highlight-color: transparent;
}
.icon {
width: 20px;
height: 20px;
border-radius: 50%;
border: 3px solid rgba(255, 255, 255, 0.3);
border-top-color: #fff7e6;
flex: none;
}
.a-spin {
animation: spin 1s linear infinite;
}
@keyframes spin {
to {
transform: rotate(360deg);
}
}
@media (prefers-reduced-motion: reduce) {
.a-spin {
animation: none;
}
}import type { CSSProperties, ReactNode } from "react";
import styles from "./SpinIcon.module.scss";
export type SpinIconProps = {
children: ReactNode;
/** 한 바퀴 도는 데 걸리는 시간 (기본 1000ms) */
duration?: number;
/** CSS easing (기본 linear — 끊김 없이 일정한 속도) */
easing?: string;
/** 링 강조 색 */
color?: string;
onClick?: () => void;
};
type Vars = CSSProperties & {
"--duration"?: string;
"--easing"?: string;
"--color"?: string;
};
export default function SpinIcon({
children,
duration = 1000,
easing = "linear",
color = "#fff7e6",
onClick,
}: SpinIconProps) {
const vars: Vars = {
"--duration": `${duration}ms`,
"--easing": easing,
"--color": color,
};
return (
<button type="button" className={styles.btn} style={vars} onClick={onClick}>
<span className={styles.icon} aria-hidden="true" />
{children}
</button>
);
}03 · 回転 — 計測 累積面積 0.233% · 動いたフレーム 23/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る
継ぎ目が目立たない理由
$duration: 1s;
$easing: linear; // 一定速度なので継ぎ目が目立たない
.a-spin { animation: spin $duration $easing infinite; }
@keyframes spin {
to { transform: rotate(360deg); }
}
04パルス
scaleとopacityを同時に揺らす
.a-pulseはscaleを1から1.06のあいだ、opacityも1から0.92のあいだで同時に揺らし、レイアウトを崩さずに視線だけを引きます。ライブ状態のドットや強調したいCTAボタンのように、目立たせたいが主張しすぎたくない場面に向いています。

<main class="stage">
<button class="btn a-pulse" type="button">今すぐ始める</button>
</main>@use "../../../../site/static/demo-tokens" as *;
// ── 변수 3개 ──────────────────────────────────────────
$duration: $dur-loop; // 2초 — 너무 빠르면 강조가 아니라 깜빡임으로 읽힌다
$easing: $ease-out;
$color: $subject-blue;
// ── 데모 무대 ───────────────────────────────
@include stage($stage-paper);
.btn {
border: 0;
border-radius: $r-control;
padding: 16px 40px;
background: $subject-blue-deep; // AA: 파란 면을 깊게 — 흰 글자 6.3:1
color: #fff;
font-weight: 700; font-size: clamp(15px, 4vw, 18px); line-height: 1; font-family: inherit;
cursor: pointer;
-webkit-tap-highlight-color: transparent;
}
// ── .a-pulse 는 레이아웃을 다시 그리지 않고 scale·opacity 만 쓴다 ─
.a-pulse {
animation: pulse $duration $easing infinite;
}
@keyframes pulse {
0%, 100% { transform: scale(1); opacity: 1; }
50% { transform: scale(1.06); opacity: .92; }
}
@media (prefers-reduced-motion: reduce) {
.a-pulse { animation: none; }
}html, body {
margin: 0;
overflow: hidden;
}
.stage {
box-sizing: border-box;
min-height: 100vh;
display: grid;
place-items: center;
margin: 0;
background: #ffffff;
font-family: -apple-system, "Segoe UI", "Noto Sans KR", sans-serif;
padding: clamp(8px, 4vw, 24px);
}
.btn {
border: 0;
border-radius: 12px;
padding: 16px 40px;
background: #0a4fe8;
color: #fff;
font-weight: 700;
font-size: clamp(15px, 4vw, 18px);
line-height: 1;
font-family: inherit;
cursor: pointer;
-webkit-tap-highlight-color: transparent;
}
.a-pulse {
animation: pulse 2s ease-out infinite;
}
@keyframes pulse {
0%, 100% {
transform: scale(1);
opacity: 1;
}
50% {
transform: scale(1.06);
opacity: 0.92;
}
}
@media (prefers-reduced-motion: reduce) {
.a-pulse {
animation: none;
}
}import type { CSSProperties, MouseEvent, ReactNode } from "react";
import styles from "./PulseButton.module.scss";
export type PulseButtonProps = {
children: ReactNode;
/** 한 번 오가는 데 걸리는 시간 (기본 2000ms) */
duration?: number;
/** CSS easing (기본 ease-out) */
easing?: string;
/** 버튼 색 */
color?: string;
onClick?: (e: MouseEvent<HTMLButtonElement>) => void;
type?: "button" | "submit";
};
type Vars = CSSProperties & {
"--duration"?: string;
"--easing"?: string;
"--color"?: string;
};
export default function PulseButton({
children,
duration = 2000,
easing = "ease-out",
color = "#0a4fe8",
onClick,
type = "button",
}: PulseButtonProps) {
const vars: Vars = {
"--duration": `${duration}ms`,
"--easing": easing,
"--color": color,
};
return (
<button type={type} className={styles.btn} style={vars} onClick={onClick}>
{children}
</button>
);
}04 · パルス — 計測 累積面積 7.199% · 動いたフレーム 23/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る
主張しすぎない強調に
$duration: 2s;
$easing: ease-out;
.a-pulse { animation: pulse $duration $easing infinite; }
@keyframes pulse {
0%, 100% { transform: scale(1); opacity: 1; }
50% { transform: scale(1.06); opacity: .92; }
}
05揺れる
0.5秒で自ら止まる単発クラス
.a-shakeはtranslateXを左右に6回短く往復させて0.5秒で自ら止まる単発クラスで、ループしません。フォームのエラー専用ではなく、新機能バッジを揺らす、チュートリアルで特定の箇所を指し示すといった汎用的な注意喚起に使います。

<main class="stage">
<button class="badge a-shake is-demo" type="button">新着</button>
</main>@use "../../../../site/static/demo-tokens" as *;
// ── 변수 3개 ──────────────────────────────────────────
$duration: 500ms; // 흔들리는 한 번의 시간
$easing: cubic-bezier(.36, .07, .19, .97);
$color: $subject-cream;
$amount: 8px; // 흔들리는 폭 (변수는 아니지만 셋만 바꾸면 되게 여기 모아 둔다)
// ── 데모 무대 ───────────────────────────────
@include stage($stage-ink);
.badge {
border: 0;
border-radius: $r-pill;
padding: 16px 36px;
background: $color;
color: $stage-ink;
font-weight: 700; font-size: clamp(15px, 4vw, 18px); line-height: 1; font-family: inherit;
cursor: pointer;
-webkit-tap-highlight-color: transparent;
}
// ── 실제 쓰임: is-shaking 을 켜면 여섯 번 왕복하고 스스로 멈춘다 ─
.a-shake.is-shaking { animation: shake $duration $easing 1; }
// ── 데모: 클릭 전까지 2초마다 스스로 다시 흔든다 ──────────
.a-shake.is-demo { animation: shake-loop $dur-loop $easing infinite; }
@keyframes shake {
10%, 90% { transform: translateX($amount * -.5); }
20%, 80% { transform: translateX($amount); }
30%, 70% { transform: translateX($amount * -1); }
40%, 60% { transform: translateX($amount * .7); }
50% { transform: translateX($amount * -.7); }
}
// shake 와 같은 모양을 2초 루프의 60~80% 구간에 압축해 넣고 나머지는 쉰다
@keyframes shake-loop {
0%, 60% { transform: translateX(0); }
62%, 78% { transform: translateX($amount * -.5); }
64%, 76% { transform: translateX($amount); }
66%, 74% { transform: translateX($amount * -1); }
68%, 72% { transform: translateX($amount * .7); }
70% { transform: translateX($amount * -.7); }
80%, 100% { transform: translateX(0); }
}
// ── 접근성: 흔들림 대신 배경으로 알린다 ──────────────────
@media (prefers-reduced-motion: reduce) {
.a-shake.is-shaking, .a-shake.is-demo { animation: none; }
.a-shake.is-shaking { background: $error-soft; }
}html, body {
margin: 0;
overflow: hidden;
}
.stage {
box-sizing: border-box;
min-height: 100vh;
display: grid;
place-items: center;
margin: 0;
background: #17141a;
font-family: -apple-system, "Segoe UI", "Noto Sans KR", sans-serif;
padding: clamp(8px, 4vw, 24px);
}
.badge {
border: 0;
border-radius: 999px;
padding: 16px 36px;
background: #fff7e6;
color: #17141a;
font-weight: 700;
font-size: clamp(15px, 4vw, 18px);
line-height: 1;
font-family: inherit;
cursor: pointer;
-webkit-tap-highlight-color: transparent;
}
.a-shake.is-shaking {
animation: shake 500ms cubic-bezier(0.36, 0.07, 0.19, 0.97) 1;
}
.a-shake.is-demo {
animation: shake-loop 2s cubic-bezier(0.36, 0.07, 0.19, 0.97) infinite;
}
@keyframes shake {
10%, 90% {
transform: translateX(-4px);
}
20%, 80% {
transform: translateX(8px);
}
30%, 70% {
transform: translateX(-8px);
}
40%, 60% {
transform: translateX(5.6px);
}
50% {
transform: translateX(-5.6px);
}
}
@keyframes shake-loop {
0%, 60% {
transform: translateX(0);
}
62%, 78% {
transform: translateX(-4px);
}
64%, 76% {
transform: translateX(8px);
}
66%, 74% {
transform: translateX(-8px);
}
68%, 72% {
transform: translateX(5.6px);
}
70% {
transform: translateX(-5.6px);
}
80%, 100% {
transform: translateX(0);
}
}
@media (prefers-reduced-motion: reduce) {
.a-shake.is-shaking, .a-shake.is-demo {
animation: none;
}
.a-shake.is-shaking {
background: #ffe0d8;
}
}// 누르면 다시 흔든다. 실제 서비스에서는 원하는 시점에 is-shaking 을 켠다.
var b = document.querySelector('.a-shake');
b.addEventListener('click', function () {
b.classList.remove('is-demo'); b.classList.remove('is-shaking');
void b.offsetWidth;
b.classList.add('is-shaking');
});
b.addEventListener('animationend', function () { b.classList.remove('is-shaking'); });import { useCallback, useState } from "react";
import type { CSSProperties, MouseEvent, ReactNode } from "react";
import styles from "./ShakeBadge.module.scss";
export type ShakeBadgeProps = {
children: ReactNode;
/** 한 번 흔들리는 데 걸리는 시간 (기본 500ms) */
duration?: number;
/** CSS easing */
easing?: string;
/** 배지 색 */
color?: string;
onClick?: (e: MouseEvent<HTMLButtonElement>) => void;
};
type Vars = CSSProperties & {
"--duration"?: string;
"--easing"?: string;
"--color"?: string;
};
// 실제 쓰임: shakeKey 를 바꿀 때마다(예: 검증 실패 횟수) 다시 흔든다.
export default function ShakeBadge({
children,
duration = 500,
easing = "cubic-bezier(.36,.07,.19,.97)",
color = "#fff7e6",
onClick,
}: ShakeBadgeProps) {
const [shaking, setShaking] = useState(false);
const handle = useCallback(
(e: MouseEvent<HTMLButtonElement>) => {
setShaking(false);
requestAnimationFrame(() => setShaking(true));
onClick?.(e);
},
[onClick]
);
const vars: Vars = {
"--duration": `${duration}ms`,
"--easing": easing,
"--color": color,
};
return (
<button
type="button"
className={`${styles.badge} ${shaking ? styles.shaking : ""}`}
style={vars}
onClick={handle}
onAnimationEnd={() => setShaking(false)}
>
{children}
</button>
);
}05 · 揺れる — 計測 累積面積 0.845% · 動いたフレーム 6/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る
エラー専用ではない使い道
$duration: 500ms;
$easing: cubic-bezier(.36, .07, .19, .97);
$amount: 8px;
.a-shake.is-shaking { animation: shake $duration $easing 1; }
@keyframes shake {
10%, 90% { transform: translateX($amount * -.5); }
20%, 80% { transform: translateX($amount); }
30%, 70% { transform: translateX($amount * -1); }
40%, 60% { transform: translateX($amount * .7); }
50% { transform: translateX($amount * -.7); }
}
06ぐらぐら
05の揺れより不規則になる理由
.a-wobbleはtranslateXとrotateを同じ@keyframesの中でタイミングをずらして重ね、05の揺れより不規則で生きた動きになります。ドラッグできるカードや並べ替え中のリスト項目が「動かせます」と伝えたいときに向いています。

<main class="stage">
<div class="card a-wobble">
<span class="card__handle" aria-hidden="true">⋮⋮</span>
カードA
</div>
</main>@use "../../../../site/static/demo-tokens" as *;
// ── 변수 3개 ──────────────────────────────────────────
$duration: 1s;
$easing: ease-in-out;
$color: $stage-ink;
// ── 데모 무대 ───────────────────────────────
@include stage($stage-yellow);
.card {
display: inline-flex;
align-items: center;
gap: 10px;
padding: 16px 28px;
border-radius: $r-card;
background: $subject-cream;
color: $color;
font-weight: 700; font-size: clamp(15px, 4vw, 18px); line-height: 1; font-family: inherit;
box-shadow: $shadow-raised;
}
.card__handle {
letter-spacing: -2px;
opacity: .72; // AA: 크림 면 위 핸들 글자 6.9:1
}
// ── .a-wobble 은 translateX 와 rotate 를 같은 keyframes 안에서 어긋나게 겹친다 ─
.a-wobble {
animation: wobble $duration $easing infinite;
}
@keyframes wobble {
0%, 100% { transform: translateX(0) rotate(0deg); }
15% { transform: translateX(-6px) rotate(-4deg); }
30% { transform: translateX(5px) rotate(3deg); }
45% { transform: translateX(-4px) rotate(-2deg); }
60% { transform: translateX(3px) rotate(2deg); }
75% { transform: translateX(-2px) rotate(-1deg); }
}
@media (prefers-reduced-motion: reduce) {
.a-wobble { animation: 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 {
display: inline-flex;
align-items: center;
gap: 10px;
padding: 16px 28px;
border-radius: 16px;
background: #fff7e6;
color: #17141a;
font-weight: 700;
font-size: clamp(15px, 4vw, 18px);
line-height: 1;
font-family: inherit;
box-shadow: 0 14px 24px rgba(23, 20, 26, 0.32);
}
.card__handle {
letter-spacing: -2px;
opacity: 0.72;
}
.a-wobble {
animation: wobble 1s ease-in-out infinite;
}
@keyframes wobble {
0%, 100% {
transform: translateX(0) rotate(0deg);
}
15% {
transform: translateX(-6px) rotate(-4deg);
}
30% {
transform: translateX(5px) rotate(3deg);
}
45% {
transform: translateX(-4px) rotate(-2deg);
}
60% {
transform: translateX(3px) rotate(2deg);
}
75% {
transform: translateX(-2px) rotate(-1deg);
}
}
@media (prefers-reduced-motion: reduce) {
.a-wobble {
animation: none;
}
}import type { CSSProperties, ReactNode } from "react";
import styles from "./WobbleCard.module.scss";
export type WobbleCardProps = {
children: ReactNode;
/** 한 번 도는 데 걸리는 시간 (기본 1000ms) */
duration?: number;
/** CSS easing (기본 ease-in-out) */
easing?: string;
/** 글자·핸들 색 */
color?: string;
};
type Vars = CSSProperties & {
"--duration"?: string;
"--easing"?: string;
"--color"?: string;
};
export default function WobbleCard({
children,
duration = 1000,
easing = "ease-in-out",
color = "#17141a",
}: WobbleCardProps) {
const vars: Vars = {
"--duration": `${duration}ms`,
"--easing": easing,
"--color": color,
};
return (
<div className={styles.card} style={vars}>
<span className={styles.handle} aria-hidden="true">⋮⋮</span>
{children}
</div>
);
}06 · ぐらぐら — 計測 累積面積 2.026% · 動いたフレーム 23/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る
並べ替え中のリスト項目に
$duration: 1s;
$easing: ease-in-out;
.a-wobble { animation: wobble $duration $easing infinite; }
@keyframes wobble {
0%, 100% { transform: translateX(0) rotate(0deg); }
15% { transform: translateX(-6px) rotate(-4deg); }
30% { transform: translateX(5px) rotate(3deg); }
45% { transform: translateX(-4px) rotate(-2deg); }
60% { transform: translateX(3px) rotate(2deg); }
75% { transform: translateX(-2px) rotate(-1deg); }
}
07点滅
完全には消さない点滅
.a-flashはopacityを1→0→1の順で2回区切って点滅させ、要素を完全には消さずに確実に目を引きます。緊急の告知アイコンやライブ配信中を示すドットのように、はっきり点滅させたい場所で使います。

<main class="stage">
<div class="notice">
<span class="notice__dot a-flash" aria-hidden="true"></span>
緊急告知
</div>
</main>@use "../../../../site/static/demo-tokens" as *;
// ── 변수 3개 ──────────────────────────────────────────
$duration: 1s;
$easing: ease-in-out;
$color: $subject-cream;
// ── 데모 무대 ───────────────────────────────
@include stage($stage-orange);
.notice {
display: inline-flex;
align-items: center;
gap: 12px;
padding: 14px 28px;
border-radius: $r-pill;
background: rgba(255, 255, 255, .14);
color: $subject-ink; // AA: 주황 면(흰 14% 겹침 #d2ada4) 위 6.28:1 — 알림 점은 크림 유지
font-weight: 700; font-size: clamp(15px, 4vw, 18px); line-height: 1; font-family: inherit;
}
.notice__dot {
width: 16px;
height: 16px;
border-radius: 50%;
background: $color;
flex: none;
}
// ── .a-flash 는 opacity 만으로 두 번 끊어 깜빡인다. 요소는 사라지지 않는다 ─
.a-flash {
animation: flash $duration $easing infinite;
}
@keyframes flash {
0%, 50%, 100% { opacity: 1; }
25%, 75% { opacity: .15; }
}
@media (prefers-reduced-motion: reduce) {
.a-flash { animation: none; }
}html, body {
margin: 0;
overflow: hidden;
}
.stage {
box-sizing: border-box;
min-height: 100vh;
display: grid;
place-items: center;
margin: 0;
background: #ff4d1f;
font-family: -apple-system, "Segoe UI", "Noto Sans KR", sans-serif;
padding: clamp(8px, 4vw, 24px);
}
.notice {
display: inline-flex;
align-items: center;
gap: 12px;
padding: 14px 28px;
border-radius: 999px;
background: rgba(255, 255, 255, 0.14);
color: #17141a;
font-weight: 700;
font-size: clamp(15px, 4vw, 18px);
line-height: 1;
font-family: inherit;
}
.notice__dot {
width: 16px;
height: 16px;
border-radius: 50%;
background: #fff7e6;
flex: none;
}
.a-flash {
animation: flash 1s ease-in-out infinite;
}
@keyframes flash {
0%, 50%, 100% {
opacity: 1;
}
25%, 75% {
opacity: 0.15;
}
}
@media (prefers-reduced-motion: reduce) {
.a-flash {
animation: none;
}
}import type { CSSProperties, ReactNode } from "react";
import styles from "./FlashNotice.module.scss";
export type FlashNoticeProps = {
children: ReactNode;
/** 한 번 깜빡이는 데 걸리는 시간 (기본 1000ms) */
duration?: number;
/** CSS easing (기본 ease-in-out) */
easing?: string;
/** 점·글자 색 */
color?: string;
};
type Vars = CSSProperties & {
"--duration"?: string;
"--easing"?: string;
"--color"?: string;
};
export default function FlashNotice({
children,
duration = 1000,
easing = "ease-in-out",
color = "#fff7e6",
}: FlashNoticeProps) {
const vars: Vars = {
"--duration": `${duration}ms`,
"--easing": easing,
"--color": color,
};
return (
<div className={styles.notice} style={vars}>
<span className={styles.dot} aria-hidden="true" />
{children}
</div>
);
}07 · 点滅 — 計測 累積面積 0.15% · 動いたフレーム 23/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る
緊急の告知アイコンに
$duration: 1s;
$easing: ease-in-out;
.a-flash { animation: flash $duration $easing infinite; }
@keyframes flash {
0%, 50%, 100% { opacity: 1; }
25%, 75% { opacity: .15; }
}
08鼓動
ドッ・ドッ・休みのリズム
.a-heartbeatはscaleを短く2回弾ませたあと休む「ドッ・ドッ・休み」のリズムで@keyframesを区切り、均等な繰り返しより実際の心拍に近い間隔で動きます。いいねアイコンの待機状態やダッシュボードの監視ドットに向いています。

<main class="stage">
<div class="like">
<span class="heart a-heartbeat" aria-hidden="true"></span>
いいね 267
</div>
</main>@use "../../../../site/static/demo-tokens" as *;
// ── 변수 3개 ──────────────────────────────────────────
$duration: 1.2s;
$easing: ease-in-out;
$color: $subject-blue;
// ── 데모 무대 ───────────────────────────────
@include stage($stage-paper);
.like {
display: inline-flex;
align-items: center;
gap: 12px;
color: $stage-ink;
font-weight: 700; font-size: clamp(15px, 4vw, 18px); line-height: 1; font-family: inherit;
}
// 순수 CSS 하트 — 원 두 개 + 45도 회전한 사각형
.heart {
position: relative;
display: inline-block;
width: 34px;
height: 30px;
flex: none;
}
.heart::before,
.heart::after {
content: "";
position: absolute;
top: 0;
width: 18px;
height: 28px;
background: $color;
border-radius: 18px 18px 0 0;
}
.heart::before { left: 17px; transform: rotate(-45deg); transform-origin: 0 100%; } // 규칙8: 하트반쪽 밑변꼭짓점 회전
.heart::after { left: 0; transform: rotate(45deg); transform-origin: 100% 100%; } // 규칙8: 하트반쪽 밑변꼭짓점 회전
// ── .a-heartbeat 는 scale 을 두 번 짧게 튀우고 쉬어, 실제 심박처럼 불균등하다 ─
.a-heartbeat {
animation: heartbeat $duration $easing infinite;
}
@keyframes heartbeat {
0%, 100% { transform: scale(1); }
14% { transform: scale(1.24); }
28% { transform: scale(1); }
42% { transform: scale(1.18); }
70% { transform: scale(1); }
}
@media (prefers-reduced-motion: reduce) {
.a-heartbeat { animation: none; }
}html, body {
margin: 0;
overflow: hidden;
}
.stage {
box-sizing: border-box;
min-height: 100vh;
display: grid;
place-items: center;
margin: 0;
background: #ffffff;
font-family: -apple-system, "Segoe UI", "Noto Sans KR", sans-serif;
padding: clamp(8px, 4vw, 24px);
}
.like {
display: inline-flex;
align-items: center;
gap: 12px;
color: #17141a;
font-weight: 700;
font-size: clamp(15px, 4vw, 18px);
line-height: 1;
font-family: inherit;
}
.heart {
position: relative;
display: inline-block;
width: 34px;
height: 30px;
flex: none;
}
.heart::before,
.heart::after {
content: "";
position: absolute;
top: 0;
width: 18px;
height: 28px;
background: #2f6df6;
border-radius: 18px 18px 0 0;
}
.heart::before {
left: 17px;
transform: rotate(-45deg);
transform-origin: 0 100%;
}
.heart::after {
left: 0;
transform: rotate(45deg);
transform-origin: 100% 100%;
}
.a-heartbeat {
animation: heartbeat 1.2s ease-in-out infinite;
}
@keyframes heartbeat {
0%, 100% {
transform: scale(1);
}
14% {
transform: scale(1.24);
}
28% {
transform: scale(1);
}
42% {
transform: scale(1.18);
}
70% {
transform: scale(1);
}
}
@media (prefers-reduced-motion: reduce) {
.a-heartbeat {
animation: none;
}
}import type { CSSProperties, ReactNode } from "react";
import styles from "./HeartbeatLike.module.scss";
export type HeartbeatLikeProps = {
children: ReactNode;
/** 한 박자가 걸리는 시간 (기본 1200ms) */
duration?: number;
/** CSS easing (기본 ease-in-out) */
easing?: string;
/** 하트 색 */
color?: string;
};
type Vars = CSSProperties & {
"--duration"?: string;
"--easing"?: string;
"--color"?: string;
};
export default function HeartbeatLike({
children,
duration = 1200,
easing = "ease-in-out",
color = "#2f6df6",
}: HeartbeatLikeProps) {
const vars: Vars = {
"--duration": `${duration}ms`,
"--easing": easing,
"--color": color,
};
return (
<div className={styles.like} style={vars}>
<span className={styles.heart} aria-hidden="true" />
{children}
</div>
);
}08 · 鼓動 — 計測 累積面積 0.356% · 動いたフレーム 19/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る
ダッシュボードの監視ドットに
$duration: 1.2s;
$easing: ease-in-out;
.a-heartbeat { animation: heartbeat $duration $easing infinite; }
@keyframes heartbeat {
0%, 100% { transform: scale(1); }
14% { transform: scale(1.24); }
28% { transform: scale(1); }
42% { transform: scale(1.18); }
70% { transform: scale(1); }
}
09ぷるぷる
scaleXとscaleYを逆方向へ
.a-jellyはscaleXとscaleYを互いに逆方向へずらして動かし、押された瞬間にゼリーのように揺れてから元の形に戻ります。カート追加の完了アイコンやクリック後に弾むボタンのフィードバックに使います。

<main class="stage">
<button class="btn a-jelly is-demo" type="button">カートに追加</button>
</main>@use "../../../../site/static/demo-tokens" as *;
// ── 변수 3개 ──────────────────────────────────────────
$duration: 600ms;
$easing: cubic-bezier(.68, -0.55, .27, 1.55); // 과도하게 넘겼다 돌아오는 탄성
$color: $subject-cream;
// ── 데모 무대 ───────────────────────────────
@include stage($stage-ink);
.btn {
border: 0;
border-radius: $r-control;
padding: 16px 34px;
background: $color;
color: $stage-ink;
font-weight: 700; font-size: clamp(15px, 4vw, 18px); line-height: 1; font-family: inherit;
cursor: pointer;
-webkit-tap-highlight-color: transparent;
}
// ── 실제 쓰임: is-jellying 을 켜면 한 번 출렁이고 멈춘다 ─────
.a-jelly.is-jellying { animation: jelly $duration $easing 1; }
// ── 데모: 클릭 전까지 2초마다 스스로 다시 출렁인다 ─────────
.a-jelly.is-demo { animation: jelly-loop $dur-loop $easing infinite; }
// scaleX 와 scaleY 를 서로 반대로 어긋나게 — 커지면서 좁아지고, 좁아지면서 커진다
@keyframes jelly {
0% { transform: scale(1, 1); }
20% { transform: scale(1.25, .75); }
40% { transform: scale(.75, 1.25); }
60% { transform: scale(1.15, .85); }
80% { transform: scale(.95, 1.05); }
100% { transform: scale(1, 1); }
}
// 같은 모양을 2초 루프의 60~100% 구간에 압축해 넣고 나머지는 쉰다
@keyframes jelly-loop {
0%, 60% { transform: scale(1, 1); }
68% { transform: scale(1.25, .75); }
76% { transform: scale(.75, 1.25); }
84% { transform: scale(1.15, .85); }
92% { transform: scale(.95, 1.05); }
100% { transform: scale(1, 1); }
}
@media (prefers-reduced-motion: reduce) {
.a-jelly.is-jellying, .a-jelly.is-demo { animation: none; }
}html, body {
margin: 0;
overflow: hidden;
}
.stage {
box-sizing: border-box;
min-height: 100vh;
display: grid;
place-items: center;
margin: 0;
background: #17141a;
font-family: -apple-system, "Segoe UI", "Noto Sans KR", sans-serif;
padding: clamp(8px, 4vw, 24px);
}
.btn {
border: 0;
border-radius: 12px;
padding: 16px 34px;
background: #fff7e6;
color: #17141a;
font-weight: 700;
font-size: clamp(15px, 4vw, 18px);
line-height: 1;
font-family: inherit;
cursor: pointer;
-webkit-tap-highlight-color: transparent;
}
.a-jelly.is-jellying {
animation: jelly 600ms cubic-bezier(0.68, -0.55, 0.27, 1.55) 1;
}
.a-jelly.is-demo {
animation: jelly-loop 2s cubic-bezier(0.68, -0.55, 0.27, 1.55) infinite;
}
@keyframes jelly {
0% {
transform: scale(1, 1);
}
20% {
transform: scale(1.25, 0.75);
}
40% {
transform: scale(0.75, 1.25);
}
60% {
transform: scale(1.15, 0.85);
}
80% {
transform: scale(0.95, 1.05);
}
100% {
transform: scale(1, 1);
}
}
@keyframes jelly-loop {
0%, 60% {
transform: scale(1, 1);
}
68% {
transform: scale(1.25, 0.75);
}
76% {
transform: scale(0.75, 1.25);
}
84% {
transform: scale(1.15, 0.85);
}
92% {
transform: scale(0.95, 1.05);
}
100% {
transform: scale(1, 1);
}
}
@media (prefers-reduced-motion: reduce) {
.a-jelly.is-jellying, .a-jelly.is-demo {
animation: none;
}
}// 누르면 젤리처럼 출렁이고 멈춘다. 실제로는 담기 요청이 성공했을 때 is-jellying 을 켠다.
var b = document.querySelector('.a-jelly');
b.addEventListener('click', function () {
b.classList.remove('is-demo'); b.classList.remove('is-jellying');
void b.offsetWidth;
b.classList.add('is-jellying');
});
b.addEventListener('animationend', function () { b.classList.remove('is-jellying'); });import { useCallback, useState } from "react";
import type { CSSProperties, MouseEvent, ReactNode } from "react";
import styles from "./JellyButton.module.scss";
export type JellyButtonProps = {
children: ReactNode;
/** 한 번 출렁이는 데 걸리는 시간 (기본 600ms) */
duration?: number;
/** CSS easing (기본 과도하게 넘겼다 돌아오는 커스텀 베지어) */
easing?: string;
/** 버튼 색 */
color?: string;
onClick?: (e: MouseEvent<HTMLButtonElement>) => void;
};
type Vars = CSSProperties & {
"--duration"?: string;
"--easing"?: string;
"--color"?: string;
};
// 실제 쓰임: 담기 요청이 성공했을 때 onClick 안에서 API 를 부르고, 결과와 무관하게 한 번 출렁인다.
export default function JellyButton({
children,
duration = 600,
easing = "cubic-bezier(.68,-0.55,.27,1.55)",
color = "#fff7e6",
onClick,
}: JellyButtonProps) {
const [jellying, setJellying] = useState(false);
const handle = useCallback(
(e: MouseEvent<HTMLButtonElement>) => {
setJellying(false);
requestAnimationFrame(() => setJellying(true));
onClick?.(e);
},
[onClick]
);
const vars: Vars = {
"--duration": `${duration}ms`,
"--easing": easing,
"--color": color,
};
return (
<button
type="button"
className={`${styles.btn} ${jellying ? styles.jellying : ""}`}
style={vars}
onClick={handle}
onAnimationEnd={() => setJellying(false)}
>
{children}
</button>
);
}09 · ぷるぷる — 計測 累積面積 7.007% · 動いたフレーム 10/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る
カート追加の完了アイコンに
$duration: 600ms;
$easing: cubic-bezier(.68, -0.55, .27, 1.55);
.a-jelly.is-jellying { animation: jelly $duration $easing 1; }
@keyframes jelly {
0% { transform: scale(1, 1); }
20% { transform: scale(1.25, .75); }
40% { transform: scale(.75, 1.25); }
60% { transform: scale(1.15, .85); }
80% { transform: scale(.95, 1.05); }
100% { transform: scale(1, 1); }
}
CSSのanimationショートハンドはなぜこの順番なのか
8つの値の並び順
animation:の1行に値をまとめて書くとき、順番を覚えていないとdurationとdelayを取り違えがちです。MDNのanimationショートハンドが示す順番は次のとおりです。
| 順番 | プロパティ | この記事の例 | 既定値 |
|---|---|---|---|
| 1 | animation-name | float | none |
| 2 | animation-duration | 3s | 0s |
| 3 | animation-timing-function | ease-in-out | ease |
| 4 | animation-delay | (省略時0) | 0s |
| 5 | animation-iteration-count | infinite | 1 |
| 6 | animation-direction | (省略時normal) | normal |
| 7 | animation-fill-mode | (省略時none) | none |
| 8 | animation-play-state | (省略時running) | running |
fill-modeが決めること
animation-fill-modeは、再生の前後にキーフレームの値を保つかどうかを決めます。MDNのanimation-fill-modeがまとめる4つの値は次のとおりです。
| 値 | 効果 |
|---|---|
none |
既定。再生前後とも元のスタイルのまま |
forwards |
再生が終わったあと、最後のキーフレームの値を保つ |
backwards |
animation-delayのあいだ、最初のキーフレームの値を先に適用する |
both |
forwardsとbackwardsの両方を適用する |
05と09にfill-modeが要らない理由
05の揺れと09のぷるぷるは1回で再生を終えますが、どちらも0%と100%が元の形と同じなので、fill-modeを指定しなくても自然に定位置へ戻ります。最後のキーフレームが元の見た目と違う場合は、forwardsを足さないと再生後の形が戻ってしまいます。
どこで壊れるか — 落とし穴
クラスを重ねると片方しか動かない
.a-floatと.a-spinのように別々のユーティリティクラスを同じ要素へ重ねて付けると、どちらもanimationショートハンドを丸ごと宣言しているため、CSSのカスケード規則で後に書かれたクラスが先のクラスを完全に上書きし、片方しか動きません。ひとつの要素へ2つの動きを重ねるには、animation-name: float, spin;のように名前をカンマで並べ、animation-durationやanimation-timing-functionも同じ数だけ並べる必要があります。両方が同じプロパティ(transform)を動かす場合は既定のreplaceが互いを上書きするため、animation-composition: addを指定すると2つのtransform値を足し合わせて両方を反映できます。
単発アニメーションの計測値
05の揺れと09のぷるぷるは一度きりで止まる単発アニメーションで、24フレームの計測のうちそれぞれ6フレーム・10フレームしか動きが記録されませんでした。この2つは9つの中でも可動時間が際立って短いのに、変化の強度の平均値はぷるぷるの方が9つの中でいちばん高く出ています。短い動きほど地味になるとは限らないと、実際に組んで測るまでわかりませんでした。この違いも含めた9ファイルの解凍パスワードはnft4q6u2で、下のカードから受け取れます。
05や09のような単発の動きが短くても伝わるのは軌道が整っているからで、逆に線そのものをわざと揺らした質感がほしい場面もあります。ここと同じ2つのプロパティだけでその手触りを出した例が、手書き風 デザイン LPキットのクレヨン風セクション9選です。
アクセシビリティ
動きだけを止めて情報は残す
動作を減らす設定を有効にしているユーザーのために、9つとも@media (prefers-reduced-motion: reduce)の中でanimation: noneに切り替えます。動きだけを止めて情報は残すのが原則で、9つとも静止した状態のまま内容自体は変わらず読めます。このメディアクエリをどのブラウザが読み取るかは、MDNのprefers-reduced-motionのドキュメントにある対応表で確認できます。
@media (prefers-reduced-motion: reduce) {
.a-float, .a-bounce, .a-spin, .a-pulse,
.a-wobble, .a-flash, .a-heartbeat { animation: none; }
.a-shake.is-shaking, .a-jelly.is-jellying { animation: none; }
}
次に読む
「css アニメーション 作り方」をもっと知りたいときはCSSアニメーションのカテゴリーに、このサイトの成り立ちは紹介ページにまとめています。
FAQ
「css アニメーション コピペ」で集めた9つを1つの画面に全部使ってもいいですか?
入れること自体はできますが、1画面には2〜3個までにとどめるのがおすすめです。04のパルスと07の点滅のように、どちらも「注目して」という信号を持つクラスを同じ画面に何個も置くと、どれも特別に見えなくなります。03の回転と08の鼓動のように状態が異なるもの同士なら、並んでいても違和感はありません。1画面で動かす対象をアイコン1つに絞りたいときは、要素全体ではなくSVGの線を動かすCSS アイコン アニメーション 9選のほうが静かに収まります。
2つのアニメーションを1つの要素に重ねるにはどうすればいいですか?
クラスを2つそのまま付けるだけでは、後に書いたクラスのanimationがすべてを上書きしてしまい片方しか動きません。animation-nameにカンマ区切りで名前を並べ、同じプロパティを動かす場合はanimation-composition: addやaccumulateで合成する方法を使ってください。具体的な例は落とし穴のセクションにあります。
Reactではクラスの切り替えをどう扱いますか?
zipのreact/フォルダの各コンポーネントは、duration・easing・colorをpropsで受け取ってCSS変数へ渡す作りです。05の揺れと09のぷるぷるのように一度きりで止まるものは、useStateでクラスをオンにし、onAnimationEndでオフに戻すことで、もう一度クリックしたときにも再生されるようにしています。









