09Inset frame — an outer curve value and an inner curve value sit apart, forming a double border
When an outer and an inner element use different curve values, the gap between the two outlines varies along the corner curve, forming a flowing double border.
Best forA profile-card border, a verified-mark frame
OUTER_RINSET_MAX$subject-mint

<main class="stage">
<div class="wrap is-demo" id="wrap">
<button class="frame" id="frame" type="button" aria-pressed="false" aria-label="Push or pull the inner curve">
<span class="inner" id="inner">Core</span>
</button>
<p class="readout" id="readout" aria-live="polite">Inset 10.0</p>
</div>
</main>@use "../../../../site/static/demo-tokens" as *;
// 계산 겹은 index.html 에 있다 — 인셋 산술(안쪽 반경 = 바깥 반경 − 인셋)과 관성 복귀.
@include stage($stage-yellow);
.wrap {
display: flex;
flex-direction: column;
align-items: center;
gap: $sp-4;
}
.frame {
appearance: none;
border: none;
width: 200px;
height: 132px;
padding: $sp-4;
background: $subject-blue-deep;
corner-shape: superellipse(3);
border-radius: $r-card;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
box-shadow: $shadow-raised;
&:focus-visible { outline: 2px solid $subject-ink; outline-offset: 3px; }
}
.inner {
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
background: $subject-mint;
corner-shape: superellipse(3);
border-radius: $r-pill;
color: $subject-ink;
font-size: 15px;
font-weight: 800;
pointer-events: none;
}
@supports not (corner-shape: superellipse(1)) {
.frame { border: 1px solid rgba($subject-ink, .2); }
.inner { border: 1px solid rgba($subject-ink, .2); }
}
.readout {
margin: 0;
color: $subject-ink;
font-size: 13px;
font-weight: 600;
font-variant-numeric: tabular-nums;
}
@media (max-width: 340px) {
.frame { width: 156px; height: 106px; }
.inner { font-size: 13px; }
}
@media (prefers-reduced-motion: reduce) {
.frame { transition: none; }
}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);
}
.wrap {
display: flex;
flex-direction: column;
align-items: center;
gap: 16px;
}
.frame {
appearance: none;
border: none;
width: 200px;
height: 132px;
padding: 16px;
background: #0a4fe8;
corner-shape: superellipse(3);
border-radius: 16px;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 14px 24px rgba(23, 20, 26, 0.32);
}
.frame:focus-visible {
outline: 2px solid #17141a;
outline-offset: 3px;
}
.inner {
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
background: #4cd4a6;
corner-shape: superellipse(3);
border-radius: 999px;
color: #17141a;
font-size: 15px;
font-weight: 800;
pointer-events: none;
}
@supports not (corner-shape: superellipse(1)) {
.frame {
border: 1px solid rgba(23, 20, 26, 0.2);
}
.inner {
border: 1px solid rgba(23, 20, 26, 0.2);
}
}
.readout {
margin: 0;
color: #17141a;
font-size: 13px;
font-weight: 600;
font-variant-numeric: tabular-nums;
}
@media (max-width: 340px) {
.frame {
width: 156px;
height: 106px;
}
.inner {
font-size: 13px;
}
}
@media (prefers-reduced-motion: reduce) {
.frame {
transition: none;
}
}(function () {
var OUTER_R = 40; // 바깥 프레임의 고정 Radius(px)
var INSET_MIN = 6; // 안쪽 곡선이 프레임에서 떨어지는 최소 거리(px)
var INSET_MAX = 22; // 최대 거리(px) — 커질수록 안쪽 곡선이 더 안으로 밀려든다
var SWEEP_MS = 2400; // 인셋이 한 번 왕복하는 주기
var reduce = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
var SUPPORTS = CSS.supports('corner-shape', 'superellipse(1)');
var wrap = document.getElementById('wrap');
var frame = document.getElementById('frame');
var inner = document.getElementById('inner');
var readout = document.getElementById('readout');
var isDemo = true;
var held = false;
var heldStart = 0;
function human() { if (isDemo) { wrap.classList.remove('is-demo'); isDemo = false; } }
// 계산 겹: Inset 산술 — 안쪽 Radius = 바깥 Radius − Inset. 인셋이 커질수록 안쪽 곡선이 더 깎인다
function innerRadius(inset) { return Math.max(2, OUTER_R - inset); }
function applyInset(inset) {
var r = innerRadius(inset);
if (SUPPORTS) {
frame.style.cornerShape = 'superellipse(3)';
inner.style.cornerShape = 'superellipse(3)';
}
frame.style.borderRadius = OUTER_R + 'px';
inner.style.borderRadius = r.toFixed(2) + 'px';
readout.textContent = 'Inset ' + inset.toFixed(1);
}
frame.addEventListener('pointerdown', function () { human(); held = true; heldStart = performance.now(); frame.setAttribute('aria-pressed', 'true'); });
frame.addEventListener('pointerup', function () { human(); held = false; heldStart = performance.now(); frame.setAttribute('aria-pressed', 'false'); });
frame.addEventListener('pointerleave', function () { if (held) { held = false; heldStart = performance.now(); frame.setAttribute('aria-pressed', 'false'); } });
frame.addEventListener('keydown', function (e) { if (e.key === ' ' || e.key === 'Enter') { e.preventDefault(); frame.dispatchEvent(new PointerEvent('pointerdown')); } });
frame.addEventListener('keyup', function (e) { if (e.key === ' ' || e.key === 'Enter') { e.preventDefault(); frame.dispatchEvent(new PointerEvent('pointerup')); } });
// 계산 겹: 뗄 때 인셋이 목표로 관성(지수 감쇠)으로 되돌아간다
var INSET_SPRING_K = 8;
var insetNow = INSET_MIN;
function stepInset(ts) {
var target = held ? INSET_MAX : INSET_MIN;
var dt = (ts - (stepInset.last || ts)) / 1000;
stepInset.last = ts;
var follow = 1 - Math.exp(-INSET_SPRING_K * dt);
insetNow += (target - insetNow) * follow;
if (Math.abs(insetNow - target) < 0.01) insetNow = target;
}
var start = 0;
function loop(ts) {
if (!start) start = ts;
if (isDemo) {
if (reduce) {
insetNow = INSET_MAX;
} else {
var t = ((ts - start) % SWEEP_MS) / SWEEP_MS;
var tri = t < 0.5 ? t * 2 : (1 - t) * 2;
insetNow = INSET_MIN + (INSET_MAX - INSET_MIN) * tri;
}
} else if (!reduce) {
stepInset(ts);
}
applyInset(insetNow);
requestAnimationFrame(loop);
}
requestAnimationFrame(loop);
})();// FrameInset — vanilla 판(run/372/demo/09-corner-shape-frame-inset/index.html) 의 React 포트.
// 인셋 산술(안쪽 반경 = 바깥 반경 − 인셋)과, 뗄 때 목표로 지수 감쇠 관성 복귀.
import { useEffect, useRef } from "react";
import styles from "./FrameInset.module.scss";
type Box<T> = { current: T };
const OUTER_R = 40; // 바깥 프레임의 고정 반경(px)
const INSET_MIN = 6; // 안쪽 곡선이 프레임에서 떨어지는 최소 거리(px)
const INSET_MAX = 22; // 최대 거리(px) — 커질수록 안쪽 곡선이 더 안으로 밀려든다
const SWEEP_MS = 2400; // 인셋이 한 번 왕복하는 주기
const INSET_SPRING_K = 8; // 뗄 때 목표로 되돌아가는 관성 속도 상수
export default function FrameInset() {
const rootRef = useRef(null) as unknown as Box<HTMLDivElement>;
const frameRef = useRef(null) as unknown as Box<HTMLButtonElement>;
const innerRef = useRef(null) as unknown as Box<HTMLSpanElement>;
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 frame = frameRef.current;
const inner = innerRef.current;
const readout = readoutRef.current;
if (!root || !frame || !inner || !readout) return;
let isDemo = true;
let held = false;
let insetNow = INSET_MIN;
let lastStepTs = 0;
function human() { if (isDemo) { root!.classList.remove(styles.isDemo); isDemo = false; } }
// 계산 겹: 인셋 산술 — 안쪽 반경 = 바깥 반경 − 인셋. 인셋이 커질수록 안쪽 곡선이 더 깎인다
function innerRadius(inset: number) { return Math.max(2, OUTER_R - inset); }
function applyInset(inset: number) {
const r = innerRadius(inset);
if (SUPPORTS) {
(frame!.style as any).cornerShape = "superellipse(3)";
(inner!.style as any).cornerShape = "superellipse(3)";
}
frame!.style.borderRadius = OUTER_R + "px";
inner!.style.borderRadius = r.toFixed(2) + "px";
readout!.textContent = "인셋 " + inset.toFixed(1);
}
function onPointerDown() { human(); held = true; frame!.setAttribute("aria-pressed", "true"); }
function onPointerUp() { human(); held = false; frame!.setAttribute("aria-pressed", "false"); }
function onPointerLeave() { if (held) { held = false; frame!.setAttribute("aria-pressed", "false"); } }
function onKeyDown(e: KeyboardEvent) { if (e.key === " " || e.key === "Enter") { e.preventDefault(); onPointerDown(); } }
function onKeyUp(e: KeyboardEvent) { if (e.key === " " || e.key === "Enter") { e.preventDefault(); onPointerUp(); } }
frame.addEventListener("pointerdown", onPointerDown);
frame.addEventListener("pointerup", onPointerUp);
frame.addEventListener("pointerleave", onPointerLeave);
frame.addEventListener("keydown", onKeyDown);
frame.addEventListener("keyup", onKeyUp);
// 계산 겹: 뗄 때 인셋이 목표로 관성(지수 감쇠)으로 되돌아간다
function stepInset(ts: number) {
const target = held ? INSET_MAX : INSET_MIN;
const dt = (ts - (lastStepTs || ts)) / 1000;
lastStepTs = ts;
const follow = 1 - Math.exp(-INSET_SPRING_K * dt);
insetNow += (target - insetNow) * follow;
if (Math.abs(insetNow - target) < 0.01) insetNow = target;
}
let start = 0;
function loop(ts: number) {
if (!start) start = ts;
if (isDemo) {
if (reduce) {
insetNow = INSET_MAX;
} else {
const t = ((ts - start) % SWEEP_MS) / SWEEP_MS;
const tri = t < 0.5 ? t * 2 : (1 - t) * 2;
insetNow = INSET_MIN + (INSET_MAX - INSET_MIN) * tri;
}
} else if (!reduce) {
stepInset(ts);
}
applyInset(insetNow);
rafRef.current = requestAnimationFrame(loop);
}
rafRef.current = requestAnimationFrame(loop);
return () => {
frame.removeEventListener("pointerdown", onPointerDown);
frame.removeEventListener("pointerup", onPointerUp);
frame.removeEventListener("pointerleave", onPointerLeave);
frame.removeEventListener("keydown", onKeyDown);
frame.removeEventListener("keyup", onKeyUp);
cancelAnimationFrame(rafRef.current);
};
}, []);
return (
<main className={styles.stage}>
<div className={styles.wrap + " " + styles.isDemo} ref={rootRef}>
<button
className={styles.frame}
ref={frameRef}
type="button"
aria-pressed="false"
aria-label="안쪽 곡선 밀기/당기기"
>
<span className={styles.inner} ref={innerRef}>코어</span>
</button>
<p className={styles.readout} ref={readoutRef} aria-live="polite">인셋 10.0</p>
</div>
</main>
);
}09 Inset frame — an outer curve value and an inner curve value sit apart, forming a double border — Measured area 0.428% · 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
base02 Box model comparison — a card that shows whether border and padding count toward its sizeCSS Reset: 9 Patterns From Basics to Safe-Area
base03 Tracking and leading tunerCSS Typography Playground: 9 Demos, No Webfonts
Dashboard UI07 A Sunburst That Splits Into Inner and Outer Rings and Expands a Branch on ClickPie Chart CSS: 9 Slices That Pop, Tilt and Morph