mizdra's blog

ぽよぐらみんぐ

似ているけどちょっと違うものをスタイリングする CSS テクニック

React + CSS Modules なプロジェクトを例にサンプルコードを書いてるけど、それ以外の構成のプロジェクトでも使えるテクニックのはず。記事タイトルは id:hitode909 さんの 似ているけどちょっと違うものたちをモデリングする技術 - hitode909 のリスペクトです。

data 属性, 属性セレクター, CSS カスタムプロパティを使う

記事のリストがあって、記事のカテゴリごとに文字色やボーダーの色を変えたい、みたいな状況だったとする。多分素朴に書くと以下のようになるはず。

import styles from './Entry.module.css';
import clsx from 'clsx'
function Entry(props) {
  return (
    <div className={
      clsx(props.category === '暮らし' ? styles.categoryLife
         : props.category === '政治' ? styles.categoryPolitics
         : props.category === '技術' ? styles.categoryTechnology
         : null)
    }>
      {/* ... */}
    </div>
  );
}
.categoryLife {
  color: red;
  border: 1px red solid;
}
.categoryPolitics {
  color: blue;
  border: 1px blue solid;
}
.categoryTechnology {
  color: yellow;
  border: 1px yellow solid;
}

ただ、これだと .categoryXxx { ... } ごとに color, border のプロパティの値を定義しないといけない。特に border1px ... solid の部分は、どのカテゴリでも同じにも関わらず、カテゴリごとに書かないといけない。

これではイマイチということで、id:mizdra はよく以下のように書いてる。

import styles from './Entry.module.css';
import clsx from 'clsx'
function Entry(props) {
  // ...
  return (
    <div
      className={styles.container}
      data-category={props.category}>
      {/* ... */}
    </div>
  );
}
.container {
  color: var(--category-color);
  border: 1px var(--category-color) solid;
}
.container[data-category="暮らし"] {
  --category-color: red;
}
.container[data-category="政治"] {
  --category-color: blue;
}
.container[data-category="技術"] {
  --category-color: yellow;
}

data 属性属性セレクターCSS カスタムプロパティ を使っているのがポイント。どれも 2016 年頃から全てのメジャーブラウザに実装されてる機能なので *1、ほとんどのサービスで利用できるはず。

自由度も結構高い。やろうと思えば、CSS カスタムプロパティは複数設定できるし、特定のカテゴリでだけ適用したいプロパティも定義できる。

.container {
  color: var(--category-text-color);
  border: 1px var(--category-border-color) solid;
}
.container[data-category="暮らし"] {
  /* テキスト用の色とボーダー用の色を別々に定義 */
  --category-text-color: red;
  --category-border-color: red;
  /* 「暮らし」カテゴリにだけ適用したいプロパティを定義 */
  border-radius: 30px;
}
.container[data-category="政治"] {
  --category-text-color: blue;
  --category-border-color: blue;
}
.container[data-category="技術"] {
  --category-text-color: yellow;
  --category-border-color: yellow;
}

コラム: CSS Nesting と組み合わせる

一応 CSS Nesting を使うと以下のようにも書ける。こちらは去年メジャーブラウザに実装されたばかりの機能なので、古いブラウザでも動かしたい場合は postcss でトランスパイルする必要がある。

.container {
  color: var(--category-color);
  border: 1px var(--category-color) solid;

  &[data-category="暮らし"] {
    --category-color: red;
  }
  &[data-category="政治"] {
    --category-color: blue;
  }
  &[data-category="技術"] {
    --category-color: yellow;
  }
}

style 属性を使って CSS カスタムプロパティを定義する

カテゴリの数が有限、つまり決まった数しかないなら上記の方法で問題ない。しかしユーザがカテゴリを自由に増やせてカテゴリカラーも好きに選べる場合は、上記の方法だけでは上手くいかない。

そういう時はどうするかというと、バックエンドからカテゴリカラーをフロントエンドに渡せるようにした上で、カテゴリカラー用の CSS カスタムプロパティを style 属性で定義すれば良い。

import styles from './Entry.module.css';
import clsx from 'clsx'
function Entry(props) {
  // props.categoryColor にはバックエンドから引いた色の情報が文字列で入っている (例`"#FF0000"`)
  return (
    <div
      className={styles.container}
      style={{ '--category-color': props.categoryColor }}>
      {/* ... */}
    </div>
  );
}
.container {
  color: var(--category-color);
  border: 1px var(--category-color) solid;
}

コラム: attr(<attribute-name> color) と組み合わせる

人によっては、style 属性の中にカスタムプロパティを定義するのを避けたく思うかもしれない。--category-color というカスタムプロパティがどこからやってきたのか、どこで値が定義されているのかが .css を見ただけでわかるようにしたい、とかそういう考えを持っていると、そう思うのではないか。

一応将来的には attr(<attribute-name> color) を使えば、以下のようにカスタムプロパティの定義も .css 側へと追いやれる。

import styles from './Entry.module.css';
import clsx from 'clsx'
function Entry(props) {
  // props.categoryColor にはバックエンドから引いた色の情報が文字列で入っている (例`"#FF0000"`)
  return (
    <div
      className={styles.container}
      data-category-color={props.categoryColor}>
      {/* ... */}
    </div>
  );
}
.container {
  color: attr(data-category-color color);
  border: 1px attr(data-category-color color) solid;
}

けどまだ attr(<attribute-name> color) をサポートしているメジャーブラウザは存在しないので、使おうにも使えない。あとそもそもカスタムプロパティの定義が .tsx から .css に移動しただけで、本質的にはやっていることは変わらないので、この書き方が特別良いかというと意見が分かれるかも...

ポケットモンスター・ポケモン・Pokémon・は任天堂・クリーチャーズ・ゲームフリークの登録商標です.

当ブログは @mizdra 個人により運営されており, 株式会社ポケモン及びその関連会社とは一切関係ありません.