目次
一覧ページなどでmasonryのようにアイテムをタイル状に並べたい。
そういう要件はありますでしょうか。
そういう話の時はだいたいアイテム要素のサイズが共通でありません。恐らく画像サイズやテキスト量が統一されておらず、凸凹しているデザインでしょう。
うまいこと並んでほしい。そんな時に便利なライブラリが「masonry」です。
masonry
https://masonry.desandro.com/
masonryはアイテム要素のサイズをゴリゴリ読み取って計算して、position:absoluteのtop&leftに数値を差し込んで、かっこいいアニメーション付きでタイル状に並べてくれるのが特徴です。

外人さんすげぇな。よくこんなの作るわ。
そのままでいいんじゃないですか!
そう思ったのですがしかし。
以下のような要件がプラスアルファされたらどうでしょう。
- ランダムに並び替えるボタンがほしい。
- ソート機能がほしい。
masonryのスクリプトをどう改良したら実現できるのでしょうか。
改造が必要なのではないですか?
並び替えた後に発火させるよう改造すればいいんじゃないですか。
知らんけど。
小一時間悩んで諦めました。すみません。
そんな時間がありません。
【探索】masonryっぽくcssで再現できないだろうか
masonryを使わないでそれっぽくしたい。
たくさんの時間を探索に回し見つけたのがWeb Design Leavesさんのブログです。
AIが無料パケット切れで課金しろと、助けてくれません。(当時)
Web Design Leaves
https://www.webdesignleaves.com/pr/plugins/css-grid-masonry.html
masonryのようにするための苦労の重なりが、とても長いページとなり、読む気力を減退させます。
見た目がすごく理想
https://www.webdesignleaves.com/pr/samples/plugins/masonry-grid/css-multicolumn-masonry-01.html
理想だったのですが、アイテム要素が縦積みになる仕様で断念です。
アイテム要素を横並びにしたいです。ソートして縦1本に並んでも困ります。
さらにブログを読み進めると、やはり。
やはりjavascriptも必要だとわかり、観念しました。
面倒でもやるしかないです。
【共有】ソースコード
作成したサンプルは以下になります。
共有サンプル
https://astrowave.jp/amnesia_record/sort_tile.php
要件は以下の3つです。
・masonryっぽいタイル並べにしたい
・メニューをつけて、ランダムとソートを機能させたい
・(オプション)アイテム要素に関係ない雰囲気イラストを挟めたい

