今関わっているプロジェクトで 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', }, };
ネストしたプロパティ
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', } }, });
自身のスタイルでないものはコンパイルエラーになる
👇
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
で定義するほか無いのかもしれません…
📝 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()] })