09둥근 모서리 형태 비교 — 원호 모서리와 매끈한 곡선 모서리를 나란히 놓고 보는 카드
같은 반지름 값을 줘도 원호로 급하게 꺾이는 기본 모서리와, 곡률이 서서히 변하는 매끈한 모서리가 나란히 놓여 차이가 눈에 바로 보입니다.
쓰임브랜드 카드 시안, 아이콘 배경 프레임
corner-shape@supportsCSS.supports

<main class="stage">
<div class="wrap is-demo" id="wrap">
<span class="supportBadge" id="supportBadge">확인 중</span>
<div class="cards">
<div class="cardCol">
<span class="colLabel">border-radius 기본 원호</span>
<div class="card cardRadius" id="cardRadius"></div>
</div>
<div class="cardCol">
<span class="colLabel">corner-shape 한 줄 추가</span>
<div class="card cardShape" id="cardShape"></div>
</div>
</div>
<div class="readout" id="readout">반경 —px</div>
</div>
</main>@use "../../../../site/static/demo-tokens" as *;
// 계산 겹은 index.html 의 apply() 에 있다 — 클릭·자동 루프가 같은 반경 값을 두 카드에 동시에
// 넣고 getComputedStyle 로 실제 렌더 결과를 다시 읽는다. corner-shape 는 여기 CSS 한 줄로만
// 오른쪽 카드에 더해진다 — @supports 안에 있어 미지원 환경은 자동으로 원호 폴백이다.
@include stage($stage-ink);
.wrap {
width: 300px;
display: flex;
flex-direction: column;
gap: $sp-3;
align-items: center;
}
.supportBadge {
align-self: flex-start;
color: $subject-cream;
font-size: 10px;
font-weight: 600;
background: rgba($subject-cream, .12);
border-radius: $r-xs;
padding: $sp-1 $sp-2;
&.fallback { background: rgba($subject-orange-deep, .5); }
}
.cards {
display: flex;
gap: $sp-3;
}
.cardCol {
display: flex;
flex-direction: column;
align-items: center;
gap: $sp-2;
}
.colLabel {
color: $subject-cream;
font-size: 10px;
font-weight: 600;
text-align: center;
width: 120px;
}
.card {
width: 100px;
height: 100px;
background: $subject-orange-deep;
cursor: pointer;
transition: border-radius $dur-base $ease-out;
}
// 주인공 — border-radius 는 두 카드에 똑같이 JS 로 들어간다. corner-shape 만 오른쪽에 한 줄 더 얹는다.
@supports (corner-shape: squircle) {
.cardShape {
corner-shape: squircle;
}
}
.readout {
color: $subject-cream;
font-size: 11px;
}
@media (max-width: 340px) {
.wrap { width: 260px; gap: $sp-2; }
.cards { gap: $sp-2; }
.card { width: 72px; height: 72px; }
.colLabel { width: 90px; }
}
@media (prefers-reduced-motion: reduce) {
// reduce: 자동 루프·클릭 전환의 완만함만 지운다(반경 값 자체는 그대로 바뀐다)
.card { 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 {
width: 300px;
display: flex;
flex-direction: column;
gap: 12px;
align-items: center;
}
.supportBadge {
align-self: flex-start;
color: #fff7e6;
font-size: 10px;
font-weight: 600;
background: rgba(255, 247, 230, 0.12);
border-radius: 4px;
padding: 4px 8px;
}
.supportBadge.fallback {
background: rgba(201, 48, 0, 0.5);
}
.cards {
display: flex;
gap: 12px;
}
.cardCol {
display: flex;
flex-direction: column;
align-items: center;
gap: 8px;
}
.colLabel {
color: #fff7e6;
font-size: 10px;
font-weight: 600;
text-align: center;
width: 120px;
}
.card {
width: 100px;
height: 100px;
background: #c93000;
cursor: pointer;
transition: border-radius 300ms ease-out;
}
@supports (corner-shape: squircle) {
.cardShape {
corner-shape: squircle;
}
}
.readout {
color: #fff7e6;
font-size: 11px;
}
@media (max-width: 340px) {
.wrap {
width: 260px;
gap: 8px;
}
.cards {
gap: 8px;
}
.card {
width: 72px;
height: 72px;
}
.colLabel {
width: 90px;
}
}
@media (prefers-reduced-motion: reduce) {
.card {
transition: none;
}
}(function () {
var STEP_MS = 900;
var MIN_R = 10;
var MAX_R = 34;
var reduce = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
// corner-shape 는 border-radius 옆에 한 줄만 더하면 되는 값이라 실제로 그렇게 확인한다(추측 금지)
var supported = typeof CSS !== 'undefined' && CSS.supports && CSS.supports('corner-shape', 'squircle');
var wrap = document.getElementById('wrap');
var supportBadge = document.getElementById('supportBadge');
var cardRadius = document.getElementById('cardRadius');
var cardShape = document.getElementById('cardShape');
var readout = document.getElementById('readout');
var isDemo = true;
var r = MIN_R;
supportBadge.textContent = supported ? 'corner-shape 지원' : '미지원 환경 — 오른쪽은 원호로 자동 대체';
supportBadge.classList.toggle('fallback', !supported);
function apply() {
var px = Math.round(r) + 'px';
cardRadius.style.borderRadius = px;
cardShape.style.borderRadius = px;
// 실측 — 실제로 렌더된 border-radius 를 getComputedStyle 로 다시 읽는다
var rendered = getComputedStyle(cardRadius).borderTopLeftRadius;
readout.textContent = '반경 ' + rendered + ' · corner-shape ' + (supported ? '적용됨' : '미지원(폴백)');
}
function human() { if (isDemo) { wrap.classList.remove('is-demo'); isDemo = false; } }
cardRadius.addEventListener('click', function () { human(); r = r === MIN_R ? MAX_R : MIN_R; apply(); });
cardShape.addEventListener('click', function () { human(); r = r === MIN_R ? MAX_R : MIN_R; apply(); });
var last = 0;
function loop(ts) {
if (!last) last = ts;
if (isDemo && !reduce && ts - last >= STEP_MS) {
last = ts;
r = r === MIN_R ? MAX_R : MIN_R;
apply();
}
requestAnimationFrame(loop);
}
apply();
requestAnimationFrame(loop);
})();// CornerShape — vanilla 판(run/376/demo/09-corner-shape/index.html) 의 React 포트.
// 클릭·자동 루프가 같은 반경 값을 두 카드에 동시에 넣고 getComputedStyle 로 실제 렌더 결과를
// 다시 읽는다. corner-shape 지원 여부는 CSS.supports 로 실측하고(추측 금지), 미지원 환경은
// @supports 블록이 자동으로 원호 폴백을 남긴다.
import { useEffect, useRef } from "react";
import styles from "./CornerShape.module.scss";
type Box<T> = { current: T };
const STEP_MS = 900;
const MIN_R = 10;
const MAX_R = 34;
export default function CornerShape() {
const wrapRef = useRef(null) as unknown as Box<HTMLDivElement>;
const supportBadgeRef = useRef(null) as unknown as Box<HTMLSpanElement>;
const cardRadiusRef = useRef(null) as unknown as Box<HTMLDivElement>;
const cardShapeRef = useRef(null) as unknown as Box<HTMLDivElement>;
const readoutRef = useRef(null) as unknown as Box<HTMLDivElement>;
const rafRef = useRef(0) as unknown as Box<number>;
useEffect(() => {
const reduce = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
const wrap = wrapRef.current;
const supportBadge = supportBadgeRef.current;
const cardRadius = cardRadiusRef.current;
const cardShape = cardShapeRef.current;
const readout = readoutRef.current;
if (!wrap || !supportBadge || !cardRadius || !cardShape || !readout) return;
const supported =
typeof CSS !== "undefined" && !!CSS.supports && CSS.supports("corner-shape", "squircle");
let isDemo = true;
let r = MIN_R;
supportBadge.textContent = supported ? "corner-shape 지원" : "미지원 환경 — 오른쪽은 원호로 자동 대체";
supportBadge.classList.toggle(styles.fallback, !supported);
function apply() {
const px = Math.round(r) + "px";
cardRadius!.style.borderRadius = px;
cardShape!.style.borderRadius = px;
const rendered = getComputedStyle(cardRadius!).borderTopLeftRadius;
readout!.textContent = "반경 " + rendered + " · corner-shape " + (supported ? "적용됨" : "미지원(폴백)");
}
function human() { if (isDemo) { wrap!.classList.remove(styles.isDemo); isDemo = false; } }
function onCardClick() { human(); r = r === MIN_R ? MAX_R : MIN_R; apply(); }
cardRadius.addEventListener("click", onCardClick);
cardShape.addEventListener("click", onCardClick);
let last = 0;
function loop(ts: number) {
if (!last) last = ts;
if (isDemo && !reduce && ts - last >= STEP_MS) {
last = ts;
r = r === MIN_R ? MAX_R : MIN_R;
apply();
}
rafRef.current = requestAnimationFrame(loop);
}
apply();
rafRef.current = requestAnimationFrame(loop);
return () => {
cardRadius!.removeEventListener("click", onCardClick);
cardShape!.removeEventListener("click", onCardClick);
cancelAnimationFrame(rafRef.current);
};
}, []);
return (
<main className={styles.stage}>
<div className={styles.wrap + " " + styles.isDemo} ref={wrapRef}>
<span className={styles.supportBadge} ref={supportBadgeRef}>확인 중</span>
<div className={styles.cards}>
<div className={styles.cardCol}>
<span className={styles.colLabel}>border-radius 기본 원호</span>
<div className={styles.card + " " + styles.cardRadius} ref={cardRadiusRef}></div>
</div>
<div className={styles.cardCol}>
<span className={styles.colLabel}>corner-shape 한 줄 추가</span>
<div className={styles.card + " " + styles.cardShape} ref={cardShapeRef}></div>
</div>
</div>
<div className={styles.readout} ref={readoutRef}>반경 —px</div>
</div>
</main>
);
}09 둥근 모서리 형태 비교 — 원호 모서리와 매끈한 곡선 모서리를 나란히 놓고 보는 카드 — 측정 누적면적 1.044% · 움직인 프레임 9/23GODRICH 원작 · zip 에 SCSS·React 포함 · css 초기화 9종 — 리셋부터 안전 영역까지 한 번에 정리
이 데모가 속한 글
css 초기화 9종 — 리셋부터 안전 영역까지 한 번에 정리아홉 개의 css 초기화 패턴 중 하나는 화면 노치를 감지해 안전 영역만큼 여백을 다시 계산하고, 하나는 모션을 줄이는 환경에서 전환 시간이 거의 0에 가깝게 줄어들고, 하나는 각진 기본 모서리가 매끈한 곡선 모양으 이 글의 데모 전부 보기


