04Mockup lơ lửng
Cả thẻ mockup có thanh trình duyệt nhỏ phía trên trôi qua lại giữa translateY và một chút rotate, nhìn như đang nhẹ nhàng nổi trong không trung.
Dùng choMockup ảnh màn hình sản phẩm app·SaaS, hero giới thiệu dashboard
transform: translateYrotate@keyframes

<main class="stage is-demo">
<div class="mockup">
<div class="mockup__bar" aria-hidden="true">
<span class="mockup__dot"></span>
<span class="mockup__dot"></span>
<span class="mockup__dot"></span>
</div>
<div class="mockup__body" aria-hidden="true">
<span class="mockup__block mockup__block--lg"></span>
<span class="mockup__block mockup__block--sm"></span>
</div>
</div>
</main>@use "../../../../site/static/demo-tokens" as *;
// ── 변수 3개 ──────────────────────────────────────────
$duration: $dur-loop; // 2s — 떠오름 한 바퀴
$easing: $ease-spring;
$color: $subject-blue;
@include stage($stage-paper);
.mockup {
box-sizing: border-box;
width: clamp(220px, 62vmin, 300px);
aspect-ratio: 4 / 3;
border-radius: $r-card;
background: $subject-cream;
border: 1px solid rgba($subject-ink, .12);
box-shadow: $shadow-raised;
overflow: hidden;
}
.mockup__bar {
display: flex;
align-items: center;
gap: $sp-1;
padding: $sp-3 $sp-4;
background: rgba($subject-ink, .06);
}
.mockup__dot { width: $sp-2; height: $sp-2; border-radius: 50%; background: rgba($subject-ink, .18); }
.mockup__dot:first-child { background: $color; }
.mockup__body { display: grid; gap: $sp-3; padding: $sp-6; }
.mockup__block { display: block; height: $sp-4; border-radius: $r-xs; background: rgba($subject-ink, .1); }
.mockup__block--lg { width: 72%; }
.mockup__block--sm { width: 46%; }
// transform 만 — translateY 와 미세 rotate 로 떠 있는 느낌
.stage.is-demo .mockup { animation: float $duration $easing infinite; }
@keyframes float {
0% { transform: translateY(0) rotate(0deg); }
50% { transform: translateY(-$sp-4) rotate(-1.2deg); }
100% { transform: translateY(0) rotate(0deg); }
}
@media (prefers-reduced-motion: reduce) {
.stage.is-demo .mockup { animation: none; transform: 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);
}
.mockup {
box-sizing: border-box;
width: clamp(220px, 62vmin, 300px);
aspect-ratio: 4/3;
border-radius: 16px;
background: #fff7e6;
border: 1px solid rgba(23, 20, 26, 0.12);
box-shadow: 0 14px 24px rgba(23, 20, 26, 0.32);
overflow: hidden;
}
.mockup__bar {
display: flex;
align-items: center;
gap: 4px;
padding: 12px 16px;
background: rgba(23, 20, 26, 0.06);
}
.mockup__dot {
width: 8px;
height: 8px;
border-radius: 50%;
background: rgba(23, 20, 26, 0.18);
}
.mockup__dot:first-child {
background: #2f6df6;
}
.mockup__body {
display: grid;
gap: 12px;
padding: 24px;
}
.mockup__block {
display: block;
height: 16px;
border-radius: 4px;
background: rgba(23, 20, 26, 0.1);
}
.mockup__block--lg {
width: 72%;
}
.mockup__block--sm {
width: 46%;
}
.stage.is-demo .mockup {
animation: float 2s cubic-bezier(0.2, 0.8, 0.2, 1) infinite;
}
@keyframes float {
0% {
transform: translateY(0) rotate(0deg);
}
50% {
transform: translateY(-16px) rotate(-1.2deg);
}
100% {
transform: translateY(0) rotate(0deg);
}
}
@media (prefers-reduced-motion: reduce) {
.stage.is-demo .mockup {
animation: none;
transform: none;
}
}import type { CSSProperties } from "react";
import styles from "./MockupFloat.module.scss";
export type MockupFloatProps = {
duration?: number;
easing?: string;
color?: string;
};
type Vars = CSSProperties & { "--duration"?: string; "--easing"?: string; "--color"?: string };
export default function MockupFloat({
duration = 2000,
easing = "cubic-bezier(.2,.8,.2,1)",
color = "#2f6df6",
}: MockupFloatProps) {
const vars: Vars = { "--duration": `${duration}ms`, "--easing": easing, "--color": color };
return (
<div className={styles.mockup} style={vars}>
<div className={styles.bar} aria-hidden="true">
<span className={styles.dot} />
<span className={styles.dotMuted} />
<span className={styles.dotMuted} />
</div>
<div className={styles.body} aria-hidden="true">
<span className={`${styles.block} ${styles.blockLg}`} />
<span className={`${styles.block} ${styles.blockSm}`} />
</div>
</div>
);
}04 Mockup lơ lửng — Diện tích đo được 16.142% · khung hình chuyển động 20/23Nguyên tác GODRICH · SCSS và React nằm trong tệp zip · 9 hero section css animation — không cần JS
Bài viết chứa demo này
9 hero section css animation — không cần JSHero section là màn hình lớn đập vào mắt đầu tiên khi bạn vừa vào một trang web, nơi đặt tiêu đề cỡ đại và nút bấm dựng Xem tất cả demo trong bài này


