05Error field breathing ring
Unlike the shake in post 052, the field itself never moves — a soft coral box-shadow ring around it breathes between opacity 0↔1 and scale(.97↔1.03) to keep signaling the error.
Best forMissing-required-field warnings, fields that stay invalid
box-shadow 브리딩opacityscale

<main class="stage">
<div class="fv5 is-demo">
<span class="fv5__field">
<input class="fv5__input" type="text" value="01021" readonly>
<span class="fv5__ring" aria-hidden="true"></span>
</span>
<p class="fv5__msg">Enter at least 4 digits</p>
</div>
</main>@use "../../../../site/static/demo-tokens" as *;
// 052편(shake-error) 은 흔들림이다. 여기서는 흔들지 않고, 필드 둘레의 글로우 링이 opacity + scale 로 천천히 숨쉬듯 반복하며 오류를 알린다
$duration: $dur-loop;
$easing: $ease-out;
$color: $error;
@include stage($stage-ink);
.fv5 { width: min(240px, 86vw); display: flex; flex-direction: column; gap: $sp-2; }
.fv5__field { position: relative; display: block; }
.fv5__ring {
position: absolute; inset: -5px; border-radius: $r-control;
box-shadow: 0 0 0 3px $error-soft; opacity: 0; transform: scale(.97); pointer-events: none;
}
.fv5__input {
position: relative; box-sizing: border-box; width: 100%;
padding: $sp-2 $sp-3; border-radius: $r-sm;
border: 2px solid $color; background: rgba(255, 255, 255, .06);
color: $subject-cream; font-weight: 400; font-size: 14px; line-height: 1; font-family: inherit;
}
.fv5__msg { margin: 0; color: $color; font-weight: 500; font-size: 11px; line-height: 1.3; font-family: inherit; }
// 한 겹(감쇠 진동): 50% 피크 뒤 단조 소멸 대신 62%·74%·86% 에 점점 약해지는 2·3차 파동 — 필드는 절대 움직이지 않는다(052 흔들림과의 차별화 유지)
@keyframes fv5-breathe {
0%, 100% { opacity: 0; transform: scale(.97); }
50% { opacity: 1; transform: scale(1.03); }
54% { opacity: .45; transform: scale(.99); }
58% { opacity: .72; transform: scale(1.022); }
62% { opacity: .3; transform: scale(.992); }
66% { opacity: .48; transform: scale(1.015); }
70% { opacity: .18; transform: scale(.995); }
74% { opacity: .28; transform: scale(1.009); }
78% { opacity: .1; transform: scale(.997); }
82% { opacity: .15; transform: scale(1.005); }
86% { opacity: .05; transform: scale(.999); }
92% { opacity: 0; transform: scale(.97); }
}
.fv5.is-demo .fv5__ring { animation: fv5-breathe $duration $easing infinite; }
@media (prefers-reduced-motion: reduce) {
.fv5.is-demo .fv5__ring { animation: none !important; opacity: .7; transform: scale(1); }
}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);
}
.fv5 {
width: min(240px, 86vw);
display: flex;
flex-direction: column;
gap: 8px;
}
.fv5__field {
position: relative;
display: block;
}
.fv5__ring {
position: absolute;
inset: -5px;
border-radius: 12px;
box-shadow: 0 0 0 3px #ffe0d8;
opacity: 0;
transform: scale(0.97);
pointer-events: none;
}
.fv5__input {
position: relative;
box-sizing: border-box;
width: 100%;
padding: 8px 12px;
border-radius: 8px;
border: 2px solid #c2361a;
background: rgba(255, 255, 255, 0.06);
color: #fff7e6;
font-weight: 400;
font-size: 14px;
line-height: 1;
font-family: inherit;
}
.fv5__msg {
margin: 0;
color: #c2361a;
font-weight: 500;
font-size: 11px;
line-height: 1.3;
font-family: inherit;
}
@keyframes fv5-breathe {
0%, 100% {
opacity: 0;
transform: scale(0.97);
}
50% {
opacity: 1;
transform: scale(1.03);
}
54% {
opacity: 0.45;
transform: scale(0.99);
}
58% {
opacity: 0.72;
transform: scale(1.022);
}
62% {
opacity: 0.3;
transform: scale(0.992);
}
66% {
opacity: 0.48;
transform: scale(1.015);
}
70% {
opacity: 0.18;
transform: scale(0.995);
}
74% {
opacity: 0.28;
transform: scale(1.009);
}
78% {
opacity: 0.1;
transform: scale(0.997);
}
82% {
opacity: 0.15;
transform: scale(1.005);
}
86% {
opacity: 0.05;
transform: scale(0.999);
}
92% {
opacity: 0;
transform: scale(0.97);
}
}
.fv5.is-demo .fv5__ring {
animation: fv5-breathe 2s ease-out infinite;
}
@media (prefers-reduced-motion: reduce) {
.fv5.is-demo .fv5__ring {
animation: none !important;
opacity: 0.7;
transform: scale(1);
}
}import type { CSSProperties } from "react";
import styles from "./ErrorBorderPulse.module.scss";
export type ErrorBorderPulseProps = { duration?: number; easing?: string; color?: string };
type Vars = CSSProperties & { "--duration"?: string; "--easing"?: string; "--color"?: string };
export default function ErrorBorderPulse({ duration = 2000, easing = "ease-out", color = "#c2361a" }: ErrorBorderPulseProps) {
const vars: Vars = { "--duration": `${duration}ms`, "--easing": easing, "--color": color };
return (
<div className={styles.fv5} style={vars}>
<span className={styles.field}>
<input className={styles.input} type="text" defaultValue="01021" readOnly />
<span className={styles.ring} aria-hidden="true" />
</span>
<p className={styles.msg}>숫자만 4자리 이상 입력해 주세요</p>
</div>
);
}05 Error field breathing ring — Measured area 2.63% · moving frames 20/23Original by GODRICH · SCSS and React ship in the zip · 9 Form Validation CSS Animations You Can Paste
The post this demo lives in
9 Form Validation CSS Animations You Can PasteForm validation CSS animations are small signals — color, icon, glow — that show a field is right or wrong, no shaking r See all demos in this post


