01Label nổi (floating label)
Label nằm chồng trong ô input rỗng bay lên góc trên trái và thu nhỏ ngay khi input được focus hoặc đã có giá trị, gộp placeholder và label làm một.
Dùng choÔ nhập biệt danh/tên trong form đăng ký, form di động hẹp
:placeholder-showntransform scale220ms

<main class="stage">
<div class="fl is-demo">
<input id="fl-nick" class="fl__input" type="text" placeholder=" " autocomplete="nickname">
<span class="fl__bloom" aria-hidden="true"></span>
<label for="fl-nick" class="fl__label">Biệt danh</label>
</div>
</main>@use "../../../../site/static/demo-tokens" as *;
// ── 변수 4개 ──────────────────────────────────────────
$duration: $dur-quick; // 라벨이 떠오르는 시간
$easing: $ease-out;
$spring: cubic-bezier(.3, 1.6, .4, 1); // 스프링 오버슛 — 라벨이 목표 스케일을 살짝 지나쳤다 돌아온다
$color: $subject-cream; // 포커스 시 라벨·테두리 색
// ── 데모 무대 ──────────────────────────────────────────
@include stage($stage-ink);
.fl {
position: relative;
width: min(240px, 82vw);
}
.fl__input {
box-sizing: border-box;
width: 100%;
border: 2px solid rgba($subject-cream, .35);
border-radius: $r-control;
padding: $sp-5 $sp-4 $sp-2;
font-weight: 500;
font-size: clamp(14px, 4vw, 16px);
line-height: 1;
font-family: inherit;
color: $subject-cream;
background: transparent;
position: relative;
transition: border-color $duration $easing;
&:focus { outline: none; border-color: $color; }
}
// 테두리 radial 글로우 확산 — 포커스 순간 input 중심에서 바깥으로 번지는 빛 한 겹
.fl__bloom {
position: absolute;
inset: -6px;
border-radius: calc(#{$r-control} + 6px);
background: radial-gradient(circle, rgba($color, .32) 0%, rgba($color, 0) 70%);
opacity: 0;
pointer-events: none;
}
.fl__input:focus ~ .fl__bloom { animation: bloom $dur-base $ease-out; }
.fl__label {
position: absolute;
left: $sp-4;
top: 50%;
transform: translateY(-50%) scale(1);
transform-origin: left center; // 규칙8: 인풋 왼쪽 붙은 라벨
color: rgba($subject-cream, .6);
font-weight: 500;
font-size: clamp(14px, 4vw, 16px);
line-height: 1;
font-family: inherit;
pointer-events: none;
transition: transform $dur-base $spring, color $duration $easing;
}
// 실사용: 포커스거나 값이 있으면(:not(:placeholder-shown)) 라벨이 떠오른다
.fl__input:focus ~ .fl__label,
.fl__input:not(:placeholder-shown) ~ .fl__label {
transform: translateY(-24px) scale(.78);
color: $color;
}
// 스프링 오버슛의 실체 — 스케일이 .78 을 지나 .72 까지 줄었다가(.625 초) 제자리로 돌아온다
@keyframes bloom {
0% { opacity: 0; transform: scale(.6); }
35% { opacity: 1; transform: scale(1); }
100% { opacity: 0; transform: scale(1.25); }
}
// ── 접근성: 움직임 줄이기 설정이면 즉시 자리만 바뀐다 ──────
@media (prefers-reduced-motion: reduce) {
.fl__label { transition: none; }
.fl__input:focus ~ .fl__bloom { 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);
}
.fl {
position: relative;
width: min(240px, 82vw);
}
.fl__input {
box-sizing: border-box;
width: 100%;
border: 2px solid rgba(255, 247, 230, 0.35);
border-radius: 12px;
padding: 20px 16px 8px;
font-weight: 500;
font-size: clamp(14px, 4vw, 16px);
line-height: 1;
font-family: inherit;
color: #fff7e6;
background: transparent;
position: relative;
transition: border-color 220ms ease-out;
}
.fl__input:focus {
outline: none;
border-color: #fff7e6;
}
.fl__bloom {
position: absolute;
inset: -6px;
border-radius: calc(12px + 6px);
background: radial-gradient(circle, rgba(255, 247, 230, 0.32) 0%, rgba(255, 247, 230, 0) 70%);
opacity: 0;
pointer-events: none;
}
.fl__input:focus ~ .fl__bloom {
animation: bloom 300ms ease-out;
}
.fl__label {
position: absolute;
left: 16px;
top: 50%;
transform: translateY(-50%) scale(1);
transform-origin: left center;
color: rgba(255, 247, 230, 0.6);
font-weight: 500;
font-size: clamp(14px, 4vw, 16px);
line-height: 1;
font-family: inherit;
pointer-events: none;
transition: transform 300ms cubic-bezier(0.3, 1.6, 0.4, 1), color 220ms ease-out;
}
.fl__input:focus ~ .fl__label,
.fl__input:not(:placeholder-shown) ~ .fl__label {
transform: translateY(-24px) scale(0.78);
color: #fff7e6;
}
@keyframes bloom {
0% {
opacity: 0;
transform: scale(0.6);
}
35% {
opacity: 1;
transform: scale(1);
}
100% {
opacity: 0;
transform: scale(1.25);
}
}
@media (prefers-reduced-motion: reduce) {
.fl__label {
transition: none;
}
.fl__input:focus ~ .fl__bloom {
animation: none;
}
}// .is-demo 자동 재생: 진짜 focus 를 오가며 :focus 규칙을 그대로 탄다 (3줄)
var el = document.querySelector('.is-demo .fl__input');
var t = function () { document.activeElement === el ? el.blur() : el.focus({ preventScroll: true }); }; // preventScroll — iframe 안 포커스가 홈 그리드의 부모 스크롤을 끌어당기지 않게 (사용자 2026-09-10 실측)
t(); setInterval(t, 400);import { useState } from "react";
import type { CSSProperties, ChangeEvent } from "react";
import styles from "./FloatingLabel.module.scss";
export type FloatingLabelProps = {
id?: string;
label?: string;
type?: string;
/** 라벨이 떠오르는 시간(ms) */
duration?: number;
easing?: string;
color?: string;
};
type Vars = CSSProperties & { "--duration"?: string; "--easing"?: string; "--color"?: string };
export default function FloatingLabel({
id = "fl-nick",
label = "Nickname",
type = "text",
duration = 220,
easing = "ease-out",
color = "#fff7e6",
}: FloatingLabelProps) {
const [value, setValue] = useState("");
const vars: Vars = { "--duration": `${duration}ms`, "--easing": easing, "--color": color };
return (
<div className={styles.fl} style={vars}>
<input
id={id}
className={styles.input}
type={type}
placeholder=" "
value={value}
onChange={(e: ChangeEvent<HTMLInputElement>) => setValue(e.target.value)}
/>
<span className={styles.bloom} aria-hidden="true" />
<label htmlFor={id} className={styles.label}>{label}</label>
</div>
);
}01 Label nổi (floating label) — Diện tích đo được 7.982% · khung hình chuyển động 8/23Nguyên tác GODRICH · SCSS và React nằm trong tệp zip · 9 CSS Input Focus Animation — dán 1 dòng là chạy
Bài viết chứa demo này
9 CSS Input Focus Animation — dán 1 dòng là chạyCSS input focus animation là các hiệu ứng dùng thẳng trạng thái trình duyệt đã có sẵn — :focus, :focus-within, :placehol Xem tất cả demo trong bài này

