CSSでチケット風クーポンのギザギザ境界線を描く

1. 背景画像でシンプルに

最も軽量なのは、linear-gradient を組み合わせた背景画像で左右のギザギザを表現する方法です。

<div class="coupon">
  <div class="coupon__body">¥500 OFF</div>
  <div class="coupon__side">GET</div>
</div>
.coupon {
  display: flex;
  height: 96px;
  border-top: 1px solid #e63946;
  border-bottom: 1px solid #e63946;
  position: relative;
  overflow: hidden;
}

.coupon::before,
.coupon::after {
  content: "";
  position: absolute;
  top: 0;
  bottom: 0;
  width: 12px;
  background: 
    radial-gradient(circle at 0 50%, transparent 6px, #e63946 6px, #e63946 7px, transparent 7px) 0 0 / 12px 12px;
}

.coupon::before {
  left: 0;
}

.coupon::after {
  right: 0;
  transform: scaleX(-1);
}

.coupon__body {
  flex: 1;
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 24px;
  color: #e63946;
  padding: 0 20px;
}

.coupon__side {
  width: 72px;
  background: #e63946;
  color: #fff;
  display: flex;
  align-items: center;
  justify-content: center;
  font-weight: bold;
}

2. 擬似要素で動的に生成

円形の擬似要素を多数並べることで、より滑らかなギザギザを実現します。

<div class="ticket">
  <div class="ticket__left">
    <strong>20%割引</strong>
    <small>期間限定</small>
  </div>
  <div class="ticket__right">
    <span>USE</span>
  </div>
</div>
.ticket {
  --radius: 8px;
  --gap: 12px;
  --color: #d62828;
  display: flex;
  width: 320px;
  height: 100px;
  position: relative;
  filter: drop-shadow(0 2px 4px rgba(0,0,0,.1));
}

.ticket__left,
.ticket__right {
  position: relative;
  background: #fff;
}

.ticket__left {
  flex: 1;
  border: 1px solid var(--color);
  border-right: none;
  border-radius: var(--radius) 0 0 var(--radius);
  display: flex;
  flex-direction: column;
  justify-content: center;
  padding-left: 16px;
}

.ticket__right {
  width: 80px;
  background: var(--color);
  border-radius: 0 var(--radius) var(--radius) 0;
  display: flex;
  align-items: center;
  justify-content: center;
  color: #fff;
  font-weight: bold;
}

.ticket__left::after,
.ticket__right::before {
  content: "";
  position: absolute;
  top: 0;
  bottom: 0;
  width: var(--gap);
  background-image: 
    radial-gradient(circle at 50% 0, transparent var(--radius), var(--color) calc(var(--radius) + 1px), transparent calc(var(--radius) + 2px)),
    radial-gradient(circle at 50% var(--gap), transparent var(--radius), var(--color) calc(var(--radius) + 1px), transparent calc(var(--radius) + 2px)),
    radial-gradient(circle at 50% calc(var(--gap) * 2), transparent var(--radius), var(--color) calc(var(--radius) + 1px), transparent calc(var(--radius) + 2px)),
    radial-gradient(circle at 50% calc(var(--gap) * 3), transparent var(--radius), var(--color) calc(var(--radius) + 1px), transparent calc(var(--radius) + 2px)),
    radial-gradient(circle at 50% calc(var(--gap) * 4), transparent var(--radius), var(--color) calc(var(--radius) + 1px), transparent calc(var(--radius) + 2px)),
    radial-gradient(circle at 50% calc(var(--gap) * 5), transparent var(--radius), var(--color) calc(var(--radius) + 1px), transparent calc(var(--radius) + 2px)),
    radial-gradient(circle at 50% calc(var(--gap) * 6), transparent var(--radius), var(--color) calc(var(--radius) + 1px), transparent calc(var(--radius) + 2px));
  background-size: 100% calc(var(--gap) * 7);
  background-repeat: repeat-y;
}

.ticket__left::after {
  right: calc(var(--gap) * -0.5);
}

.ticket__right::before {
  left: calc(var(--gap) * -0.5);
  transform: rotate(180deg);
}

3. SVGマスクを使った高解像度対応

Retina や印刷時も綺麗に表示したい場合は、SVGマスクが最適です。

<svg width="0" height="0">
  <defs>
    <mask id="gizaMask">
      <rect x="0" y="0" width="100%" height="100%" fill="white"/>
      <circle cx="0" cy="12" r="6" fill="black"/>
      <circle cx="0" cy="36" r="6" fill="black"/>
      <circle cx="0" cy="60" r="6" fill="black"/>
      <circle cx="0" cy="84" r="6" fill="black"/>
    </mask>
  </defs>
</svg>

<div class="clip-coupon">
  <div class="clip-coupon__main">¥1,000 OFF</div>
  <div class="clip-coupon__action">TAP</div>
</div>
.clip-coupon {
  display: flex;
  width: 280px;
  height: 88px;
  border-radius: 8px;
  overflow: hidden;
  box-shadow: 0 4px 8px rgba(0,0,0,.15);
}

.clip-coupon__main {
  flex: 1;
  background: #fff;
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 22px;
  color: #e63946;
  mask: url(#gizaMask) 100% 0 / 12px 100% no-repeat;
}

.clip-coupon__action {
  width: 72px;
  background: linear-gradient(135deg, #e63946, #f77f00);
  color: #fff;
  display: flex;
  align-items: center;
  justify-content: center;
  font-weight: bold;
  mask: url(#gizaMask) 0 0 / 12px 100% no-repeat;
}

4. CSS Paint API で完全に動的に

Chrome 系ブラウザ限定ですが、paint() を使えば JavaScript なしで動的にギザギザを描画できます。

/* paint-worklet.js */
registerPaint('giza-border', class {
  static get inputProperties() {
    return ['--giza-radius', '--giza-gap'];
  }
  paint(ctx, geom, props) {
    const r = props.get('--giza-radius').value;
    const g = props.get('--giza-gap').value;
    const h = geom.height;
    for (let y = r; y < h; y += g) {
      ctx.beginPath();
      ctx.arc(0, y, r, 0, 2 * Math.PI);
      ctx.fillStyle = '#fff';
      ctx.fill();
    }
  }
});

/* メインCSS */
.coupon-paint {
  --giza-radius: 6;
  --giza-gap: 12;
  background: paint(giza-border) right / 12px 100% no-repeat, #fff;
  border: 1px solid #e63946;
  border-right: none;
  border-radius: 8px 0 0 8px;
  height: 96px;
  width: 240px;
}

上記のように、用途に応じて背景画像・擬似要素・SVGマスク・Paint API の 4 パターンを使い分けることで、様々な環境でチケット風クーポンを実装できます。

タグ: CSS linear-gradient radial-gradient SVGマスク Paint API

5月13日 21:41 投稿