目次
ブログやニュースサイトなど、大量のコンテンツを扱うWebアプリケーションでは、ページネーションは必須の機能です。この記事では、DaisyUIのjoinコンポーネントを活用して、美しく機能的な静的ページネーションを実装する方法を詳しく解説します。
DaisyUIのjoinコンポーネントとは
DaisyUIのjoinコンポーネントは、複数の要素を視覚的に結合するためのコンポーネントです。ページネーションのようなナビゲーション要素を作成するのに最適です。
主な特徴:
- 統一感のあるデザイン: 隣接する要素が自然に結合される
- レスポンシブ対応: モバイルでも適切に表示される
- アクセシビリティ: スクリーンリーダーに優しい構造
- カスタマイズ可能: Tailwind CSSクラスで柔軟にスタイル調整
基本的なページネーション実装
1ページ目用のコンポーネント
// pages/recent.tsx
import Link from 'next/link'
type Props = {
posts: Post[]
totalPages: number
}
export default function RecentPage({ posts, totalPages }: Props) {
return (
<Layout>
<Container>
<div className="flex flex-col max-w-6xl mx-auto px-10 md:px-0">
<main>
<RecentPost posts={posts} />
</main>
{/* DaisyUIページネーション */}
{totalPages > 1 && (
<nav className="flex justify-center pt-5 mx-auto max-w-4xl">
<div className="join">
<span className="join-item btn btn-active">1</span>
{totalPages > 1 && (
<Link href="/recent/2" className="join-item btn">
»
</Link>
)}
</div>
</nav>
)}
</div>
</Container>
</Layout>
)
}
2ページ目以降用のコンポーネント
// pages/recent/[page].tsx
import { GetStaticProps, GetStaticPaths } from 'next'
import Link from 'next/link'
type Props = {
posts: Post[]
currentPage: number
totalPages: number
}
export default function RecentPageWithPagination({ posts, currentPage, totalPages }: Props) {
return (
<Layout>
<Container>
<div className="flex flex-col max-w-6xl mx-auto px-10 md:px-0">
<main>
<RecentPost posts={posts} />
</main>
{/* 静的ページネーション */}
<nav className="flex justify-center pt-5 mx-auto max-w-4xl">
<div className="join">
{currentPage > 1 && (
<Link
href={currentPage === 2 ? '/recent' : `/recent/${currentPage - 1}`}
className="join-item btn"
>
«
</Link>
)}
<span className="join-item btn btn-active">
{currentPage}
</span>
{currentPage < totalPages && (
<Link href={`/recent/${currentPage + 1}`} className="join-item btn">
»
</Link>
)}
</div>
</nav>
</div>
</Container>
</Layout>
)
}
高度なページネーション実装
数字ページング
複数のページ番号を表示する場合:
const PaginationWithNumbers = ({ currentPage, totalPages, basePath }: Props) => {
const generatePageNumbers = () => {
const pages = []
const showPages = 5 // 表示するページ数
let startPage = Math.max(1, currentPage - Math.floor(showPages / 2))
let endPage = Math.min(totalPages, startPage + showPages - 1)
// 最初に調整
if (endPage - startPage + 1 < showPages) {
startPage = Math.max(1, endPage - showPages + 1)
}
for (let i = startPage; i <= endPage; i++) {
pages.push(i)
}
return pages
}
return (
<nav className="flex justify-center pt-5 mx-auto max-w-4xl">
<div className="join">
{/* 前へボタン */}
{currentPage > 1 && (
<Link
href={currentPage === 2 ? basePath : `${basePath}/${currentPage - 1}`}
className="join-item btn"
>
«
</Link>
)}
{/* 最初のページ */}
{generatePageNumbers()[0] > 1 && (
<>
<Link href={basePath} className="join-item btn">
1
</Link>
{generatePageNumbers()[0] > 2 && (
<span className="join-item btn btn-disabled">...</span>
)}
</>
)}
{/* ページ番号 */}
{generatePageNumbers().map(page => (
<Link
key={page}
href={page === 1 ? basePath : `${basePath}/${page}`}
className={`join-item btn ${page === currentPage ? 'btn-active' : ''}`}
>
{page}
</Link>
))}
{/* 最後のページ */}
{generatePageNumbers()[generatePageNumbers().length - 1] < totalPages && (
<>
{generatePageNumbers()[generatePageNumbers().length - 1] < totalPages - 1 && (
<span className="join-item btn btn-disabled">...</span>
)}
<Link href={`${basePath}/${totalPages}`} className="join-item btn">
{totalPages}
</Link>
</>
)}
{/* 次へボタン */}
{currentPage < totalPages && (
<Link href={`${basePath}/${currentPage + 1}`} className="join-item btn">
»
</Link>
)}
</div>
</nav>
)
}
レスポンシブデザイン対応
モバイルファーストなページネーション
const ResponsivePagination = ({ currentPage, totalPages, basePath }: Props) => {
return (
<nav className="flex justify-center pt-5 mx-auto max-w-4xl">
<div className="join">
{/* モバイル: シンプルな前後ボタンのみ */}
<div className="flex md:hidden">
{currentPage > 1 && (
<Link
href={currentPage === 2 ? basePath : `${basePath}/${currentPage - 1}`}
className="join-item btn btn-sm"
>
← 前
</Link>
)}
<span className="join-item btn btn-sm btn-active">
{currentPage} / {totalPages}
</span>
{currentPage < totalPages && (
<Link href={`${basePath}/${currentPage + 1}`} className="join-item btn btn-sm">
次 →
</Link>
)}
</div>
{/* デスクトップ: 完全なページネーション */}
<div className="hidden md:flex">
{currentPage > 1 && (
<Link
href={currentPage === 2 ? basePath : `${basePath}/${currentPage - 1}`}
className="join-item btn"
>
«
</Link>
)}
<span className="join-item btn btn-active">
{currentPage}
</span>
{currentPage < totalPages && (
<Link href={`${basePath}/${currentPage + 1}`} className="join-item btn">
»
</Link>
)}
</div>
</div>
</nav>
)
}
カスタムスタイリング
テーマに応じたスタイル調整
const ThemedPagination = ({ currentPage, totalPages, basePath }: Props) => {
return (
<nav className="flex justify-center pt-5 mx-auto max-w-4xl">
<div className="join shadow-lg">
{currentPage > 1 && (
<Link
href={currentPage === 2 ? basePath : `${basePath}/${currentPage - 1}`}
className="join-item btn btn-outline hover:btn-primary transition-all duration-200"
>
«
</Link>
)}
<span className="join-item btn btn-primary">
{currentPage}
</span>
{currentPage < totalPages && (
<Link
href={`${basePath}/${currentPage + 1}`}
className="join-item btn btn-outline hover:btn-primary transition-all duration-200"
>
»
</Link>
)}
</div>
</nav>
)
}
ダークモード対応
/* styles/globals.css */
[data-theme="dark"] .pagination {
--btn-text-case: none;
--rounded-btn: 0.5rem;
}
[data-theme="dark"] .join-item.btn {
background-color: hsl(var(--b2));
border-color: hsl(var(--b3));
color: hsl(var(--bc));
}
[data-theme="dark"] .join-item.btn:hover {
background-color: hsl(var(--b3));
border-color: hsl(var(--b3));
}
[data-theme="dark"] .join-item.btn.btn-active {
background-color: hsl(var(--p));
color: hsl(var(--pc));
border-color: hsl(var(--p));
}
アクセシビリティの考慮
WAI-ARIA属性の追加
const AccessiblePagination = ({ currentPage, totalPages, basePath }: Props) => {
return (
<nav
aria-label="ページネーション"
className="flex justify-center pt-5 mx-auto max-w-4xl"
>
<div className="join" role="group" aria-label="ページナビゲーション">
{currentPage > 1 && (
<Link
href={currentPage === 2 ? basePath : `${basePath}/${currentPage - 1}`}
className="join-item btn"
aria-label={`${currentPage - 1}ページに移動`}
>
«
</Link>
)}
<span
className="join-item btn btn-active"
aria-current="page"
aria-label={`現在のページ: ${currentPage}`}
>
{currentPage}
</span>
{currentPage < totalPages && (
<Link
href={`${basePath}/${currentPage + 1}`}
className="join-item btn"
aria-label={`${currentPage + 1}ページに移動`}
>
»
</Link>
)}
</div>
</nav>
)
}
パフォーマンス最適化
プリフェッチの活用
import Link from 'next/link'
const OptimizedPagination = ({ currentPage, totalPages, basePath }: Props) => {
return (
<nav className="flex justify-center pt-5 mx-auto max-w-4xl">
<div className="join">
{currentPage > 1 && (
<Link
href={currentPage === 2 ? basePath : `${basePath}/${currentPage - 1}`}
className="join-item btn"
prefetch={true} // 重要: プリフェッチを有効化
>
«
</Link>
)}
<span className="join-item btn btn-active">
{currentPage}
</span>
{currentPage < totalPages && (
<Link
href={`${basePath}/${currentPage + 1}`}
className="join-item btn"
prefetch={true} // 重要: プリフェッチを有効化
>
»
</Link>
)}
</div>
</nav>
)
}
まとめ
DaisyUIのjoinコンポーネントを使用することで、以下のメリットを持つページネーションを効率的に実装できます:
主な利点
- 一貫性のあるデザイン: DaisyUIのデザインシステムに準拠
- レスポンシブ対応: モバイルとデスクトップで最適化された表示
- アクセシビリティ: スクリーンリーダーに対応した構造
- カスタマイズ性: Tailwind CSSクラスで柔軟にスタイル調整
実装のポイント
- 適切なURL設計: 1ページ目と2ページ目以降の適切な分離
- レスポンシブ考慮: モバイルとデスクトップでの最適化
- アクセシビリティ: ARIA属性とセマンティックHTML
- パフォーマンス: Next.jsのプリフェッチ機能活用
DaisyUIの力を借りることで、美しく機能的なページネーションを短時間で実装できるでしょう。