メニューには上記のように機能を持たせます。
html
<div class="nav-box">
<ul id="sort-nav">
<li id="all" class="shuffle-button tab-menu js-tab current" data-filter="all">ALL</li>
<li id="friends-button" class="tab-menu js-tab" data-filter="friends">FRIENDS</li>
<li id="partner-button" class="tab-menu js-tab" data-filter="partner">PARTNER</li>
<li id="family-button" class="tab-menu js-tab" data-filter="family">FAMILY</li>
<li id="me-button" class="tab-menu js-tab" data-filter="me">ME</li>
</ul>
</div>
<div class="masonry-wrapper">
<div id="list-cont" class="masonry">
<div class="masonry-item friends shuffled-item">
<div class="masonry-content">
<img class="thumb" src="img/sort01.jpg" alt="image">
<h3 class="title">Items 1</h3>
<div class="desc">
<p>If a foolish person considers themselves to be foolish, that person is wise.</p>
</div>
</div>
</div>
<div class="masonry-item friends shuffled-item">
<div class="masonry-content">
<img class="thumb" src="img/sort02.jpg" alt="image">
<h3 class="title">Items 2</h3>
<div class="desc">
<p>If a foolish person considers themselves to be foolish, that person is wise.</p>
<p>When you try to prioritize only yourself, anger and sadness arise.</p>
<p>If you hesitate to do good, your heart will find pleasure in evil.</p>
</div>
</div>
</div>
<div class="masonry-item friends shuffled-item">
<div class="masonry-content">
<img class="thumb" src="img/sort03.jpg" alt="image">
<h3 class="title">Items 3</h3>
<div class="desc">
<p>If a foolish person considers themselves to be foolish, that person is wise.</p>
</div>
</div>
</div>
<div class="masonry-item friends shuffled-item">
<div class="masonry-content">
<img class="thumb" src="img/sort04.jpg" alt="image">
<h3 class="title">Items 4</h3>
<div class="desc">
<p>If a foolish person considers themselves to be foolish, that person is wise.</p>
</div>
</div>
</div>
<div class="masonry-item friends shuffled-item illust">
<div class="masonry-content">
<img class="thumb" src="img/illust_01.webp" alt="illust A friends">
</div>
</div>
<div class="masonry-item partner shuffled-item">
<div class="masonry-content">
<img class="thumb" src="img/sort05.jpg" alt="image">
<h3 class="title">Items 5</h3>
<div class="desc">
<p>If a foolish person considers themselves to be foolish, that person is wise.</p>
</div>
</div>
</div>
<div class="masonry-item partner shuffled-item">
<div class="masonry-content">
<img class="thumb" src="img/sort06.jpg" alt="image">
<h3 class="title">Items 6</h3>
<div class="desc">
<p>If a foolish person considers themselves to be foolish, that person is wise.</p>
</div>
</div>
</div>
<div class="masonry-item partner shuffled-item">
<div class="masonry-content">
<img class="thumb" src="img/sort07.jpg" alt="image">
<h3 class="title">Items 7</h3>
<div class="desc">
<p>If a foolish person considers themselves to be foolish, that person is wise.</p>
</div>
</div>
</div>
<div class="masonry-item partner shuffled-item">
<div class="masonry-content">
<img class="thumb" src="img/sort08.jpg" alt="image">
<h3 class="title">Items 8</h3>
<div class="desc">
<p>If a foolish person considers themselves to be foolish, that person is wise.</p>
<p>When you try to prioritize only yourself, anger and sadness arise.</p>
</div>
</div>
</div>
<div class="masonry-item partner shuffled-item illust">
<div class="masonry-content">
<img class="thumb" src="img/illust_02.webp" alt="illust B partner">
</div>
</div>
<div class="masonry-item family shuffled-item">
<div class="masonry-content">
<img class="thumb" src="img/sort09.jpg" alt="image">
<h3 class="title">Items 9</h3>
<div class="desc">
<p>If a foolish person considers themselves to be foolish, that person is wise.</p>
</div>
</div>
</div>
<div class="masonry-item family shuffled-item">
<div class="masonry-content">
<img class="thumb" src="img/sort10.jpg" alt="image">
<h3 class="title">Items 101</h3>
<div class="desc">
<p>If a foolish person considers themselves to be foolish, that person is wise.</p>
<p>When you try to prioritize only yourself, anger and sadness arise.</p>
</div>
</div>
</div>
<div class="masonry-item family shuffled-item">
<div class="masonry-content">
<img class="thumb" src="img/sort11.jpg" alt="image">
<h3 class="title">Items 11</h3>
<div class="desc">
<p>If a foolish person considers themselves to be foolish, that person is wise.</p>
</div>
</div>
</div>
<div class="masonry-item family shuffled-item">
<div class="masonry-content">
<img class="thumb" src="img/sort12.jpg" alt="image">
<h3 class="title">Items 12</h3>
<div class="desc">
<p>If a foolish person considers themselves to be foolish, that person is wise.</p>
<p>When you try to prioritize only yourself, anger and sadness arise.</p>
</div>
</div>
</div>
<div class="masonry-item family shuffled-item illust">
<div class="masonry-content">
<img class="thumb" src="img/illust_03.webp" alt="illust C family">
</div>
</div>
<div class="masonry-item me shuffled-item">
<div class="masonry-content">
<img class="thumb" src="img/sort13.jpg" alt="image">
<h3 class="title">Items 13</h3>
<div class="desc">
<p>If a foolish person considers themselves to be foolish, that person is wise.</p>
</div>
</div>
</div>
<div class="masonry-item me shuffled-item">
<div class="masonry-content">
<img class="thumb" src="img/sort14.jpg" alt="image">
<h3 class="title">Items 14</h3>
<div class="desc">
<p>If a foolish person considers themselves to be foolish, that person is wise.</p>
<p>If you hesitate to do good, your heart will find pleasure in evil.</p>
<p>Conquer anger by not giving in to it.</p>
</div>
</div>
</div>
<div class="masonry-item me shuffled-item">
<div class="masonry-content">
<img class="thumb" src="img/sort15.jpg" alt="image">
<h3 class="title">Items 15</h3>
<div class="desc">
<p>If a foolish person considers themselves to be foolish, that person is wise.</p>
</div>
</div>
</div>
<div class="masonry-item me shuffled-item">
<div class="masonry-content">
<img class="thumb" src="img/sort16.jpg" alt="image">
<h3 class="title">Items 16</h3>
<div class="desc">
<p>If a foolish person considers themselves to be foolish, that person is wise.</p>
</div>
</div>
</div>
<div class="masonry-item me shuffled-item">
<div class="masonry-content">
<img class="thumb" src="img/sort17.jpg" alt="image">
<h3 class="title">Items 17</h3>
<div class="desc">
<p>If a foolish person considers themselves to be foolish, that person is wise.</p>
</div>
</div>
</div>
<div class="masonry-item me shuffled-item">
<div class="masonry-content">
<img class="thumb" src="img/sort18.jpg" alt="image">
<h3 class="title">Items 18</h3>
<div class="desc">
<p>If a foolish person considers themselves to be foolish, that person is wise.</p>
<p>If you hesitate to do good, your heart will find pleasure in evil.</p>
</div>
</div>
</div>
<div class="masonry-item me shuffled-item">
<div class="masonry-content">
<img class="thumb" src="img/sort19.jpg" alt="image">
<h3 class="title">Items 19</h3>
<div class="desc">
<p>If a foolish person considers themselves to be foolish, that person is wise.</p>
<p>If you hesitate to do good, your heart will find pleasure in evil.</p>
</div>
</div>
</div>
<div class="masonry-item me shuffled-item">
<div class="masonry-content">
<img class="thumb" src="img/sort20.jpg" alt="image">
<h3 class="title">Items 20</h3>
<div class="desc">
<p>If a foolish person considers themselves to be foolish, that person is wise.</p>
<p>If you hesitate to do good, your heart will find pleasure in evil.</p>
</div>
</div>
</div>
<div class="masonry-item me shuffled-item">
<div class="masonry-content">
<img class="thumb" src="img/sort21.jpg" alt="image">
<h3 class="title">Items 21</h3>
<div class="desc">
<p>If a foolish person considers themselves to be foolish, that person is wise.</p>
<p>If you hesitate to do good, your heart will find pleasure in evil.</p>
</div>
</div>
</div>
<div class="masonry-item me shuffled-item illust">
<div class="masonry-content">
<img class="thumb" src="img/illust_04.webp" alt="illust D me">
</div>
</div>
<div class="masonry-item illast fixed-item">
<div class="masonry-content">
<img class="thumb" src="img/illust_01.webp" alt="illust A">
</div>
</div>
<div class="masonry-item illast fixed-item">
<div class="masonry-content">
<img class="thumb" src="img/illust_02.webp" alt="illust B">
</div>
</div>
<div class="masonry-item illast fixed-item">
<div class="masonry-content">
<img class="thumb" src="img/illust_03.webp" alt="illust C">
</div>
</div>
<div class="masonry-item illast fixed-item">
<div class="masonry-content">
<img class="thumb" src="img/illust_04.webp" alt="illust D">
</div>
</div>
</div>
</div>
ボタンの役割があるメニューに、あまり見ないコードがあります。
・2〜7行目の<li>にあるdata-filter=”●●●●●●”の記述
<li id="friends-button" class="tab-menu js-tab" data-filter="friends">FRIENDS</li>
data-filter=”●●●●●●”のボタンを押したら、●●●●●●と共通のclassがあるアイテム要素が表示させるようにclass指定されています。
<div class="masonry-item friends shuffled-item">
css
.nav-box {
text-align: center;
margin-top: 20px;
}
.nav-box ul{
margin: 0;
padding: 0;
display: flex;
}
.nav-box li{
list-style: none;
text-decoration: none;
background-color: #ccc;
color: #333;
cursor: pointer;
display: inline-block;
pointer-events: visible;
width: 100%;
padding: 10px 0;
}
.nav-box li:not(:last-child){
border-right:1px solid #a1a1a1;
}
.nav-box li.current{
color: #fff;
background-color: #000;
cursor: default;
pointer-events: none;
}
.nav-box li:hover{
color:#fff;
background-color: #000;
}
.masonry-wrapper {
padding: 1rem 0 0;
max-width: 1200px;
margin-right: auto;
margin-left: auto;
}
.masonry {
display: grid;
grid-template-columns: repeat(3, 1fr);
grid-auto-rows: 10px;
gap: 20px;
}
@media only screen and (min-width: 460px) {
.masonry {
grid-template-columns: repeat(2, 1fr);
}
}
@media only screen and (min-width: 768px) {
.masonry {
grid-template-columns: repeat(2, 1fr);
}
}
@media only screen and (min-width: 1024px) {
.masonry {
grid-template-columns: repeat(3, 1fr);
}
}
@media only screen and (min-width: 1240px) {
.masonry {
grid-template-columns: repeat(3, 1fr);
}
}
@media (width < 459px) {
.nav-box ul{
margin: 0;
padding: 0;
display: flex;
flex-wrap: wrap;
}
.nav-box li{
list-style: none;
text-decoration: none;
background-color: #ccc;
color: #333;
cursor: pointer;
display: inline-block;
pointer-events: visible;
width: calc(50% - 1px);
padding: 10px 0;
}
.nav-box li:not(:last-child){
border-right: unset;
}
.nav-box li {
border-top:1px solid #a1a1a1;
}
.nav-box li:nth-child(odd){
border-right:1px solid #a1a1a1;
}
.masonry {
grid-template-columns: repeat(2, 1fr);
}
}
.masonry-content {
position: relative;
}
.friends .masonry-content::before {
content: "Friends";
position: absolute;
top: 0;
left: 0;
width: auto;
height: auto;
padding: 0 5px;
color: #fff;
background: #d62d2d;
z-index: 1;
}
.partner .masonry-content::before {
content: "Partner";
position: absolute;
top: 0;
left: 0;
width: auto;
height: auto;
padding: 0 5px;
color: #fff;
background: #006a1e;
z-index: 1;
}
.family .masonry-content::before {
content: "Family";
position: absolute;
top: 0;
left: 0;
width: auto;
height: auto;
padding: 0 5px;
color: #fff;
background: #001ae5;
z-index: 1;
}
.me .masonry-content::before {
content: "Me";
position: absolute;
top: 0;
left: 0;
width: auto;
height: auto;
padding: 0 5px;
color: #fff;
background: #d68b00;
z-index: 1;
}
.masonry-item {
background-color: #efefef;
}
.masonry .thumb {
max-width: 100%;
}
.masonry .title {
padding: 0 1rem;
margin: 1em 0;
font-size: 15px;
font-weight: normal;
color: #000;
text-align: left;
}
.masonry .desc {
padding: 0 1rem;
position: relative;
overflow: visible;
color: #000;
}
.masonry .desc p {
font-size: 14px;
line-height: 1.4;
margin: 1em 0;
color: #000;
}
.masonry .hide {
opacity: 0;
}
.masonry .show {
animation: anime .6s;
animation-delay: 0s;
}
@keyframes anime {
0% {
opacity: 0;
}
50% {
opacity: 0;
}
60% {
opacity: .5;
}
100% {
opacity: 1;
}
}
表示のためのcssがほとんどですが、以下の黄色の記述は必要です。
.masonry {
display: grid;
grid-template-columns: repeat(3, 1fr);
grid-auto-rows: 10px;
gap: 20px;
}
以下のスクリプト(36行目のrowHeight)で高さを拾うために使います。
※cssに無いと数値がNaNと返されます。
javascript(jQuery含)
<script>
$(function () {
$(document).ready(function () {
// 初期読み込み時の不要イラスト
$("#list-cont .masonry-item.illust").hide().removeClass("show hide").addClass("hide");
// タブメニューのクリックイベントハンドラ
$(".js-tab").on("click", function () {
const filter = $(this).data("filter");
// クリックされたタブに "current" クラスを追加し、他のタブから削除
$(this).addClass("current").siblings().removeClass("current");
// メニュータブに対応するクラスを持つ要素を表示し、他の要素を非表示にする
if (filter === "all") {
$("#list-cont .masonry-item").show().removeClass("show hide").addClass("show");
$("#list-cont .masonry-item.illust").hide().removeClass("show hide").addClass("hide");
} else {
$("#list-cont .masonry-item").hide().removeClass("show hide").addClass("hide");
$(`.masonry-item.${filter}`).show().removeClass("show hide").addClass("show");
}
//resizeしても、ボタンを押して高さが欲しい
resizeAllGridItems();
});
// 初期表示を設定
$(".shuffle-button").trigger("click");
});
//-------masonry---------------------------------------
// グリッドコンテナを取得
const $grid = $('.masonry');
// 全てのグリッドアイテムを取得
const $allItems = $('.masonry-item');
// グリッドコンテナの grid-auto-rows の値を取得
let rowHeight = parseInt($grid.css('grid-auto-rows'));
// グリッドコンテナの grid-row-gap の値を取得
let rowGap = parseInt($grid.css('grid-row-gap'));
// デバッグ用のログ
console.log('rowHeight:', rowHeight);
console.log('rowGap:', rowGap);
// グリッドアイテムの grid-row-end プロパティを更新(設定)する関数
const resizeGridItem = (item) => {
const masonryContent = item.querySelector('.masonry-content');
if (masonryContent) {
// grid-row-end の span に指定する値を算出
const itemHeight = masonryContent.getBoundingClientRect().height;
const rowSpan = Math.ceil((itemHeight + rowGap) / (rowHeight + rowGap));
// デバッグ用のログ
console.log('itemHeight:', itemHeight);
console.log('rowSpan:', rowSpan);
// グリッドアイテムの grid-row-end プロパティを更新(設定)
$(item).css('grid-row-end', 'span ' + rowSpan);
}
}
// 全てのアイテムの grid-row-end プロパティを更新する関数
const resizeAllGridItems = () => {
$allItems.each(function () {
resizeGridItem(this);
});
}
// リサイズ時に全てのアイテムの grid-row-end プロパティを更新
let timer = false;
$(window).on('resize', function () {
if (timer !== false) {
clearTimeout(timer);
}
timer = setTimeout(function () {
// grid-auto-rowsとgrid-row-gapを再取得してリサイズ
rowHeight = parseInt($grid.css('grid-auto-rows'));
rowGap = parseInt($grid.css('grid-row-gap'));
// デバッグ用のログ
console.log('Resized: rowHeight:', rowHeight, 'rowGap:', rowGap);
resizeAllGridItems();
}, 200);
});
resizeAllGridItems();
// ロード時に全てのアイテムの grid-row-end プロパティを設定
$(window).on('load', function () {
resizeAllGridItems();
});
//-------イラスト固定シャッフル---------------------------------------
const fixedBoxPositions = [5, 10, 15, 20]; // 固定ボックスの位置を指定
function shuffleBoxes() {
const container = $("#list-cont");
const items = container.children(".shuffled-item");
// 固定ボックスを取得
const fixedItems = container.children(".fixed-item");
const fixedItemsArray = fixedItems.toArray();
// ランダムに並び替える要素を取得
const shuffledItems = items.toArray();
// 固定ボックスは位置を保持しつつ、シャッフル対象から削除
fixedItems.each(function() {
container.append($(this));
shuffledItems.splice(shuffledItems.indexOf($(this)[0]), 1);
});
// ランダムにシャッフル
shuffleArray(shuffledItems);
// シャッフル対象要素を元の位置に戻す
for (let i = 0; i < shuffledItems.length; i++) {
const position = i + 1;
if (position <= container.children().length) {
container.children().eq(position - 1).before(shuffledItems[i]);
} else {
container.append(shuffledItems[i]);
}
}
// 固定ボックスを元の位置に戻す
for (let i = 0; i < fixedItemsArray.length; i++) {
const position = fixedBoxPositions[i];
if (position <= container.children().length) {
container.children().eq(position - 1).before(fixedItemsArray[i]);
} else {
container.append(fixedItemsArray[i]);
}
}
}
// シャッフル関数
function shuffleArray(array) {
for (let i = array.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
// 要素の順序を交換
const temp = array[i];
array[i] = array[j];
array[j] = temp;
}
}
// シャッフルボタンがクリックされたときにシャッフル関数を呼び出す
$(".shuffle-button").on("click", function () {
shuffleBoxes();
});
// 初期表示を設定
$(".shuffle-button").trigger("click");
});// End function --jquery
</script>
長いです。すみません。
解説はchatGPTさんに放り込んで聞いてください。
【課題】タイル要素の高さ指定に難あります
スクリプト50行目の計算で、rowSpanを求めます。
const rowSpan = Math.ceil((itemHeight + rowGap) / (rowHeight + rowGap));
$(item).css('grid-row-end', 'span ' + rowSpan);
$(item).cssで各要素に「grid-row-end」のcssがstyleで出力されます。
grid-row-end: span 数値; のcssは 、1つ数が違うと差が激しいのがわかりました。

