04多角形の頂点クリップ
六つの頂点をパーセント座標で動かすと、その場で画像が切り抜かれます。座標の並びがそのまま clip-path のコードになります。
使いどころ斜めに切れたヒーロー画像、クーポン・タグ型のカード
clip-pathpolygonpointerdown

<main class="stage">
<div class="gen is-demo" id="gen">
<div class="gen__top">
<div class="gen__preview-frame" aria-hidden="true"><div class="gen__preview"></div></div>
<div class="gen__grid">
<label class="gen__row">
<span class="gen__lab">頂点1</span>
<span class="gen__slot">
<input class="gen__rng" type="range" min="20" max="100" step="5" value="90" data-var="--v0" aria-label="一つ目の頂点の半径">
<span class="gen__thumb" aria-hidden="true"></span>
</span>
</label>
<label class="gen__row">
<span class="gen__lab">頂点2</span>
<span class="gen__slot">
<input class="gen__rng" type="range" min="20" max="100" step="5" value="60" data-var="--v1" aria-label="二つ目の頂点の半径">
<span class="gen__thumb" aria-hidden="true"></span>
</span>
</label>
<label class="gen__row">
<span class="gen__lab">頂点3</span>
<span class="gen__slot">
<input class="gen__rng" type="range" min="20" max="100" step="5" value="80" data-var="--v2" aria-label="三つ目の頂点の半径">
<span class="gen__thumb" aria-hidden="true"></span>
</span>
</label>
<label class="gen__row">
<span class="gen__lab">頂点4</span>
<span class="gen__slot">
<input class="gen__rng" type="range" min="20" max="100" step="5" value="60" data-var="--v3" aria-label="四つ目の頂点の半径">
<span class="gen__thumb" aria-hidden="true"></span>
</span>
</label>
<label class="gen__row">
<span class="gen__lab">頂点5</span>
<span class="gen__slot">
<input class="gen__rng" type="range" min="20" max="100" step="5" value="85" data-var="--v4" aria-label="五つ目の頂点の半径">
<span class="gen__thumb" aria-hidden="true"></span>
</span>
</label>
<label class="gen__row">
<span class="gen__lab">頂点6</span>
<span class="gen__slot">
<input class="gen__rng" type="range" min="20" max="100" step="5" value="65" data-var="--v5" aria-label="六つ目の頂点の半径">
<span class="gen__thumb" aria-hidden="true"></span>
</span>
</label>
</div>
</div>
<div class="gen__out">
<code class="gen__code"><span class="gen__prop">clip-path:</span> <span class="gen__v gen__v--c0"></span>% <span class="gen__v gen__v--c1"></span>% <span class="gen__v gen__v--c2"></span>% <span class="gen__v gen__v--c3"></span>% <span class="gen__v gen__v--c4"></span>% <span class="gen__v gen__v--c5"></span>%</code>
<button class="gen__copy" type="button" id="copy">コピー</button>
</div>
</div>
</main>@use "../../../../site/static/demo-tokens" as *;
// 다각형 꼭짓점 자르기 — 여섯 꼭짓점이 60도 간격 고정이고 각 꼭짓점의 반경만 속성 여섯 개로 조절된다.
// 미리보기의 clip-path 를 **CSS calc 로 직접** 계산한다(아래 계수표) — JS 의 clipText() 도 같은 계수표로
// 복사용 문자열을 만든다. 두 판이 같은 표를 공유하므로 화면 모양과 복사한 코드가 어긋나지 않는다.
// 세로 계수 0.6 은 미리보기 상자 비율(124×76)로 납작하게 누르는 값이다.
$duration: $dur-loop;
$easing: $ease-spring;
$color: $subject-ink;
$mono: ui-monospace, SFMono-Regular, "SF Mono", Consolas, "Liberation Mono", monospace;
$line: rgba(23, 20, 26, .14);
$track: rgba(23, 20, 26, .18);
$rail: 4px;
$thumb: 12px;
@property --v0 { syntax: "<integer>"; inherits: true; initial-value: 90; }
@property --v1 { syntax: "<integer>"; inherits: true; initial-value: 60; }
@property --v2 { syntax: "<integer>"; inherits: true; initial-value: 80; }
@property --v3 { syntax: "<integer>"; inherits: true; initial-value: 60; }
@property --v4 { syntax: "<integer>"; inherits: true; initial-value: 85; }
@property --v5 { syntax: "<integer>"; inherits: true; initial-value: 65; }
@include stage($stage-paper);
.gen {
min-width: 0; // stage grid item 의 자동 최소폭(min-content) 을 끊는다 — BD9
--v0: 90;
--v1: 60;
--v2: 80;
--v3: 60;
--v4: 85;
--v5: 65;
display: flex; flex-direction: column; gap: $sp-3;
box-sizing: border-box; width: min(100%, 400px);
padding: $sp-3; border: 1px solid $line; border-radius: $r-card;
background: rgba(23, 20, 26, .04);
}
.gen__top { display: flex; align-items: center; gap: $sp-3; }
// ── 미리보기 — 계수표: (dx, dy) = 위(0,-1) · 오위(0.87,-0.5) · 오아(0.87,0.5) · 아래(0,1) · 왼아(-0.87,0.5) · 왼위(-0.87,-0.5)
.gen__preview-frame { flex: 0 0 auto; padding: 4px; border: 1px dashed rgba(23, 20, 26, .3); border-radius: $r-xs; }
.gen__preview {
box-sizing: border-box; width: 116px; height: 72px;
background: $subject-blue;
clip-path: polygon(
calc(50% + 0 * var(--v0) * 1%) calc(50% + -1 * var(--v0) * 0.6 * 1%),
calc(50% + 0.87 * var(--v1) * 1%) calc(50% + -0.5 * var(--v1) * 0.6 * 1%),
calc(50% + 0.87 * var(--v2) * 1%) calc(50% + 0.5 * var(--v2) * 0.6 * 1%),
calc(50% + 0 * var(--v3) * 1%) calc(50% + 1 * var(--v3) * 0.6 * 1%),
calc(50% + -0.87 * var(--v4) * 1%) calc(50% + 0.5 * var(--v4) * 0.6 * 1%),
calc(50% + -0.87 * var(--v5) * 1%) calc(50% + -0.5 * var(--v5) * 0.6 * 1%)
);
}
.gen__grid { flex: 1 1 auto; min-width: 0; display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: $sp-1 $sp-2; }
.gen__row { display: flex; align-items: center; gap: $sp-1; cursor: pointer; }
.gen__lab {
flex: 0 0 auto; width: 38px;
color: $subject-ink;
font-weight: 700; font-size: 10px; line-height: 1.4;
}
.gen__slot {
position: relative; flex: 1 1 auto; min-width: 0;
display: grid; align-items: center; height: 16px;
}
.gen__slot::before {
content: ""; grid-area: 1 / 1;
height: $rail; border-radius: $r-pill; background: $track;
}
.gen__rng {
grid-area: 1 / 1; appearance: none; -webkit-appearance: none;
width: 100%; min-width: 0; height: 16px; margin: 0; background: transparent;
cursor: pointer;
}
.gen__rng::-webkit-slider-thumb {
-webkit-appearance: none; appearance: none;
width: $thumb; height: $thumb; border: 0; border-radius: 50%;
background: transparent;
}
.gen__rng::-moz-range-thumb {
width: $thumb; height: $thumb; border: 0; border-radius: 50%;
background: transparent;
}
.gen__rng:focus-visible { outline: 2px solid $color; outline-offset: 2px; border-radius: $r-pill; }
.gen__thumb {
position: absolute; top: 50%; left: calc(var(--pct) * 1%);
width: $thumb; height: $thumb; margin-top: -($thumb * .5); margin-left: -($thumb * .5);
border-radius: 50%; background: $color;
pointer-events: none;
}
.gen__row:nth-child(1) .gen__thumb { --pct: calc((var(--v0) - 20) / 80 * 100); }
.gen__row:nth-child(2) .gen__thumb { --pct: calc((var(--v1) - 20) / 80 * 100); }
.gen__row:nth-child(3) .gen__thumb { --pct: calc((var(--v2) - 20) / 80 * 100); }
.gen__row:nth-child(4) .gen__thumb { --pct: calc((var(--v3) - 20) / 80 * 100); }
.gen__row:nth-child(5) .gen__thumb { --pct: calc((var(--v4) - 20) / 80 * 100); }
.gen__row:nth-child(6) .gen__thumb { --pct: calc((var(--v5) - 20) / 80 * 100); }
.gen__out { display: flex; align-items: flex-start; gap: $sp-2; }
.gen__code {
flex: 1 1 auto; min-width: 0;
padding: $sp-2; border-radius: $r-sm;
background: $subject-ink; color: $subject-cream;
font-family: $mono; font-weight: 500; font-size: 10px; line-height: 1.5;
white-space: nowrap; overflow: hidden;
}
.gen__prop { color: $subject-blue-light; }
.gen__v { font-weight: 700; }
.gen__v--c0::after { counter-reset: v var(--v0); content: counter(v); }
.gen__v--c1::after { counter-reset: v var(--v1); content: counter(v); }
.gen__v--c2::after { counter-reset: v var(--v2); content: counter(v); }
.gen__v--c3::after { counter-reset: v var(--v3); content: counter(v); }
.gen__v--c4::after { counter-reset: v var(--v4); content: counter(v); }
.gen__v--c5::after { counter-reset: v var(--v5); content: counter(v); }
.gen__copy {
flex: 0 0 auto;
padding: $sp-2 $sp-3; border: 0; border-radius: $r-sm;
background: $subject-blue-deep; color: $subject-cream;
font-family: inherit; font-weight: 700; font-size: 11px; line-height: 1.4;
cursor: pointer;
}
.gen__copy:focus-visible { outline: 2px solid $color; outline-offset: 2px; }
// ── 격자 미리보기 — 여섯 반경이 엇갈려 오가면 육각형이 별처럼 숨쉰다
@keyframes pv0 { 0%, 100% { --v0: 90; } 50% { --v0: 45; } }
@keyframes pv1 { 0%, 100% { --v1: 60; } 40% { --v1: 95; } 80% { --v1: 35; } }
@keyframes pv2 { 0%, 100% { --v2: 80; } 50% { --v2: 40; } }
@keyframes pv3 { 0%, 100% { --v3: 60; } 45% { --v3: 95; } 85% { --v3: 40; } }
@keyframes pv4 { 0%, 100% { --v4: 85; } 50% { --v4: 40; } }
@keyframes pv5 { 0%, 100% { --v5: 65; } 40% { --v5: 30; } 80% { --v5: 90; } }
.gen.is-demo {
animation-name: pv0, pv1, pv2, pv3, pv4, pv5;
animation-duration: $duration;
animation-timing-function: $easing;
animation-iteration-count: infinite;
}
@media (prefers-reduced-motion: reduce) {
.gen.is-demo { animation: none !important; }
}
@media (max-width: 340px) {
.gen { gap: $sp-2; padding: $sp-2; }
.gen__top { gap: $sp-2; }
.gen__preview { width: 96px; height: 60px; }
.gen__code { font-size: 9px; }
}@property --v0 {
syntax: "<integer>";
inherits: true;
initial-value: 90;
}
@property --v1 {
syntax: "<integer>";
inherits: true;
initial-value: 60;
}
@property --v2 {
syntax: "<integer>";
inherits: true;
initial-value: 80;
}
@property --v3 {
syntax: "<integer>";
inherits: true;
initial-value: 60;
}
@property --v4 {
syntax: "<integer>";
inherits: true;
initial-value: 85;
}
@property --v5 {
syntax: "<integer>";
inherits: true;
initial-value: 65;
}
html, body {
margin: 0;
overflow: hidden;
}
.stage {
box-sizing: border-box;
min-height: 100vh;
display: grid;
place-items: center;
margin: 0;
background: #ffffff;
font-family: -apple-system, "Segoe UI", "Noto Sans KR", sans-serif;
padding: clamp(8px, 4vw, 24px);
}
.gen {
min-width: 0;
--v0: 90;
--v1: 60;
--v2: 80;
--v3: 60;
--v4: 85;
--v5: 65;
display: flex;
flex-direction: column;
gap: 12px;
box-sizing: border-box;
width: min(100%, 400px);
padding: 12px;
border: 1px solid rgba(23, 20, 26, 0.14);
border-radius: 16px;
background: rgba(23, 20, 26, 0.04);
}
.gen__top {
display: flex;
align-items: center;
gap: 12px;
}
.gen__preview-frame {
flex: 0 0 auto;
padding: 4px;
border: 1px dashed rgba(23, 20, 26, 0.3);
border-radius: 4px;
}
.gen__preview {
box-sizing: border-box;
width: 116px;
height: 72px;
background: #2f6df6;
clip-path: polygon(calc(50% + 0 * var(--v0) * 1%) calc(50% + -1 * var(--v0) * 0.6 * 1%), calc(50% + 0.87 * var(--v1) * 1%) calc(50% + -0.5 * var(--v1) * 0.6 * 1%), calc(50% + 0.87 * var(--v2) * 1%) calc(50% + 0.5 * var(--v2) * 0.6 * 1%), calc(50% + 0 * var(--v3) * 1%) calc(50% + 1 * var(--v3) * 0.6 * 1%), calc(50% + -0.87 * var(--v4) * 1%) calc(50% + 0.5 * var(--v4) * 0.6 * 1%), calc(50% + -0.87 * var(--v5) * 1%) calc(50% + -0.5 * var(--v5) * 0.6 * 1%));
}
.gen__grid {
flex: 1 1 auto;
min-width: 0;
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 4px 8px;
}
.gen__row {
display: flex;
align-items: center;
gap: 4px;
cursor: pointer;
}
.gen__lab {
flex: 0 0 auto;
width: 38px;
color: #17141a;
font-weight: 700;
font-size: 10px;
line-height: 1.4;
}
.gen__slot {
position: relative;
flex: 1 1 auto;
min-width: 0;
display: grid;
align-items: center;
height: 16px;
}
.gen__slot::before {
content: "";
grid-area: 1/1;
height: 4px;
border-radius: 999px;
background: rgba(23, 20, 26, 0.18);
}
.gen__rng {
grid-area: 1/1;
appearance: none;
-webkit-appearance: none;
width: 100%;
min-width: 0;
height: 16px;
margin: 0;
background: transparent;
cursor: pointer;
}
.gen__rng::-webkit-slider-thumb {
-webkit-appearance: none;
appearance: none;
width: 12px;
height: 12px;
border: 0;
border-radius: 50%;
background: transparent;
}
.gen__rng::-moz-range-thumb {
width: 12px;
height: 12px;
border: 0;
border-radius: 50%;
background: transparent;
}
.gen__rng:focus-visible {
outline: 2px solid #17141a;
outline-offset: 2px;
border-radius: 999px;
}
.gen__thumb {
position: absolute;
top: 50%;
left: calc(var(--pct) * 1%);
width: 12px;
height: 12px;
margin-top: -6px;
margin-left: -6px;
border-radius: 50%;
background: #17141a;
pointer-events: none;
}
.gen__row:nth-child(1) .gen__thumb {
--pct: calc((var(--v0) - 20) / 80 * 100);
}
.gen__row:nth-child(2) .gen__thumb {
--pct: calc((var(--v1) - 20) / 80 * 100);
}
.gen__row:nth-child(3) .gen__thumb {
--pct: calc((var(--v2) - 20) / 80 * 100);
}
.gen__row:nth-child(4) .gen__thumb {
--pct: calc((var(--v3) - 20) / 80 * 100);
}
.gen__row:nth-child(5) .gen__thumb {
--pct: calc((var(--v4) - 20) / 80 * 100);
}
.gen__row:nth-child(6) .gen__thumb {
--pct: calc((var(--v5) - 20) / 80 * 100);
}
.gen__out {
display: flex;
align-items: flex-start;
gap: 8px;
}
.gen__code {
flex: 1 1 auto;
min-width: 0;
padding: 8px;
border-radius: 8px;
background: #17141a;
color: #fff7e6;
font-family: ui-monospace, SFMono-Regular, "SF Mono", Consolas, "Liberation Mono", monospace;
font-weight: 500;
font-size: 10px;
line-height: 1.5;
white-space: nowrap;
overflow: hidden;
}
.gen__prop {
color: #5b8cf8;
}
.gen__v {
font-weight: 700;
}
.gen__v--c0::after {
counter-reset: v var(--v0);
content: counter(v);
}
.gen__v--c1::after {
counter-reset: v var(--v1);
content: counter(v);
}
.gen__v--c2::after {
counter-reset: v var(--v2);
content: counter(v);
}
.gen__v--c3::after {
counter-reset: v var(--v3);
content: counter(v);
}
.gen__v--c4::after {
counter-reset: v var(--v4);
content: counter(v);
}
.gen__v--c5::after {
counter-reset: v var(--v5);
content: counter(v);
}
.gen__copy {
flex: 0 0 auto;
padding: 8px 12px;
border: 0;
border-radius: 8px;
background: #0a4fe8;
color: #fff7e6;
font-family: inherit;
font-weight: 700;
font-size: 11px;
line-height: 1.4;
cursor: pointer;
}
.gen__copy:focus-visible {
outline: 2px solid #17141a;
outline-offset: 2px;
}
@keyframes pv0 {
0%, 100% {
--v0: 90;
}
50% {
--v0: 45;
}
}
@keyframes pv1 {
0%, 100% {
--v1: 60;
}
40% {
--v1: 95;
}
80% {
--v1: 35;
}
}
@keyframes pv2 {
0%, 100% {
--v2: 80;
}
50% {
--v2: 40;
}
}
@keyframes pv3 {
0%, 100% {
--v3: 60;
}
45% {
--v3: 95;
}
85% {
--v3: 40;
}
}
@keyframes pv4 {
0%, 100% {
--v4: 85;
}
50% {
--v4: 40;
}
}
@keyframes pv5 {
0%, 100% {
--v5: 65;
}
40% {
--v5: 30;
}
80% {
--v5: 90;
}
}
.gen.is-demo {
animation-name: pv0, pv1, pv2, pv3, pv4, pv5;
animation-duration: 2s;
animation-timing-function: cubic-bezier(0.2, 0.8, 0.2, 1);
animation-iteration-count: infinite;
}
@media (prefers-reduced-motion: reduce) {
.gen.is-demo {
animation: none !important;
}
}
@media (max-width: 340px) {
.gen {
gap: 8px;
padding: 8px;
}
.gen__top {
gap: 8px;
}
.gen__preview {
width: 96px;
height: 60px;
}
.gen__code {
font-size: 9px;
}
}var gen = document.getElementById('gen');
var copyBtn = document.getElementById('copy');
var ranges = Array.prototype.slice.call(gen.querySelectorAll('.gen__rng'));
var COPY_LABEL = 'コピー';
var DONE_LABEL = 'コピーしました';
// 여섯 꼭짓점은 60도 間隔 고정. 각 꼭짓점의 (dx, dy) 계수표 — CSS 와 JS 가 같은 표를 공유한다
// (60도 間隔 여섯 방향의 단위원 좌표, 소수 셋째에서 반올림). 중심 50% 에서 계수만큼 뻗는다.
var DIRS = [
[0, -1], [0.87, -0.5], [0.87, 0.5], [0, 1], [-0.87, 0.5], [-0.87, -0.5]
];
function readVar(name) {
return parseInt(getComputedStyle(gen).getPropertyValue(name), 10) || 0;
}
// 복사용 문자열 — 실제 polygon 전체 좌표. 미리보기는 CSS 가 같은 계수표로 그린다(주석 참조)
function clipText() {
var pts = DIRS.map(function (d, i) {
var r = readVar('--v' + i);
var x = Math.round(50 + d[0] * r);
var y = Math.round(50 + d[1] * r * 0.6);
return x + '% ' + y + '%';
});
return 'clip-path: polygon(' + pts.join(', ') + ');';
}
// 클립보드는 iframe·http 환경에서 막힐 수 있다 — 거부된 프로미스를 반드시 catch 로 받아 선택 방식으로 떨어진다
function fallbackCopy(text) {
var area = document.createElement('textarea');
area.value = text;
area.setAttribute('readonly', '');
area.style.position = 'fixed';
area.style.opacity = '0';
document.body.appendChild(area);
area.select();
try { document.execCommand('copy'); } catch (err) { /* 여기까지 막히면 조용히 포기한다 */ }
document.body.removeChild(area);
}
ranges.forEach(function (input) {
input.addEventListener('input', function () {
gen.classList.remove('is-demo');
gen.style.setProperty(input.dataset.var, input.value);
});
input.addEventListener('pointerdown', function () { gen.classList.remove('is-demo'); });
});
copyBtn.addEventListener('click', function () {
gen.classList.remove('is-demo');
var text = clipText();
if (navigator.clipboard && navigator.clipboard.writeText) {
navigator.clipboard.writeText(text).catch(function () { fallbackCopy(text); });
} else {
fallbackCopy(text);
}
copyBtn.textContent = DONE_LABEL;
setTimeout(function () { copyBtn.textContent = COPY_LABEL; }, 1200);
});import { useState, useMemo } from 'react';
import styles from './ClipPolygon.module.scss';
// 60도 간격 여섯 방향의 계수표 — vanilla scss 의 clip-path calc 와 같은 표를 공유한다
const DIRS: Array<[number, number]> = [
[0, -1], [0.87, -0.5], [0.87, 0.5], [0, 1], [-0.87, 0.5], [-0.87, -0.5]
];
// ambient 선언에 React 이벤트 제네릭이 없어 로컬 타입으로 받는다 (blog-demo TS7006 항)
type RangeEvt = { target: { value: string } };
export type ClipPolygonProps = {
duration?: string;
easing?: string;
color?: string;
};
export default function ClipPolygon({ duration = '2s', easing = 'cubic-bezier(.2, .8, .2, 1)', color = '#17141a' }: ClipPolygonProps) {
const [values, setValues] = useState<number[]>([90, 60, 80, 60, 85, 65]);
const [live, setLive] = useState(true);
const clipStyle = useMemo(() => {
const pts = DIRS.map((d, i) => {
const x = 50 + d[0] * values[i];
const y = 50 + d[1] * values[i] * 0.6;
return `${x.toFixed(0)}% ${y.toFixed(0)}%`;
});
return `polygon(${pts.join(', ')})`;
}, [values]);
const codeText = useMemo(() => {
const pts = DIRS.map((d, i) => {
const x = Math.round(50 + d[0] * values[i]);
const y = Math.round(50 + d[1] * values[i] * 0.6);
return `${x}% ${y}%`;
});
return `clip-path: ${clipStyle};`;
}, [values, clipStyle]);
const style = {
'--dur-loop': duration,
'--ease-spring': easing,
'--accent': color
} as Record<string, string | number>;
function copy() {
setLive(false);
try {
navigator.clipboard.writeText(codeText);
} catch (err) {
/* 클립보드가 막힌 환경에서는 조용히 넘어간다 */
}
}
return (
<div className={live ? `${styles.gen} ${styles.isDemo}` : styles.gen} style={style}>
<div className={styles.top}>
<div className={styles.previewFrame} aria-hidden="true">
<div className={styles.preview} style={{ clipPath: clipStyle }}></div>
</div>
<div className={styles.grid}>
{DIRS.map((_, i) => (
<label className={styles.row} key={i}>
<span className={styles.lab}>{'꼭짓점' + (i + 1)}</span>
<span className={styles.slot}>
<input
className={styles.rng}
type="range"
min={20}
max={100}
step={5}
value={values[i]}
aria-label={(i + 1) + ' 번째 꼭짓점 반경'}
onChange={(e: RangeEvt) => {
setLive(false);
setValues((v) => v.map((x, j) => (j === i ? Number(e.target.value) : x)));
}}
/>
<span className={styles.thumb} aria-hidden="true" style={{ left: `${((values[i] - 20) / 80) * 100}%` }} />
</span>
</label>
))}
</div>
</div>
<div className={styles.out}>
<code className={styles.code}>
<span className={styles.prop}>clip-path:</span> {values[0]}% {values[1]}% {values[2]}% {values[3]}% {values[4]}% {values[5]}%
</code>
<button className={styles.copy} type="button" onClick={copy}>복사</button>
</div>
</div>
);
}04 多角形の頂点クリップ — 計測 累積面積 6.233% · 動いたフレーム 23/23GODRICH オリジナル · SCSS と React は zip に同梱 · css ジェネレーター 9種 — スライダーで作る css 影のつけ方
このデモが属する記事
css ジェネレーター 9種 — スライダーで作る css 影のつけ方css ジェネレーター とは、CSS の値を画面の上でつまんで決める道具のことです — スライダーを動かすとコードが変わります。影・グラデーション・角丸・タイミングを目で合わせ、下のコード欄からそのまま貼って使えます。9つを自作し、グリッド この記事のデモをすべて見る


