CSSで、カードの角に斜めのリボンを付ける方法
カードの角に「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、スマートフォンの実機、両端を折り返したような立体的なリボンは試していません。
右上に斜めのリボンを置く

HTML は、カードの中にリボンの要素を1つ置くだけです。
<div class="card">
<span class="ribbon">NEW</span>
<h3>春の新作セット</h3>
<p>カードの角に、斜めのリボンを付けた例です。</p>
</div>
.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 を使い、逆向きに回します。
.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 ではあふれた内容が要素のパディングボックスで切り取られ、見えなくなります。

カードの外にあるリボンの色の画素を数えると、overflow: hidden がないときは 1,430 個、付けたときは 0 個でした。
角からの距離に合わせて、リボンの長さを決める
リボンが短すぎると、帯の両端がカードの縁まで届かず、帯の端の角がカードの中にのぞきます。

帯の両端がカードの縁まで届くかどうかは、帯の2本の長い辺のうち、角から遠い側の辺で決まります。角から帯の中心までの縦・横の距離を d、帯の太さを H とすると、この辺がカードの上の辺と交わる点は、帯の中心から帯の向きに沿って √2 × d + H ÷ 2 の位置にあります。帯の長さ W は、その2倍以上にします。
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() で求め、変数はカスタムプロパティで持たせます。
.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 個に減りました。先頭の「期」がカードの上の辺にかかり、白い文字の部分が入ったためです。

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