テキスト下の間隔が不揃いなのが気になる人は気になると思います。
微調整が難しく今回は放置しましたが、違う良い方法はないでしょうか。
AIさんにひつこく質問していると「masonry」を使うように促されます。
※「しつこい」は標準語、「ひつこい」は関西圏の方言
【オプション】アイテム要素に関係ないイラスト要素
共有コードには、「アイテム要素に関係ないイラスト要素」が入っています。
ほとんどの場合その要素は必要ないでしょう。

使用しない場合は、HTMLからイラスト要素を削除してください。
HTMLからの削除のみで、スクリプトの改修は必要ないです。
HTML(削除対象の要素は2種類です)
・以下のクラス(shuffled-item illust)のあるイラスト要素、4種あり。
<div class="masonry-item friends shuffled-item illust">
<div class="masonry-content">
<img class="thumb" src="img/illust_01.webp" alt="illust A friends">
</div>
</div>
※shuffled-item illustは、ソートした後に表示されるイラスト要素です。
全all表示の時は非表示になっています。
・以下のクラス(fixed-item)を丸ごと。
<div class="masonry-item illast fixed-item">
<div class="masonry-content">
<img class="thumb" src="img/illust_01.webp" alt="illust A">
</div>
</div>
<div class="masonry-item illast fixed-item">
<div class="masonry-content">
<img class="thumb" src="img/illust_02.webp" alt="illust B">
</div>
</div>
<div class="masonry-item illast fixed-item">
<div class="masonry-content">
<img class="thumb" src="img/illust_03.webp" alt="illust C">
</div>
</div>
<div class="masonry-item illast fixed-item">
<div class="masonry-content">
<img class="thumb" src="img/illust_04.webp" alt="illust D">
</div>
</div>
※fixed-itemは、ランダムで全all表示されるときに出るイラスト要素です。
ソート時は非表示になっています。
※スクリプトの95行目
const fixedBoxPositions = [5, 10, 15, 20];
に関連づけられ、ランダム順番シャッフルが発動したとしても、5番目にあたりに配置、10番目に15番目にと、5つごとに4種は固定fixedでいてほしい。
そのためのコードです。
なぜ同じイラスト要素のコードが2つあるのですか?
何回もランダム順番シャッフルしていると「イラストが隣り合う」時が出てくるのが気になるという事でした。
ランダムなのでイラスト数が多くなるほど、隣り合う確率は増えるでしょう。
ランダムなのに固定したいという矛盾。
意味がわからない後出しの要件で焦り、急遽増設しました。
参考資料サイト置き場
参考にさせてもらいました。ありがとうございます。
Masonry
https://masonry.desandro.com/
Web Design Leaves
https://www.webdesignleaves.com/pr/plugins/css-grid-masonry.html
Sitest
https://sitest.jp/blog/?p=11921
driveway
https://jh3y.github.io/driveway/
※Masonryと同じような
星間旅路のメロディ
「宇宙の静けさに包まれながら、漂流する過去の音楽を捜し求め、銀河の奥底でその旋律に耳を傾ける。」
「この電波はどこの星からきたのだろうか。」