05Wave panel — the four corners' curve values sway in sequence with a phase offset
Each of the four corners bends its curve on its own phase value, so the panel outline moves like a wave traveling one full lap.
Best forA hero-section backdrop panel, an intro card
AMPWAVE_MS$subject-lilac

<main class="stage">
<div class="wrap is-demo" id="wrap">
<button class="panel" id="panel" type="button" aria-pressed="false" aria-label="Pause or play the four-corner wave"></button>
<p class="readout" id="readout" aria-live="polite">Phase offset 0.0</p>
</div>
</main>@use "../../../../site/static/demo-tokens" as *;
// 계산 겹은 index.html 에 있다 — 모서리 네 곳의 위상을 어긋낸 사인파 네 개를 동시에 계산한다.
@include stage($stage-ink);
.wrap {
display: flex;
flex-direction: column;
align-items: center;
gap: $sp-4;
}
.panel {
appearance: none;
border: none;
width: 200px;
height: 120px;
background: linear-gradient(135deg, $subject-sky, $subject-lilac);
cursor: pointer;
box-shadow: $shadow-raised;
&:focus-visible { outline: 2px solid $subject-blue-light; outline-offset: 3px; }
}
@supports not (corner-shape: superellipse(1)) {
.panel { border: 1px solid rgba($subject-cream, .3); }
}
.readout {
margin: 0;
color: $subject-cream;
font-size: 13px;
font-weight: 600;
font-variant-numeric: tabular-nums;
}
@media (max-width: 340px) {
.panel { width: 150px; height: 96px; }
}
@media (prefers-reduced-motion: reduce) {
.panel { transition: 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);
}
.wrap {
display: flex;
flex-direction: column;
align-items: center;
gap: 16px;
}
.panel {
appearance: none;
border: none;
width: 200px;
height: 120px;
background: linear-gradient(135deg, #6ec8ff, #b9a0ff);
cursor: pointer;
box-shadow: 0 14px 24px rgba(23, 20, 26, 0.32);
}
.panel:focus-visible {
outline: 2px solid #5b8cf8;
outline-offset: 3px;
}
@supports not (corner-shape: superellipse(1)) {
.panel {
border: 1px solid rgba(255, 247, 230, 0.3);
}
}
.readout {
margin: 0;
color: #fff7e6;
font-size: 13px;
font-weight: 600;
font-variant-numeric: tabular-nums;
}
@media (max-width: 340px) {
.panel {
width: 150px;
height: 96px;
}
}
@media (prefers-reduced-motion: reduce) {
.panel {
transition: none;
}
}(function () {
var BASE_R = 20; // 네 모서리가 공유하는 기준 Radius(px)
var AMP = 16; // 반경이 기준에서 출렁이는 폭(px)
var WAVE_MS = 2200; // 물결이 한 바퀴 도는 주기
var reduce = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
var SUPPORTS = CSS.supports('corner-shape', 'superellipse(1)');
var wrap = document.getElementById('wrap');
var panel = document.getElementById('panel');
var readout = document.getElementById('readout');
var isDemo = true;
var paused = false;
var frozenAt = 0;
var CORNERS = ['TopLeft', 'TopRight', 'BottomRight', 'BottomLeft'];
function human() { if (isDemo) { wrap.classList.remove('is-demo'); isDemo = false; } }
// 계산 겹: 모서리마다 위상을 PI/2 씩 어긋낸 사인파 4개를 동시에 계산해 물결처럼 번갈아 부풀린다
function corners(t) {
var out = [];
for (var i = 0; i < 4; i++) {
var phase = i * (Math.PI / 2);
var r = BASE_R + AMP * Math.sin(t * Math.PI * 2 + phase);
out.push(Math.max(2, r));
}
return out;
}
function applyCorners(rs) {
for (var i = 0; i < 4; i++) {
if (SUPPORTS) panel.style.cornerShape = 'superellipse(2.2)';
panel.style['border' + CORNERS[i] + 'Radius'] = rs[i].toFixed(2) + 'px';
}
readout.textContent = 'Phase offset ' + (rs[0] - rs[2]).toFixed(1);
}
panel.addEventListener('click', function () {
human();
paused = !paused;
panel.setAttribute('aria-pressed', String(paused));
if (paused) frozenAt = performance.now();
});
panel.addEventListener('keydown', function (e) {
if (e.key === ' ' || e.key === 'Enter') { e.preventDefault(); panel.click(); }
});
var start = 0;
function loop(ts) {
if (!start) start = ts;
var t;
if (reduce) {
t = 0.25;
} else if (paused) {
t = ((frozenAt - start) % WAVE_MS) / WAVE_MS;
} else {
t = ((ts - start) % WAVE_MS) / WAVE_MS;
}
applyCorners(corners(t));
requestAnimationFrame(loop);
}
requestAnimationFrame(loop);
})();// WavePanel — vanilla 판(run/372/demo/05-corner-shape-wave-panel/index.html) 의 React 포트.
// 모서리 네 곳의 위상을 어긋낸 사인파 네 개를 동시에 계산해 물결처럼 번갈아 부풀린다.
import { useEffect, useRef } from "react";
import styles from "./WavePanel.module.scss";
type Box<T> = { current: T };
const BASE_R = 20; // 네 모서리가 공유하는 기준 반경(px)
const AMP = 16; // 반경이 기준에서 출렁이는 폭(px)
const WAVE_MS = 2200; // 물결이 한 바퀴 도는 주기
const CORNERS = ["TopLeft", "TopRight", "BottomRight", "BottomLeft"] as const;
export default function WavePanel() {
const rootRef = useRef(null) as unknown as Box<HTMLDivElement>;
const panelRef = useRef(null) as unknown as Box<HTMLButtonElement>;
const readoutRef = useRef(null) as unknown as Box<HTMLParagraphElement>;
const rafRef = useRef(0) as unknown as Box<number>;
useEffect(() => {
const reduce = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
const SUPPORTS = CSS.supports("corner-shape", "superellipse(1)");
const root = rootRef.current;
const panel = panelRef.current;
const readout = readoutRef.current;
if (!root || !panel || !readout) return;
let isDemo = true;
let paused = false;
let frozenAt = 0;
function human() { if (isDemo) { root!.classList.remove(styles.isDemo); isDemo = false; } }
// 계산 겹: 모서리마다 위상을 PI/2 씩 어긋낸 사인파 4개를 동시에 계산해 물결처럼 번갈아 부풀린다
function corners(t: number) {
const out: number[] = [];
for (let i = 0; i < 4; i++) {
const phase = i * (Math.PI / 2);
const r = BASE_R + AMP * Math.sin(t * Math.PI * 2 + phase);
out.push(Math.max(2, r));
}
return out;
}
function applyCorners(rs: number[]) {
for (let i = 0; i < 4; i++) {
if (SUPPORTS) (panel!.style as any).cornerShape = "superellipse(2.2)";
(panel!.style as any)["border" + CORNERS[i] + "Radius"] = rs[i].toFixed(2) + "px";
}
readout!.textContent = "위상 어긋남 " + (rs[0] - rs[2]).toFixed(1);
}
function onClick() {
human();
paused = !paused;
panel!.setAttribute("aria-pressed", String(paused));
if (paused) frozenAt = performance.now();
}
function onKeyDown(e: KeyboardEvent) {
if (e.key === " " || e.key === "Enter") { e.preventDefault(); onClick(); }
}
panel.addEventListener("click", onClick);
panel.addEventListener("keydown", onKeyDown);
let start = 0;
function loop(ts: number) {
if (!start) start = ts;
let t: number;
if (reduce) {
t = 0.25;
} else if (paused) {
t = ((frozenAt - start) % WAVE_MS) / WAVE_MS;
} else {
t = ((ts - start) % WAVE_MS) / WAVE_MS;
}
applyCorners(corners(t));
rafRef.current = requestAnimationFrame(loop);
}
rafRef.current = requestAnimationFrame(loop);
return () => {
panel.removeEventListener("click", onClick);
panel.removeEventListener("keydown", onKeyDown);
cancelAnimationFrame(rafRef.current);
};
}, []);
return (
<main className={styles.stage}>
<div className={styles.wrap + " " + styles.isDemo} ref={rootRef}>
<button
className={styles.panel}
ref={panelRef}
type="button"
aria-pressed="false"
aria-label="네 모서리 물결 정지·재생"
/>
<p className={styles.readout} ref={readoutRef} aria-live="polite">위상 어긋남 0.0</p>
</div>
</main>
);
}05 Wave panel — the four corners' curve values sway in sequence with a phase offset — Measured area 1.01% · moving frames 23/23Original by GODRICH · SCSS and React ship in the zip · CSS corner-shape: 9 Corner Cutting Patterns
The post this demo lives in
CSS corner-shape: 9 Corner Cutting Patternscss corner-shape, live. Nine corner-shape patterns: one sweeps a superellipse exponent from circle to square, one interp See all demos in this post
If you like this effect - similar demos
base09 Rounded corner comparison — a card placing a circular-arc corner next to a smooth squircle cornerCSS Reset: 9 Patterns From Basics to Safe-Area
base01 System font stack comparerCSS Typography Playground: 9 Demos, No Webfonts
Dashboard UI06 Parallel-Coordinates Chart Linking Values Across Axes With Crossing Lines9 Data Visualization Chart Types With Real Math