CSSで、カードの角に斜めのリボンを付ける方法

​​CSS​HTML,Playwright

カードの角に「NEW」のような斜めのリボンを付けるには、カードに position: relative と overflow: hidden を付けます。その中に細長い要素を position: absolute で角の近くに置き、transform: rotate(45deg) で斜めにします。カードからはみ出した両端は overflow: hidden で切れるので、リボンが角を斜めに横切って見えます。

補足

検証環境は Ubuntu 24.04、Node.js 22.23.4、Playwright 1.63.0、Chromium 153 です。HTML と CSS だけのページを Playwright で開き、リボンの位置を getBoundingClientRect() で測り、スクリーンショットの画素からカードの外にはみ出した部分や、カードの縁にかかったリボンの長さを数えました。文字は Noto Sans CJK JP の太字・16px です。Firefox と Safari、スマートフォンの実機、両端を折り返したような立体的なリボンは試していません。

右上に斜めのリボンを置く

上下2段のカード。上は右上の角に、濃い水色の帯に白い文字で「NEW」と書いたリボンが斜めにかかっている。下は左上の角に「おすすめ」のリボンが逆向きの斜めにかかり、見出しはリボンより下に下げてある
右上に付けたリボン(上)と、左上に付けて見出しを下げたリボン(下)

HTML は、カードの中にリボンの要素を1つ置くだけです。

html
<div class="card">
  <span class="ribbon">NEW</span>
  <h3>春の新作セット</h3>
  <p>カードの角に、斜めのリボンを付けた例です。</p>
</div>
css
.card {
  position: relative; /* リボンの位置の基準にする */
  overflow: hidden;   /* カードからはみ出した部分を切る */
  width: 280px;
  padding: 24px;
  border-radius: 12px;
  background: #fff;
}

.ribbon {
  position: absolute;
  top: 24px;     /* 40px − 32px ÷ 2 */
  right: -40px;  /* 40px − 160px ÷ 2 */
  width: 160px;
  line-height: 32px;
  text-align: center;
  background: #067a98;
  color: #fff;
  font-weight: bold;
  transform: rotate(45deg);
}

リボンは長さ 160px、太さ 32px の帯です。高さは指定せず、line-height: 32px で1行の高さを決めています。検証環境で測ると、リボンの要素は幅 160px、高さ 32px でした。

top と right の値は、回す前の帯の中心が「カードの右上の角から、左に 40px・下に 40px の点」に来るように決めています。帯の上端を中心から太さの半分だけ上にするので top は 40 − 16 = 24px、右端を中心から長さの半分だけ右にするので right は 40 − 80 = −40px です。transform-origin の初期値は 50% 50% なので、rotate() は要素の中心を軸に回ります。中心の位置は回しても変わりません。

rotate() は、左から右に書く文書では正の角度で時計回りに回します。45deg で、帯は左上から右下へ下がる向きになり、カードの上の辺と右の辺をつなぎます。回したあとの帯の外接矩形の中心は、カードの右上の角から左に 40px、下に 40px の位置でした。

左上に付けるときは、right の代わりに left を使い、逆向きに回します。

css
.ribbon.left {
  right: auto;
  left: -40px;
  transform: rotate(-45deg);
}

この指定で測ると、帯の中心はカードの左上の角から右に 40px、下に 40px の位置でした。

リボンはカードの中身の上に重なって描かれます。左上に付けると、左上から始まる見出しの先頭がリボンに隠れました。見出しの文字の範囲にあるリボンの色の画素を数えると 485 個です。上の画像の左上の例では、カードの padding-top を 84px にして、見出しを帯より下に下げています。同じ padding-top で測ると、見出しにかかるリボンの画素は 0 個でした。リボンを付ける角の近くには、見出しや本文を置かないようにします。

カードに付ける position と overflow の役割

position: absolute の要素は、static 以外の position を持つ直近の祖先を基準に置かれます。そのような祖先がなければ、初期包含ブロック(文書のルート要素の包含ブロック)が基準になります。カードの position: relative は、リボンの top と right を「カードの端から」の距離にするためのものです。

カードから position: relative を外して測ると、リボンの中心は幅 1280px のページの右端から 40px、上端から 40px の位置に移りました。カードは左端近くにあるので、リボンはカードから遠く離れた画面の右上に出ます。

overflow: hidden は、帯の両端を切るために付けています。MDN によると、hidden ではあふれた内容が要素のパディングボックスで切り取られ、見えなくなります。

