ライブデモ Live Demo
「もっと見る」ボタンをクリックして次のバッチをフェードインで追加表示します。
Click "Load More" to reveal the next batch of items with a fade-in animation.
すべて表示しました All items loaded
AI向け説明 AI Description
`I-011` はリスト / グリッドに「もっと見る」ボタンを追加し、クリックで次バッチをフェードインで追加表示するUIパターンです。全アイテムを最初から DOM に配置し、`visibleCount` 変数で表示数を管理します。ボタンクリック時は `loading` クラスでスピナーを表示してフェッチを模倣し、一定時間後に次バッチのアイテムを表示して `is-new` クラスで `fadeSlideUp` アニメーションを適用します。残件数をボタンテキストに表示し、全件表示後はボタンを非表示にして「すべて表示しました」メッセージを出します。`--item-fade-duration` CSS変数でアニメーション速度、`--load-btn-bg` / `--load-btn-radius` でボタン外観を制御します。
`I-011` adds a "Load More" button to a list or grid, revealing the next batch with a fade-slide-up animation on each click. All items live in the DOM from the start; `visibleCount` tracks how many are displayed. On click, a `loading` class triggers a spinner to simulate a fetch, then newly revealed items receive the `is-new` class which applies a `fadeSlideUp` keyframe animation. The remaining count is shown in the button text, and once all items are visible the button is replaced by an "All items loaded" message. Button appearance is controlled via `--load-btn-bg`, `--load-btn-color`, `--load-btn-radius`, and animation speed via `--item-fade-duration`.
調整可能パラメータ Adjustable Parameters
- --load-btn-bg — ボタンの背景色Button background color
- --load-btn-color — ボタンのテキスト色Button text color
- --load-btn-radius — ボタンの角丸サイズButton border radius
- --item-fade-duration — 新アイテムのフェードイン速度Fade-in duration for newly revealed items
- initialCount — 初期表示件数(デフォルト: 4)Number of items shown initially (default: 4)
- batchSize — 1クリックで追加表示する件数(デフォルト: 4)Items to reveal per click (default: 4)
- loadDelay — スピナー表示時間(デフォルト: 500ms)Spinner display duration before revealing items (default: 500ms)
実装 Implementation
HTML + CSS + JS
<!-- (1) Items container: place all items here, hidden by default -->
<div class="items-grid" id="items_grid">
<div class="item-card">Item 1</div>
<div class="item-card">Item 2</div>
<div class="item-card">Item 3</div>
<!-- ... more items ... -->
</div>
<!-- (2) Load More button -->
<div class="load-more-wrap" id="load_wrap">
<button class="load-more-btn" id="load_btn" type="button">
<span class="spinner"></span>
Load More (<span id="rem_count">0</span>)
</button>
</div>
<p class="loaded-msg" id="loaded_msg">All items loaded</p>
<style>
:root {
--load-btn-bg: #111827;
--load-btn-color: #ffffff;
--load-btn-radius: 8px;
--item-fade-duration: 0.4s;
}
/* Grid */
.items-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
gap: 16px;
}
.item-card {
background: #fff;
border: 1px solid #e5e7eb;
border-radius: 10px;
padding: 20px;
}
/* Fade-in for newly revealed items */
.item-card.is-new {
animation: fadeSlideUp var(--item-fade-duration) ease both;
}
@keyframes fadeSlideUp {
from { opacity: 0; transform: translateY(14px); }
to { opacity: 1; transform: translateY(0); }
}
/* Load More button */
.load-more-wrap {
display: flex;
justify-content: center;
padding: 32px 0 8px;
}
.load-more-btn {
display: inline-flex;
align-items: center;
justify-content: center;
gap: 8px;
padding: 12px 32px;
background: var(--load-btn-bg);
color: var(--load-btn-color);
border: none;
border-radius: var(--load-btn-radius);
font-size: 15px;
font-weight: 600;
cursor: pointer;
min-width: 180px;
transition: opacity 0.2s, transform 0.15s;
}
.load-more-btn:hover { opacity: 0.82; }
.load-more-btn:active { transform: scale(0.97); }
.load-more-btn.loading { opacity: 0.65; cursor: wait; }
/* Spinner */
.spinner {
display: none;
width: 14px; height: 14px;
border: 2px solid rgba(255,255,255,0.3);
border-top-color: #fff;
border-radius: 50%;
animation: spin 0.6s linear infinite;
}
.load-more-btn.loading .spinner { display: block; }
@keyframes spin { to { transform: rotate(360deg); } }
/* All loaded message */
.loaded-msg { display: none; text-align: center; color: #6b7280; }
</style>
<script>
(function () {
var INITIAL_COUNT = 4; // items shown on load
var BATCH_SIZE = 4; // items added per click
var LOAD_DELAY = 500; // spinner duration (ms)
var grid = document.getElementById('items_grid');
var wrap = document.getElementById('load_wrap');
var btn = document.getElementById('load_btn');
var remSpan = document.getElementById('rem_count');
var doneMsg = document.getElementById('loaded_msg');
var items = Array.from(grid.querySelectorAll('.item-card'));
var visible = 0;
// Hide all, then show initial batch
items.forEach(function (el) { el.style.display = 'none'; });
showBatch(INITIAL_COUNT, false);
function showBatch(count, animate) {
var shown = 0;
for (var i = visible; i < items.length && shown < count; i++, shown++) {
items[i].style.display = '';
if (animate) {
items[i].classList.remove('is-new');
// Force reflow to restart animation
void items[i].offsetWidth;
items[i].classList.add('is-new');
}
}
visible += shown;
updateUI();
}
function updateUI() {
var remaining = items.length - visible;
if (remaining > 0) {
wrap.style.display = '';
remSpan.textContent = remaining;
doneMsg.style.display = 'none';
} else {
wrap.style.display = 'none';
doneMsg.style.display = 'block';
}
}
btn.addEventListener('click', function () {
if (btn.classList.contains('loading')) return;
btn.classList.add('loading');
setTimeout(function () {
btn.classList.remove('loading');
showBatch(BATCH_SIZE, true);
}, LOAD_DELAY);
});
})();
</script>
React (JSX + CSS)
// react/I-011.jsx
import { useState, useRef } from "react";
import "./I-011.css";
// Sample data — replace with your own array of items
const ALL_ITEMS = Array.from({ length: 12 }, (_, i) => ({
id: i + 1,
title: `Item ${i + 1}`,
description: "Short description goes here.",
}));
export default function LoadMoreList({
items = ALL_ITEMS,
initialCount = 4,
batchSize = 4,
loadDelay = 500,
}) {
const [visible, setVisible] = useState(initialCount);
const [loading, setLoading] = useState(false);
const newRefs = useRef([]);
const shownItems = items.slice(0, visible);
const remaining = items.length - visible;
const allLoaded = remaining <= 0;
function handleLoadMore() {
if (loading) return;
setLoading(true);
setTimeout(() => {
setLoading(false);
setVisible((v) => Math.min(v + batchSize, items.length));
}, loadDelay);
}
return (
<div>
<div className="items-grid">
{shownItems.map((item, i) => (
<div
key={item.id}
className={`item-card${i >= visible - batchSize ? " is-new" : ""}`}
>
<h3>{item.title}</h3>
<p>{item.description}</p>
</div>
))}
</div>
<div className="load-more-wrap">
{!allLoaded ? (
<button
className={`load-more-btn${loading ? " loading" : ""}`}
onClick={handleLoadMore}
>
{loading && <span className="spinner" />}
Load More ({remaining})
</button>
) : (
<p className="loaded-msg">All items loaded</p>
)}
</div>
</div>
);
}
/* react/I-011.css */
:root {
--load-btn-bg: #111827;
--load-btn-color: #ffffff;
--load-btn-radius: 8px;
--item-fade-duration: 0.4s;
}
.items-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
gap: 16px;
}
.item-card {
background: #1e1e2e;
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: 10px;
padding: 20px;
}
.item-card.is-new {
animation: fadeSlideUp var(--item-fade-duration) ease both;
}
@keyframes fadeSlideUp {
from { opacity: 0; transform: translateY(14px); }
to { opacity: 1; transform: translateY(0); }
}
.load-more-wrap {
display: flex;
justify-content: center;
padding: 32px 0 8px;
}
.load-more-btn {
display: inline-flex;
align-items: center;
justify-content: center;
gap: 8px;
padding: 12px 32px;
background: var(--load-btn-bg);
color: var(--load-btn-color);
border: none;
border-radius: var(--load-btn-radius);
font-size: 15px;
font-weight: 600;
font-family: inherit;
cursor: pointer;
min-width: 180px;
transition: opacity 0.2s, transform 0.15s;
}
.load-more-btn:hover { opacity: 0.82; }
.load-more-btn:active { transform: scale(0.97); }
.load-more-btn.loading { opacity: 0.65; cursor: wait; }
.spinner {
display: inline-block;
width: 14px; height: 14px;
border: 2px solid rgba(255, 255, 255, 0.3);
border-top-color: #fff;
border-radius: 50%;
animation: spin 0.6s linear infinite;
}
@keyframes spin { to { transform: rotate(360deg); } }
.loaded-msg { text-align: center; color: #6b7280; font-size: 13px; }
AIへの指示テンプレート AI Prompt Template
以下のテンプレートをコピーしてAIアシスタントに貼り付けると、このパターンの実装を依頼できます。 Copy the template below and paste it into your AI assistant to request an implementation of this pattern.
注意とバリエーション Notes & Variations
- 実際のAPIと組み合わせる場合: `loadDelay` を削除し、`fetch()` の Promise 解決後に `showBatch()` を呼ぶことで同じ UX が実現できます。 With a real API: remove the `loadDelay` timeout and call `showBatch()` after the `fetch()` Promise resolves to achieve the same UX.
- 無限スクロールへの転換: `loadMoreBtn.click()` を `IntersectionObserver` でビューポートに入ったときにトリガーするだけで Infinite Scroll に変換できます。 Switching to Infinite Scroll: trigger `loadMoreBtn.click()` via an `IntersectionObserver` when the sentinel element enters the viewport to convert it into infinite scroll.
- スタガーアニメーション: `is-new` クラスを付与する際に各アイテムに `animation-delay` を設定すると、バッチ内で順番にフェードインする効果が得られます。 Staggered animation: set incremental `animation-delay` on each item when adding the `is-new` class to achieve a staggered fade-in within each batch.
- アクセシビリティ: ボタンに `aria-live="polite"` リージョンを関連付けるか、追加後に追加件数を `aria-live` で通知することでスクリーンリーダー対応が向上します。 Accessibility: associate an `aria-live="polite"` region with the button, or announce the newly added count after reveal, to improve screen reader support.