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 を1行追加</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 オリジナル · SCSS と React は zip に同梱 · css リセット 9選 — 基本からセーフエリアまで一気に整理
このデモが属する記事
css リセット 9選 — 基本からセーフエリアまで一気に整理9つのcss リセットパターンのうち、1つはノッチのセーフエリアに合わせて余白を再計算し、1つはモーションを減らす環境で遷移時間がほぼ0まで縮み、1つは角ばった初期の角が滑らかな曲線に変わります。 この記事のデモをすべて見る


