かもメモ

自分の落ちた落とし穴に何度も落ちる人のメモ帳

React vanilla-extract 使ってみたのメモ

今関わっているプロジェクトで vanilla-extract という CSS Modules っぽく書ける CSS in JS が使われており初めて触ってみたの感想的なメモ

vanilla-extract の特徴

  • *.css.ts という TypeScript ファイルにオブジェクト形式で CSS を作成して CSS Modude っぽく使える
  • TypeScript なので型の恩恵が受けられる (存在しないプロパティなどがコンパイルエラーになる)
  • CSS のネストが制限される

CSS Modules っぽい

*.css.ts ファイルに書いたクラス定義を import して className に割り当て、出力されるクラスは hash 化されてスコープが作られる。

/src
  |- /components
      |-/header
          |- index.tsx
          |- header.css.ts
CSS の定義

/header/header.css.ts

import { style } from '@vanilla-extract/css';

export const header = style({
  background: "#fff",
  width: '100%',
});

export const wrapper = style({
  display: 'flex',
  justifyContent: 'space-between',
  alignItems: 'center',
  padding: '0.5rem 1rem',
});

export const title = style({
  fontSize: '2rem',
});

export const link = style({
  color: '#2ea4de',
  textDecoration: 'none',
  transition: 'color .3s',
  ':hover': {
    color: '#fbcb62'
  }
});
コンポーネント側での使い方

/header/index.ts

import { FC } from "react";
import { header, wrapper, title, link } from './header.css';

export const Header: FC = () => {
  return (
    <header className={header}>
      <div className={wrapper}>
        <h1 className={title}>Title</h1>
        <nav>
          <a className={link} href="#">Link</a>
        </nav>
      </div>
    </header>
  );
};

ほぼ CSS Modules と同じな印象でした。
CSS のスタイルを文字列で囲わないとなのが少し面倒ですが、プロパティ・値共に TypeScript での補完が効くので書きやすい印象でした。オブジェクト形式なので途中で追加変更した際に , を書き忘れるとビルドエラーになってしまうのが少し注意ポイントですが formatter などでどうにでもなりそうです。

Global なスタイルと CSS 変数 (カスタムプロパティ)

Global なスタイル

a タグなど共通してスタイルは globalStyle として定義する

import { globalStyle } from '@vanilla-extract/css';

globalStyle('html, body', {
  margin: 0
});
⚠ globalStyle は :hover などの疑似セレクタを一緒に定義できない
globalStyle('.link', {
  color: '#2ea4de',
  textDecoration: 'underline',
  transition: 'color .3s',
  ':hover': {
    color: '#fbcb62',
  },
});

これはコンパイルエラーになる
=> Object literal may only specify known properties, and '':hover'' does not exist in type 'GlobalStyleRule'.

疑似セレクタは現状別々に定義しなければならない

globalStyle での issue が出ているのでこの先変更されるかもしれないが、現状 .link.link:hover の用に別々に定義しなければならない

globalStyle('.link', {
  color: '#2ea4de',
  textDecoration: 'underline',
  transition: 'color .3s',
});
globalStyle('.link:hover', {
  color: '#fbcb62'
});

CSS 変数 (カスタムプロパティ)

CSS 変数は createGlobalTheme を使って定義できる

import { createGlobalTheme } from '@vanilla-extract/css';

const vars = createGlobalTheme(':root', {
  color: {
    primary: 'red',
    secondary: 'blue'
  },
});

👇 CSS 変数もハッシュが追加されたものが出力される

:root {
  --color-primary__1cfdkwm0: red;
  --color-secondary__1cfdkwm1: blue;
}

createGlobalTheme で作成した CSS 変数を使う方法

createGlobalTheme で定義した変数を import してプロパティ指定すればOK

import { style } from '@vanilla-extract/css';
import { vars } from './styles/theme.css';

const container = styles({
  color: vars.color.primary,
});

cf. createGlobalTheme — vanilla-extract

疑似セレクタ・ネストしたプロパティ

疑似セレクタ

疑似セレクタはキーを:始まりの文字列として定義できる

import { style } from '@vanilla-extract/css';

const link = style({
  opacity: 1,
  ':hover': {
      opacity: 0.8,
  },
  ':before': {
    content: '>',
    marginRight: '.5rem',
  },
};

ネストしたプロパティ

  • selectors プロパティの中に定義する
  • 自身を示す & が必須
  • 文字列展開 (テンプレートリテラル) を使う場合は、セレクタ[] で囲う
import { style } from '@vanilla-extract/css';

const link = style({
  selectors: {
    '&:hover:not(:active)': {
      border: '2px solid aquamarine'
    },
    'nav li > &': {
      textDecoration: 'underline'
    },
  },
});

👇 ビルド結果

.styles_link__1hiof570:hover:not(:active) {
  border: 2px solid aquamarine;
}
nav li > .styles_link__1hiof570 {
  text-decoration: underline;
}

🙅 自身の子・自身からの隣接セレクタは指定できない

オブジェクトの中はあくまで自身が最終的なスタイルの対象でなければならない

import { style } from '@vanilla-extract/css';

const section = style({
  marginBottom: '6rem',
});

const container = style({
  // some style…
  selectors: {
     // ❌ ERROR
    `& > *:first-child`: {
      marginTop: '0px',
    },
    // ❌ ERROR
    [`& > ${section}`]: {
      marginBottom: '3rem',
    }
  },
});

自身のスタイルでないものはコンパイルエラーになる

👇

  • 🙆 あくまで自身のスタイル (自身が子) でなければならない
  • 🙆 最終的に適応したいセレクタa など global なセレクタの場合は globalStyle を使う
import { globalStyle, style } from '@vanilla-extract/css';

const container = style({
  // some style…
});

const section = style({
  marginBottom: '6rem',
  selectors: {
    [`${container} > &`]: {
      marginBottom: '3rem',
    }
  },
});

globalStyle(`${container} > *:first-child`, {
  backgroundColor: '#F00',
});

🤔 .section + .section のような自身との隣接セレクタの指定方法がわからない

.section + .section { margin-top: 3rem; }

みたいなスタイルを結構使うのですが vanilla-extract での指定方法がわかりませんでした

import { style } from '@vanilla-extract/css';

const section = style({
  selectors: {
    // ❌ ERROR: Top-level "this" will be replaced with undefined since this file is an ECMAScript module
    [`${this} + &`]: {
      marginTop: '3rem',
    },
    // エラーにはならないが無視される
    // Block-scoped variable 'section' used before its declaration.
    [`${section} + &`]: {
      marginTop: '3rem',
    },
  }
});

globalStyle で定義するほか無いのかもしれません…

cf. Styling — vanilla-extract


📝 Vite React のプロジェクトに vanilla-extract を導入する

vite 用のプラグインを導入する必要がある

$ npm i -D @vanilla-extract/vite-plugin

vite.config.js

import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
+ import { vanillaExtractPlugin } from '@vanilla-extract/vite-plugin'

// https://vitejs.dev/config/
export default defineConfig({
- plugins: [react()]
+ plugins: [react(), vanillaExtractPlugin()]
})

cf. Vite — vanilla-extract

📝 CSS Modules の将来性に関してのメモ

cf. CSS Modulesの歴史、現在、これから - Hatena Developer Blog