🏄‍♂️

レスポンシブデザインで Tailwind CSS と CSS custom properties を併用する体験が良過ぎる

2023/04/28に公開

Tailwind CSS での開発体験が個人的に最高すぎて、最近はどんなWebサイトの実装でも利用しています。
CSS custom properties を併用することもあり、そのときに嬉しいことを紹介します。
Next.js を使っている前提でコード・ファイル名を例示しますが、他でも扱えると思います。

CSS custom properties 併用したい場面

  • スマートフォンでは、コンテンツを端から 16px の位置に置く
  • パソコンでは、コンテンツ幅を 960px にし中央寄せにする

こんなデザイン仕様、レスポンシブデザインだとよく出きます。
tailwind.config.js でテーマを拡張して実装すると以下のようになります。

tailwind.config.ts
import type { Config } from 'tailwindcss';

export default {
  theme: {
    extend: {
      spacing: {
        'content-width-sp': 'calc(100% - 32px)',
        'content-side-width-sp': '16px',
        'content-width-pc': '960px',
        'content-side-width-pc': 'calc((100% - 960px) / 2)',
      },
    },
  },
} satisfies Config;
Section.tsx
export const Section: React.FC<{ children?: React.ReactNode }> = ({ children }) => {
  return <section className="px-content-side-width-sp md:px-content-side-width-pc ...">{children}</section>
};
Dialog.tsx
export const Dialog: React.FC<{ children?: React.ReactNode }> = ({ children }) => {
  return <dialog className="w-content-width-sp md:w-content-width-pc ...">{children}</dialog>
};

メディアクエリを伴ったテーマの拡張はできません。
利用ごとに「スマホでは、PCでは、」と書く必要があり、面倒くさいです。
そこで CSS custom properties の出番です。

CSS custom properties で書き直し

tailwind.config.ts
import type { Config } from 'tailwindcss';

export default {
  theme: {
    extend: {
      spacing: {
        'content-width': 'var(--content-width)',
        'content-side-width': 'var(--content-side-width)',
      },
    },
  },
} satisfies Config;
global.css
@tailwind ...;

:root {
  --content-width: calc(100% - var(--content-side-width) * 2);
  --content-side-width: 16px;

  @media screen(md) {
    --content-width: 960px;
    --content-side-width: calc((100% - var(--content-width)) / 2);
  }
}
Section.tsx
export const Section: React.FC<{ children?: React.ReactNode }> = ({ children }) => {
  return <section className="px-content-side-width ...">{children}</section>
};
Dialog.tsx
export const Dialog: React.FC<{ children?: React.ReactNode }> = ({ children }) => {
  return <dialog className="w-content-width ...">{children}</dialog>
};

メディアクエリによって、デバイス幅ごとに CSS custom properties が変わります。
これで、class 名を二重で書く必要はなくなりました。

ただ、見慣れないコード @media screen(md) が気になるかと思います。
これこそ、良い体験を作っている Tailwind CSS の機能です。

Tailwind CSS の嬉しい機能

CSS ファイルで使える関数 screen() が用意されています。
https://tailwindcss.com/docs/functions-and-directives#screen

これで、レスポンシブデザインのブレークポイントを CSS 側で二重に宣言する必要もなく、テーマで定義しているスクリーン名をそのままメディアクエリに変換してくれます。
テーマのスクリーンを拡張、上書きしている場合でも、クラス名と同じスクリーン名を利用できるわけです。

またもう一つの関数に theme() が存在します。
例示した 16px も、テーマで定義した単位で扱うこともできます。
デフォルトのテーマで mt-4margin-top: 16px となる感じですね。

global.css
:root {
  --content-width: calc(100% - var(--content-side-width) * 2);
  --content-side-width: theme(spacing.4);
}

これらの関数は、PostCSS で変換されるようになっています。
Tailwind CSS 専用の PostCSS プラグインという認識でよさそうです。
https://github.com/tailwindlabs/tailwindcss/blob/v3.3.2/src/lib/evaluateTailwindFunctions.js#L200-L269

はまった点と解決

最初 @tailwind ... などを記述している、グローバルな css を global.scss という scss ファイルで管理していました。
ただこれだと、scss から css にビルドする段階で、@media screen() の変換ができず、エラーが出てしまいます。global.css にして解決しました。
css 単体でネストしたスタイルの記述もできるようになりましたし、拡張子変えるだけで基本オッケーのはずです。

追記 (2023.5.1)

https://tailwindcss.com/blog/tailwindcss-v3-3#esm-and-typescript-support
Tailwind CSS v3.3 より、設定ファイルが ESM, TypeScript で記述できるようになったようです。
せっかくなので、tailwind.config.ts にしたコードに変更しました。

Discussion