CSSで、写真を少し傾けて重ねて並べる方法
写真を机に散らしたように並べるには、写真ごとに白い枠を付け、rotate で数度ずつ傾けます。となりの写真と重ねるのは、横に並べたうえで、左のマージンをマイナスにして写真どうしを近づけるだけです。傾ける角度は、写真ごとにカスタムプロパティで渡します。
検証環境は Ubuntu 24.04、Node.js 22.23.4、Playwright 1.63.0、Chromium 153 です。HTML と CSS のページを Playwright で開き、getComputedStyle() の値、要素の位置と大きさ、重なった点にある要素と画素の色を記録しました。写真は、筆者が SVG で描いた4枚の絵で代用しています。Firefox と Safari、スマートフォンの実機は試していません。各ブラウザの対応バージョンは、互換性データ(@mdn/browser-compat-data 8.1.8)の値です。

白い枠を付けて、写真ごとに角度を変える
HTML は、写真1枚を <figure> で包み、傾ける角度を --tilt というカスタムプロパティで渡します。
<div class="photos">
<figure class="photo" style="--tilt: -6deg"><img src="img/sea.svg" alt="海" width="400" height="300"></figure>
<figure class="photo" style="--tilt: 4deg"><img src="img/mountain.svg" alt="山" width="400" height="300"></figure>
<figure class="photo" style="--tilt: -3deg"><img src="img/flower.svg" alt="花" width="400" height="300"></figure>
<figure class="photo" style="--tilt: 7deg"><img src="img/night.svg" alt="夜空" width="400" height="300"></figure>
</div>
CSS は次のとおりです。これで上の画像のように並びます。
.photos {
--w: min(160px, 22vw);
display: flex;
justify-content: center;
padding: 32px 16px;
}
.photo {
flex: 0 0 auto;
width: var(--w);
margin: 0;
padding: 8px 8px 28px;
background: #fff;
box-shadow: 0 4px 12px rgb(0 0 0 / 0.25);
rotate: var(--tilt, 0deg);
transition: rotate 0.2s, scale 0.2s;
}
.photo + .photo {
margin-left: calc(var(--w) * -0.25);
}
.photo img {
display: block;
width: 100%;
height: auto;
}
.photo:hover {
z-index: 1;
rotate: 0deg;
scale: 1.1;
}
.photo の白い背景と padding が、写真の枠になります。下だけ 28px と広くすると、プリントした写真のような枠になります。box-shadow の影は、写真が背景から少し浮いて見えるように付けています。
傾けるのは rotate: var(--tilt, 0deg) の1行です。正の角度で時計回り、負の角度で反時計回りに回ります。検証環境で getComputedStyle() の rotate を読むと、4枚それぞれ -6deg、4deg、-3deg、7deg で、style 属性で渡した値がそのまま使われていました。角度は、どれも 10 度未満にとどめ、向きを交互に変えています。
負のマージンで、となりの写真に重ねる
.photo + .photo は、2枚目以降の写真を指します。その左のマージンを、--w(枠を含まない写真の幅)の4分の1だけマイナスにして、左の写真に重ねています。写真の幅は --w にまとめ、幅 160px を上限に、画面の幅の 22% まで縮むようにしました。幅 680px の画面では、どの写真も幅 166px(枠を含む)で、左端は 65px、193px、321px、450px と、128〜129px ずつずれていました。
重なった部分では、HTML で後ろに書いた写真が上になります。1枚目と2枚目が重なる点を document.elementFromPoint() で調べると、返ってきたのは2枚目の写真(山)で、その点の画素は2枚目の枠の白でした。
傾けても、まわりの配置は変わりません。1枚目は、offsetWidth が 166px のままで、傾いた後の外側を囲む四角(getBoundingClientRect())は幅 180.2px に広がっていました。重なり具合は、マージンで決まった位置を、そのまま回して見せているだけです。CSS Transforms の仕様にも、変形は見た目を変えるだけで、まわりのレイアウトには、はみ出しの範囲を除いて影響しないと書かれています。
カーソルを乗せた写真を、まっすぐにして手前に出す
.photo:hover では、rotate: 0deg で傾きを戻し、scale: 1.1 で少し大きくしています。z-index: 1 は、重なっている右どなりの写真より手前に出すためです。z-index は、position を指定していなくても、フレックスアイテムには効きます。