上下2段の比較。上の「overflow: hidden なし」は、リボンの両端がカードの上と右の外に突き出ている。下の「overflow: hidden あり」は、リボンがカードの上の辺と右の辺で切れ、角を斜めに横切っている
overflow: hidden がないと、帯の両端がカードの外に突き出る(上)。付けると、カードの縁で切れる(下)

カードの外にあるリボンの色の画素を数えると、overflow: hidden がないときは 1,430 個、付けたときは 0 個でした。

角からの距離に合わせて、リボンの長さを決める

リボンが短すぎると、帯の両端がカードの縁まで届かず、帯の端の角がカードの中にのぞきます。

上下2段の比較。上の「リボンの長さが 120px」は、帯の両端がカードの上の辺と右の辺に届かず、帯の端の角がカードの中にのぞいていて、そこをピンクの丸で囲んでいる。下の「リボンの長さが 160px」は、帯がカードの上の辺と右の辺まで届いている
長さが足りないと、帯の端の角がカードの中にのぞく(上)。足りていれば、縁で切れる(下)

帯の両端がカードの縁まで届くかどうかは、帯の2本の長い辺のうち、角から遠い側の辺で決まります。角から帯の中心までの縦・横の距離を d、帯の太さを H とすると、この辺がカードの上の辺と交わる点は、帯の中心から帯の向きに沿って √2 × d + H ÷ 2 の位置にあります。帯の長さ W は、その2倍以上にします。

text
W ≥ 2 × √2 × d + H

d が 40px、H が 32px なら、W は約 145.1px 以上です。カードのいちばん上の行にかかるリボンの色の画素を数えて、帯の長さを変えて比べました。

d 帯の長さ W 上の行にかかるリボンの画素 縁まで届くか
40px 120px 27 個 届かない
40px 140px 41 個 届かない
40px 146px 43 個 届く
40px 160px 43 個 届く
50px 160px 35 個 届かない
50px 180px 43 個 届く

届いているときは、どれも 43 個でそろいました。145.1px より短い 140px では 41 個に減り、帯の端の角がカードの中にのぞいています。d を 50px に広げると必要な長さは約 173.4px になるので、160px のままでは届かず、180px にすると届きました。

d と H と W を変数にしておくと、リボンの大きさを変えるときに top と right を計算し直さずに済みます。値は calc() で求め、変数はカスタムプロパティで持たせます。

css
.ribbon.vars {
  --w: 160px; /* リボンの長さ */
  --h: 32px;  /* リボンの太さ */
  --d: 40px;  /* 角からリボンの中心までの距離(縦・横) */
  top: calc(var(--d) - var(--h) / 2);
  right: calc(var(--d) - var(--w) / 2);
  width: var(--w);
  line-height: var(--h);
}

この書き方で --d: 50px にすると、帯の中心はカードの右上の角から左に 50px、下に 50px の位置に移りました。--d を広げるときは、上の式で --w も伸ばします。

文字が長いと、カードの縁で文字が切れる

帯は長くしても、カードの中に見えるのは角の近くの一部だけです。帯の中心線のうち、カードの中に見える長さは 2 × √2 × d で、d が 40px なら約 113px です。文字はこれより短くても、角に近い側の辺の近くでは先に縁にかかります。

太字・16px の文字の幅を測ると、「NEW」は 36.47px、「おすすめ」は 64.02px、「期間限定セール」は 112.02px でした。d が 40px・長さ 160px のリボンでは、「NEW」と「おすすめ」のときカードのいちばん上の行にかかるリボンの色の画素は 43 個でしたが、「期間限定セール」では 35 個に減りました。先頭の「期」がカードの上の辺にかかり、白い文字の部分が入ったためです。

上下2段の比較。上の「d が 40px、長さ 160px」は、「期間限定セール」の先頭の「期」がカードの上の辺にかかって切れていて、そこをピンクの丸で囲んでいる。下の「d が 50px、長さ 180px」は、同じ文字がリボンの中に収まっているが、帯が本文の行末の「た」に重なっている
d が 40px では先頭の文字が上の辺で切れる(上)。d を 50px、長さを 180px にすると収まる(下)

長い文字を入れるときは、--d を広げてリボンを角から遠ざけ、上の式に合わせて --w も伸ばします。--d: 50px; --w: 180px にすると、「期間限定セール」でも上の行にかかるリボンの色の画素は 43 個に戻りました。そのぶん帯はカードの内側まで入るので、画像の下の段のように本文の行末に重なります。リボンを大きくしたときは、角の近くの中身の余白も合わせて広げます。

​Posted by 86