CSS Modulesにおけるシンプルなレスポンシブ対応
本記事では、CSS ModulesとVite環境下におけるシンプルなレスポンシブの手法を紹介します。また、将来的に導入可能なコンテナスタイルクエリやif()関数を用いて、さらに記述を簡潔にできることにも触れます。
課題
レスポンシブ対応には、一般的にメディアクエリが使われます。しかし、ReactコンポーネントのCSS Modulesファイルごとにメディアクエリを書くと、768pxのようなブレークポイント定義が散在し、統一性が損なわれる恐れがあります。
.foo {
/* スマホ版の記述 */
}
@media (max-width: 768px) {
.foo {
/* タブレット版の記述 */
}
}
@media (max-width: 1280px) {
.foo {
/* PC版の記述 */
}
}
TypeScript側にも条件分岐が必要な場合、そちらでもブレークポイントの定義が必要になります。ブレークポイント定義を一元管理しつつ、簡潔に記述したいところです。
シナリオ
- React, TypeScript, Vite
- SPA
- CSS Modulesでコンポーネントにスタイルを適用する
-
import classes from 'foo.module.css';→className={classes.foo} - CSS Modules固有の拡張構文はなるべく使わない
-
- レスポンシブ対応
- ブレークポイントはアプリ全体で統一(コンポーネント毎に異なることはない)
- PC、タブレット、スマホの3種類
手法
- ブレークポイントクラス(
screen-pc,screen-tab,screen-sp)をグローバルセレクタとして定義する。 - ハッシュ化回避のため、
vite.config.tsのcss.modules.generateScopedNameをカスタマイズする。 - 画面サイズを判定し、ブレークポイントクラス名を返すカスタムフックを定義する。
- ルート要素などで使うラッパーコンポーネントを作成する。
- CSS Module内でブレークポイントクラス使ってスタイルを記述する。(例:
.screen-pc .foo {...})または、コンポーネント内でカスタムフックを使って条件分岐する。
1. ブレークポイントクラスの定義
レスポンシブ対応用のブレークポイントクラス(screen-pc, screen-tab, screen-sp)を決めます。
各ブレークポイントのグローバルなスタイル情報は、次のようにCSS変数で定義できます。
.screen-sp {
--spacing-small: 8px;
--spacing-large: 16px;
--font-size-body: 14px;
}
.screen-tab {
--spacing-small: 12px;
--spacing-large: 24px;
--font-size-body: 16px;
}
.screen-pc {
--spacing-small: 16px;
--spacing-large: 32px;
--font-size-body: 18px;
}
2. ブレークポイントクラスのハッシュ化除外
ブレークポイントクラスはグローバルクラスとして扱うため、CSS Modulesのハッシュ化から除外する必要があります。
そのため、vite.config.ts の css.modules.generateScopedName をカスタマイズします。
import crypto from 'node:crypto';
import { defineConfig } from 'vite';
export default defineConfig({
// (他の設定)
css: {
modules: {
generateScopedName: (name, filename, css) => {
// screen-pc, screen-tab, screen-sp はハッシュ化しない
if (/^screen-(pc|tab|sp)$/.test(name)) {
return name;
}
// 上記以外はハッシュ化する
// ここでは [name]_[hash:base64:6] のような形式は使えないので、自前でハッシュを生成する
const hash = crypto.createHash('sha1').update(`${name}${filename}${css}`).digest('hex').slice(0, 6);
return `${name}_${hash}`;
},
},
},
});
3. useResponsiveフック
画面サイズを判定し、対応するブレークポイントクラスを返すカスタムフックを作成します。ここではreact-responsiveを使っています。
768のような数値定義を、このフック一箇所に集約できます。
import { useMediaQuery } from 'react-responsive';
type ScreenType = 'pc' | 'tab' | 'sp';
type ScreenClassName = `screen-${ScreenType}`;
/**
* 画面サイズに応じてブレークポイントクラスを返すフック
*
* - **PC**: 1280px以上
* - **Tab**: 768px以上1280px未満
* - **SP**: 768px未満
*/
export function useResponsive(): { screenType: ScreenType; screenClassName: ScreenClassName } {
const isPc = useMediaQuery({ minWidth: 1280 });
const isTablet = useMediaQuery({ minWidth: 768 });
const screenType: ScreenType = isPc ? 'pc' : isTablet ? 'tab' : 'sp';
const screenClassName: ScreenClassName = `screen-${screenType}`;
return { screenType, screenClassName };
}
4. Responsiveコンポーネント
ブレークポイントクラスを付与するラッパーコンポーネントを作成します。
import { ReactNode } from 'react';
import { useResponsive } from '../hooks/useResponsive';
/**
* レスポンシブ対応のためのラッパー
*/
export function Responsive({ children }: { children: ReactNode }) {
const { screenClassName } = useResponsive();
return <div className={screenClassName}>{children}</div>;
}
アプリのルートを<Responsive>でラップします。これで祖先要素にクラスが付与され、各CSSファイルで参照可能になります。
import { RouterProvider, createRouter } from '@tanstack/react-router';
import { StrictMode } from 'react';
import { createRoot } from 'react-dom/client';
import { Responsive } from './components/Responsive';
import { routeTree } from './routeTree.gen';
import './styles/global.css';
const router = createRouter({ routeTree });
createRoot(document.getElementById('root')!).render(
<StrictMode>
<Responsive>
<RouterProvider router={router} />
</Responsive>
</StrictMode>,
);
5.コンポーネントの実装
定義したクラスとフックを使ったコンポーネントの実装例です。
CSS内でブレークポイントクラスを使用するパターン
レイアウトコンポーネントの例です。PC・タブレットは2カラム表示、スマホは1カラム表示にします。
CSS内でブレークポイントクラスを使用し、画面サイズごとに適用するスタイルを切り替えます。
import type { ReactNode } from 'react';
import classes from './PageLayout.module.css';
type Props = {
main: ReactNode;
sidebar: ReactNode;
};
/**
* レスポンシブ対応のレイアウトコンポーネント
*
* - PC: 2カラム(サイドバー幅400px)
* - タブレット: 2カラム(サイドバー幅200px)
* - スマホ: 1カラム(サイドバーの内容を下に配置)
*/
export function PageLayout({ main, sidebar }: Props) {
return (
<div className={classes.layout}>
<main className={classes.main}>{main}</main>
<aside className={classes.sidebar}>{sidebar}</aside>
</div>
);
}
.layout {
width: 100%;
display: grid;
/* 画面サイズによって値が変わります */
padding: var(--spacing-large);
gap: var(--spacing-small);
}
/* 画面サイズごとのレイアウト(定義漏れ防止のため全パターンでクラス名を記述しています) */
.screen-pc .layout {
grid-template-columns: 1fr 400px;
}
.screen-tab .layout {
grid-template-columns: 1fr 200px;
}
.screen-sp .layout {
grid-template-columns: 1fr;
}
コンポーネント内でuseResponsiveを使用するパターン
商品画像コンポーネントの例です。PC・タブレットはスライダー、スマホは縦並び表示にします。
画面サイズごとにレンダリングするコンポーネントを切り替えるため、コンポーネント内でuseResponsiveを使います。
import { useResponsive } from '../../hooks/useResponsive';
import { Slider } from './components/Slider';
import classes from './ImageGallery.module.css';
type Props = {
images: ReadonlyArray<{ id: string; url: string; alt: string }>;
};
/**
* 商品画像コンポーネント
*
* - PC・タブレット: スライダー
* - スマホ: 画像を縦に並べる
*/
export function ImageGallery({ images }: Props) {
const { screenType } = useResponsive();
// スマホの場合縦並び表示
if (screenType === 'sp') {
return (
<div className={classes['sp-list']}>
{images.map((img) => (
<img src={img.url} alt={img.alt} className={classes.image} key={img.id} />
))}
</div>
);
}
// PC・タブレットの場合スライダー表示
return <Slider images={images} />;
}
.sp-list {
display: grid;
grid-template-columns: 1fr;
gap: 4px;
}
.image {
width: 100%;
height: auto;
}
body直下に要素を追加するライブラリを使用する場合
モーダルなどbody直下に要素を生成するライブラリは、表示内容を別途<Responsive>でラップする必要があります。
import type { ReactNode } from 'react';
import { Modal, ModalOverlay, Dialog } from 'react-aria-components';
import { Responsive } from './components/Responsive';
type Props = {
isOpen: boolean;
onClose: () => void;
children: ReactNode;
};
export function MyModal({ isOpen, onClose, children }: Props) {
return (
<ModalOverlay isOpen={isOpen} onOpenChange={onClose}>
<Modal>
<Responsive> {/* ★ */}
<Dialog>{children}</Dialog>
</Responsive>
</Modal>
</ModalOverlay>
);
}
将来的な展望
コンテナスタイルクエリやif()関数を使うと、本記事の手法と同様のことがより簡潔に記述できます。
ブラウザ対応状況から採用を見送りましたが、普及したら本記事の手法よりもこれらを使うことを推奨します。
コンテナスタイルクエリを使った方法
親要素のスタイルに基いて、適用するスタイルを切り替える機能です。
下記は、先ほどのレイアウトコンポーネントをコンテナスタイルクエリを使って書き換えた例です。
Responsiveコンポーネントではグローバルクラスの代わりにCSS変数 --screen-type を定義します。
(略)
export function Responsive({ children }: { children: ReactNode }) {
const { screenType } = useResponsive();
return <div style={{ '--screen-type': screenType }}>{children}</div>
}
本記事の方法では.screen-pc .layoutのようにブレークポイントクラスを祖先要素として書く必要がありましたが、コンテナスタイルクエリでは.layout内に直接記述できます。
.layout {
/* (略) */
@container style(--screen-type: pc) {
grid-template-columns: 1fr 400px;
}
@container style(--screen-type: tab) {
grid-template-columns: 1fr 200px;
}
@container style(--screen-type: sp) {
grid-template-columns: 1fr;
}
}
2025年11月現在、Chrome/EdgeとSafariが対応していますが、Firefoxは未対応です。
- 'container style queries' | Can I use... Support tables for HTML5, CSS3, etc
- 参考: 親要素のスタイルの基づいてスタイルを適用するコンテナスタイルクエリー CSS | azukiazusa.dev
if()関数を使った方法
CSSの値定義内で条件分岐できる関数です。記述がさらに簡潔になります。
.layout {
(略)
grid-template-columns: if(
style(--screen-type: pc): 1fr 400px;
style(--screen-type: tab): 1fr 200px;
style(--screen-type: sp): 1fr;
);
}
2025年11月現在、Chrome/Edgeが対応していますが、SafariとFirefoxは未対応です。
- if() | Can I use... Support tables for HTML5, CSS3, etc
- 参考: インラインで条件分岐する CSS の if() 関数 | azukiazusa.dev
比較表
| 本記事の方法 | コンテナスタイルクエリ |
if()関数 |
|
|---|---|---|---|
| ハッシュ化からの除外処理 | 必要 | 不要 | 不要 |
| 条件の記述位置 | クラスを祖先セレクタとして使用 | ルールセット内でCSS変数を参照 | プロパティ値内でCSS変数を参照 |
| 記述の簡潔さ | やや煩雑 | 簡潔 | 非常に簡潔 |
| 2025/11現在の対応状況 | 全モダンブラウザが対応 | Firefox未対応 | SafariとFirefox未対応 |
Discussion