検証環境で2枚目にカーソルを乗せると、rotate は 0deg、scale は 1.1、z-index は 1 になりました。2枚目と3枚目が重なる点は、乗せる前は3枚目(花)でしたが、乗せた後は2枚目(山)に変わっていました。
transition で 0.2 秒かけて変わるようにしているので、途中の値も読みました。遷移を 0.1 秒の位置で止めると、rotate は 0.790386deg、scale は 1.08024 で、4 度からまっすぐに戻る途中でした。
transform ではなく、rotate と scale で書く
傾きは transform: rotate(-6deg) でも付けられます。ただ、カーソルを乗せたときに transform: scale(1.1) とだけ書くと、transform の値ごと置き換わり、傾きが消えます。検証環境で、写真を transform: rotate(var(--tilt)) で傾け、:hover で transform: scale(1.1) にして1枚目にカーソルを乗せると、カーソルを乗せた後の値は matrix(1.1, 0, 0, 1.1, 0, 0) で、回転の成分が残っていませんでした。
rotate と scale を別々のプロパティで書くと、片方だけを変えられます。:hover で scale だけを変え、rotate は var(--tilt) のままにしたところ、カーソルを乗せた後も rotate は -6deg で、scale だけが 1.1 になっていました。この記事の CSS で、まっすぐに戻す rotate: 0deg と、大きくする scale: 1.1 を別々に書いているのも、このためです。
CSS Transforms Level 2 の仕様では、translate、rotate、scale を組み合わせる順番が決まっています。transform の中に関数を並べるときと違って、書く順番を気にしなくて済みます。
rotate と scale のプロパティは、互換性データでは Chrome 104、Edge 104、Firefox 72、Safari 14.1、iOS の Safari 14.5 から使えます。
傾けない写真にも rotate: 0deg を書く
並べた写真のうち1枚だけまっすぐにしたいとき、その写真の rotate を none にすると、左どなりの写真と重なった部分が、その下に隠れます。2枚目だけを rotate: none にして、1枚目と2枚目が重なる点を調べると、document.elementFromPoint() が返したのは1枚目の写真(海)で、画素の色も1枚目の海の色 rgb(31, 163, 198) でした。HTML では2枚目が後ろにあるのに、1枚目が上に描かれています。

CSS Transforms Level 2 の仕様では、rotate が none のときは重ね合わせコンテキストを作らず、0deg のように見た目が変わらない値でも、none 以外なら作ります。transform や rotate などで変形を指定した要素は、position を指定して z-index: 0 にした要素と同じ順番で描かれます(CSS Transforms の仕様)。そのため、傾いた1枚目は、何も指定していない2枚目より後から描かれ、上に重なります。
まっすぐにしたい写真も、rotate: 0deg にしておけば、HTML の順番どおりに重なります。2枚目を rotate: 0deg にして同じ点を調べると、2枚目の写真(山)が返り、画素は2枚目の枠の白でした。この記事の CSS では、--tilt を書かなかった写真も rotate: var(--tilt, 0deg) で 0deg になります。
傾いた角で、横スクロールが出ないようにする
傾けた写真の角は、要素の四角より外へ出ます。CSS Transforms の仕様では、変形した後の範囲も、スクロールできる範囲(はみ出しの範囲)に含まれます。写真の列を画面の幅いっぱいに並べると、右端の写真の角がはみ出して、横スクロールが出ます。
幅 375px の画面で、.photos の左右の padding を 0 にし、写真の幅を画面の幅の 25% にして試しました。右端の写真は、7 度傾いた角が 377.9px まで出て、ページの scrollWidth は、画面の幅(375px)より大きい 378px になりました。
この記事の CSS は、.photos の左右に 16px の padding を取り、写真の幅を 22% にして、角の分の余白を残しています。幅 375px の画面でも、写真の角は左端が 16.6px、右端が 359.2px の間に収まり、scrollWidth は 375px で、横スクロールは出ませんでした。角が外へ出る量は、角度が大きいほど増えます。幅 680px の画面では、どれも幅 166px の写真の外側を囲む四角が、3 度で 173.1px、4 度で 175.5px、6 度で 180.2px、7 度で 182.4px でした。角度を大きくするときは、そのぶん余白も広く取ります。





