06遅延表示
マウスを乗せてもすぐには出ず、少し留まったときだけ現れるので通り過ぎでは出ない。
使いどころアイコンが密集したツールバー、リストの各行にあるアイコン
@keyframes delay-loop짧은 등장긴 유지

<main class="stage">
<div class="tip-wrap">
<button class="save-btn" type="button" aria-describedby="tip06">保存</button>
<span class="tooltip is-demo" role="tooltip" id="tip06">変更内容は自動的に保存されます</span>
</div>
</main>@use "../../../../site/static/demo-tokens" as *;
$duration: $dur-loop; // 숨김→표시→숨김 한 주기 (지연 포함)
$easing: $ease-out;
$color: $subject-ink; // 버튼 색 (노란 무대라 먹색)
@include stage($stage-yellow);
.tip-wrap { position: relative; }
.save-btn {
border: 0;
border-radius: $r-control;
padding: 12px 26px;
font-weight: 700;
font-size: 15px;
line-height: 1;
font-family: inherit;
color: $stage-yellow;
background: $color;
cursor: pointer;
}
.tooltip {
position: absolute;
bottom: calc(100% + 8px);
left: 50%;
transform-origin: bottom center; // 규칙8: 버튼 쪽 아래변 축 — 흔들림의 회전 중심
transform: translateX(-50%) scale(.85);
opacity: 0;
background: $stage-ink;
color: $stage-yellow;
padding: 8px 14px;
border-radius: $r-control;
font-size: clamp(12px, 3.4vw, 14px);
max-width: clamp(140px, 42vw, 220px);
white-space: nowrap; // 한글은 글자마다 줄바꿈 후보라, 없으면 세로로 한 글자씩 쪼개진다
text-align: center;
pointer-events: none;
}
// 마우스를 올려도 곧바로 뜨지 않는다 — 25%(≈500ms) 까지 숨겨 두고, 뜬 뒤엔 길게 유지한다
// 지연 뒤 등장에 스프링 한 겹 — 스케일 .85 → 1.12 오버슛 → 감쇠 반동, 회전은 아래변 축으로 2.4° → -1.4° → .5° 감쇠
@keyframes delay-loop {
0%, 20% { opacity: 0; transform: translateX(-50%) scale(.85) rotate(0deg); }
28% { opacity: 1; transform: translateX(-50%) scale(1.12) rotate(2.4deg); }
37% { opacity: 1; transform: translateX(-50%) scale(.95) rotate(-1.4deg); }
45% { opacity: 1; transform: translateX(-50%) scale(1.02) rotate(.5deg); }
51%, 80% { opacity: 1; transform: translateX(-50%) scale(1) rotate(0deg); }
100% { opacity: 0; transform: translateX(-50%) scale(.85) rotate(0deg); }
}
.tooltip.is-demo {
animation: delay-loop $duration $easing infinite;
}
@media (prefers-reduced-motion: reduce) {
.tooltip.is-demo { animation: none; opacity: 1; transform: translateX(-50%) scale(1) rotate(0deg); }
}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);
}
.tip-wrap {
position: relative;
}
.save-btn {
border: 0;
border-radius: 12px;
padding: 12px 26px;
font-weight: 700;
font-size: 15px;
line-height: 1;
font-family: inherit;
color: #ffd23f;
background: #17141a;
cursor: pointer;
}
.tooltip {
position: absolute;
bottom: calc(100% + 8px);
left: 50%;
transform-origin: bottom center;
transform: translateX(-50%) scale(0.85);
opacity: 0;
background: #17141a;
color: #ffd23f;
padding: 8px 14px;
border-radius: 12px;
font-size: clamp(12px, 3.4vw, 14px);
max-width: clamp(140px, 42vw, 220px);
white-space: nowrap;
text-align: center;
pointer-events: none;
}
@keyframes delay-loop {
0%, 20% {
opacity: 0;
transform: translateX(-50%) scale(0.85) rotate(0deg);
}
28% {
opacity: 1;
transform: translateX(-50%) scale(1.12) rotate(2.4deg);
}
37% {
opacity: 1;
transform: translateX(-50%) scale(0.95) rotate(-1.4deg);
}
45% {
opacity: 1;
transform: translateX(-50%) scale(1.02) rotate(0.5deg);
}
51%, 80% {
opacity: 1;
transform: translateX(-50%) scale(1) rotate(0deg);
}
100% {
opacity: 0;
transform: translateX(-50%) scale(0.85) rotate(0deg);
}
}
.tooltip.is-demo {
animation: delay-loop 2s ease-out infinite;
}
@media (prefers-reduced-motion: reduce) {
.tooltip.is-demo {
animation: none;
opacity: 1;
transform: translateX(-50%) scale(1) rotate(0deg);
}
}import { useRef, useState } from "react";
import type { CSSProperties, ReactNode } from "react";
import styles from "./DelayTooltip.module.scss";
export type DelayTooltipProps = {
label: ReactNode;
text: string;
/** 호버 후 뜨기까지 기다리는 시간 (기본 400ms) */
delay?: number;
duration?: number;
easing?: string;
color?: string;
};
type Vars = CSSProperties & { "--duration"?: string; "--easing"?: string; "--color"?: string };
export default function DelayTooltip({
label,
text,
delay = 400,
duration = 160,
easing = "ease-out",
color = "#17141a",
}: DelayTooltipProps) {
const [open, setOpen] = useState(false);
const timer = useRef<ReturnType<typeof setTimeout> | null>(null);
const vars: Vars = { "--duration": `${duration}ms`, "--easing": easing, "--color": color };
const start = () => {
timer.current = setTimeout(() => setOpen(true), delay);
};
const cancel = () => {
if (timer.current) clearTimeout(timer.current);
setOpen(false);
};
return (
<span className={styles.wrap} style={vars}>
<button
type="button"
className={styles.btn}
aria-describedby="delay-tooltip"
onMouseEnter={start}
onMouseLeave={cancel}
onFocus={start}
onBlur={cancel}
>
{label}
</button>
<span id="delay-tooltip" role="tooltip" className={styles.tooltip} data-open={open}>
{text}
</span>
</span>
);
}06 遅延表示 — 計測 累積面積 6.83% · 動いたフレーム 13/23GODRICH オリジナル · SCSS と React は zip に同梱 · CSS ツールチップ 吹き出しアニメーション9選 一行でコピペOK
このデモが属する記事
CSS ツールチップ 吹き出しアニメーション9選 一行でコピペOKcss ツールチップ 吹き出しは、ボタンやアイコンにカーソルを合わせると短い説明が浮かび上がる小さな部品です。今回は9種類を選び、位置合わせの基本からCSS Anchor PositioningやPopover APIといった新しい仕組みま この記事のデモをすべて見る


