
ブログをNext.js14(App Router)でリファクタリングしました
Meta社製のスタイリングライブラリであるStyleXの使い方について解説します。StyleXはCss-in-JSライブラリの一つですが、何よりRSCで使えるのが魅力の一つです。この記事では公式ドキュメントやサンプルコードをもとに、基礎から応用的な使い方や型定義をまとめて完全解説します。
この記事では、React(Next.js)+ TypeScirptのサンプルコードを載せて解説しておりますが、JavaScriptであったり、AngularやVueといった他のライブラリ、フレームワークでも使い方は同じなので、適宜読み替えてください。
StyleXはReactと同じくMeta社製で、更にRSCで使えるCSS-in-JSライブラリです。つまりNext.jsのappルータで、Server Componentのスタイリングに使用できます。執筆時点(2024年4月)では、CSS-in-JSとして以前から有力だったStyled-componentsはRSC対応しておらずClient Componentでしか使えないため、RSCの恩恵を受けることができません。
最もシンプルな使い方を解説します。
次のようにライブラリをimportし、
import * as stylex from "@stylexjs/stylex"
CSSスタイルを定義するのにcreate()
関数を使い、スタイルはキャメルケースで指定します。例えば次のように、textとtitleという名前の2つのスタイルを定義します。名前は任意につけてください。
const styles = stylex.create({
text: {
fontSize: 16,
lineHeight: 1.5,
color: "#333"
},
title: {
fontSize: 24,
lineHeight: 1.2,
color: "red",
fontWeight: "bold"
}
})
そして、props()
関数とスプレッド構文を用いて、次のように名前を指定して、コンポーネントにスタイルを当てます。
export default function MyComponent() {
return (
<div>
<h1 {...stylex.props(styles.title)}>Title</h1>
<p {...stylex.props(styles.text)}>some texts</p>
</div>
)
}
実行結果は次のとおりです。
以上のように、create関数内でtext、titleといった自由なキー名とスタイルを定義し、それを当てたいHTML要素に展開させるのが基本の使い方です。
stylesの記述場所ですが、CSS-in-JSですのでTSXファイル内でコロケーションさせるのが基本です。公式のサンプルコードでは、コンポーネントの後にスタイルの定義が書かれているため、以下の例でもそれに倣います。
次のコードのように、用途に応じて複数のstyleを定義することも可能です。
import * as stylex from "@stylexjs/stylex"
export default function MyComponent() {
return (
<div>
<h1 {...stylex.props(styles.title, colorStyles.blue)}>Title</h1>
<p {...stylex.props(styles.text, colorStyles.red)}>some texts</p>
</div>
);
}
const styles = stylex.create({
text: {
fontSize: 16,
lineHeight: 1.5,
},
title: {
fontSize: 24,
lineHeight: 1.2,
fontWeight: "bold",
color: "red"
}
})
const colorStyles = stylex.create({
blue: {
color: "blue",
backgroundColor: "skyblue"
},
red: {
color: "tomato"
}
})
なお、複数のスタイルを当てた場合、propsに渡したスタイルは後ろに行けば行くほど優先順位が高く、前のプロパティが上書きされます。
上のコードでは、styles.titleキーでテキストカラーを赤色、colorStyles.blueキーでテキストカラーを青色に定義していますが、
<h1 {...stylex.props(styles.title, colorStyles.blue)}>Title</h1>
のようにblueキーを後ろに定義しているので、Titleの出力結果は次のように赤色ではなく青色になります。
:hoverや:activeなどの擬似クラスは、次のcolorプロパティのようにプロパティ値にネストしたオブジェクトのキーとして定義します。
const styles = stylex.create({
title: {
fontSize: 24,
fontWeight: "bold",
color: {
default: "red",
":hover": "blue"
},
}
})
デフォルト値をdefaultで指定し、”:hover”でホバー時のスタイルを定義しています。
上の例ではcolorの中にhoverを定義しましたが、hover時の挙動をまとめて定義したい場合は、次のように”:hover”キーをネストの一番上に持ってきて、まとめて定義したプロパティオブジェクトのキーとして定義します。
const styles = stylex.create({
title: {
fontSize: 24,
fontWeight: "bold",
color: "red",
":hover": {
color: "blue",
cursor: "pointer",
opacity: 0.5
}
}
})
メディアクエリは、擬似クラス同様、次のようにネストしたキーとして定義します。
const styles = stylex.create({
title: {
fontSize: 24,
fontWeight: "bold",
color: {
default: "red",
"@media (max-width: 768px)": "blue",
"@media (max-width: 568px)": "green",
}
}
})
メディアクエリでも、まとめて指定したい際は次のように、メディアクエリをオブジェクトのキーにします。
const styles = stylex.create({
title: {
fontSize: 24,
fontWeight: "bold",
"@media (max-width: 768px)": {
color: "red",
backgroundColor: "pink",
fontSize: 32,
},
}
})
値を指定したくなかったり、消去する場合はnullを定義します。CSSにはinitial、unsetやrevertがありますが、StyleXでは全てnullで置き換えることが可能です。
以下の例では、defaultをnullとして、値を設定していません。
const styles = stylex.create({
title: {
fontSize: 24,
fontWeight: "bold",
color: {
default: "red",
"@media (max-width: 768px)": "blue",
"@media (max-width: 568px)": "green",
":hover": {
default: null,
"@media (max-width: 768px)": "tomato",
"@media (max-width: 568px)": "skyblue",
}
}
}
})
次のように、スタイルの非対応ブラウザなども考慮して、スタイルのフォールバック値を設定する場合があるかと思います。
.header {
position: fixed;
position: -webkit-sticky;
position: sticky;
}
StyleXでは、上のコードを次のようにfirstThatWorks()
関数で指定します。
const styles = stylex.create({
header: {
position: stylex.firstThatWorks('sticky', '-webkit-sticky', 'fixed'),
},
})
keyframes()
関数で指定します。
const rotation = stylex.keyframes({
from: { transform: "rotate(0deg)" },
to: { transform: "rotate(360deg)" }
})
const styles = stylex.create({
title: {
display: "inline-block",
fontSize: 24,
fontWeight: "bold",
animationName: rotation,
animationDuration: "3s",
}
})
create内のプロパティで関数を定義すれば、propsで呼び出し時に引数を渡せる動的なスタイルの定義ができます。
しかしむやみやたらにこの機能を利用するのは控えるように、と公式ドキュメントに書いてあり、動的なスタイルは大体の場合、後で紹介する条件付きスタイルに置き換えが可能です。
それでも動的なスタイルが便利に使える場面もあります、次のように、コンポーネントのstateに応じてプロパティの値が変わるようなスタイルを例に挙げます。
export default function MyComponent() {
const [height, setHeight] = useState(100)
return (
<div>
<h1 {...stylex.props(styles.title, styles.heightState(height))}>Title</h1>
</div>
)
}
const styles = stylex.create({
title: {
fontSize: 24,
fontWeight: "bold",
backgroundColor: "skyblue"
},
heightState: height => ({
height
})
})
props関数内で &&演算子や ?:三項演算子を利用することで、適用するスタイルの条件分岐が可能となります。
import * as stylex from "@stylexjs/stylex"
export default function MyComponent({
isActive = false,
isStrong = false
}: {
isActive?: boolean
isStrong?: boolean
}) {
return (
<div>
<h1 {...stylex.props(
styles.title,
isActive ? styles.active : styles.inactive,
isStrong && styles.strong
)}>Title</h1>
</div>
)
}
const styles = stylex.create({
title: {
fontSize: 24,
},
active: {
color: "red"
},
inactive: {
color: "#ccc"
},
strong: {
fontWeight: "bold"
}
})
ボタンの色が赤いパターン、青いパターンなどのバリアントは、特別な機能を使いません。最初に紹介した単なるオブジェクトで実現できます。
import * as stylex from "@stylexjs/stylex"
export default function MyComponent({
variant
}: {
variant: "info" | "warn"
}) {
return (
<div>
<h1 {...stylex.props(styles[variant])}>Title</h1>
</div>
)
}
const styles = stylex.create({
info: {
backgroundColor: "blue",
color: "#fff"
},
warn: {
backgroundColor: "yellow",
color: "#000"
}
})
ポイントとしては、props内でstylexを参照するのにブラケット構文を利用して、変数によってstylexの違うキーにアクセスできるようにすることです。
次のように、propsとして子コンポーネントに渡すことが可能です。
import * as stylex from "@stylexjs/stylex"
import type {StyleXStyles} from '@stylexjs/stylex'
export default function MyComponent() {
return (
<ChildComponent style={styles.title}/>
)
}
function ChildComponent({
style
}: {
style?: StyleXStyles
}) {
return <h1 {...stylex.props(style)}>Child Title</h1>
}
const styles = stylex.create({
title: {
fontSize: 24,
fontWeight: "bold"
}
})
styleを定義しているStyleXStylesという型についてですが、もう少し後でStyleXにおける型についてまとめて説明します。
ここで重要なのは、propsへの渡し方です。styles.xxxとして渡しており、props関数を通して渡してはいけません。
CSS変数はコンポーネントファイルにコロケーションさせるのではなく、「ファイル名.stylex.ts」という風に、拡張子の前にstylexをつけたファイル内で定義します。
その定義方法は、defineVars()
関数内にオブジェクトで定義したものを、名前付きエクスポートします。export defaultではダメです。
実際のコード例を挙げます。
import * as stylex from '@stylexjs/stylex'
export const colors = stylex.defineVars({
primary: "blue",
secondary: "skyblue",
accent: "yellow",
white: "rgb(240, 240, 255)",
black: "rgb(0, 0, 10)"
})
このように、変数は意味のある1つのオブジェクトでグループ化できます。この例では色に関するグループとして変数を定義しています。
このファイルをbasic.stylex.tsとします。そして、これらの変数を、使いたいファイル内でimportして使います。
import * as stylex from "@stylexjs/stylex"
import { colors } from "./basic.stylex"
export default function MyComponent() {
return (
<div>
<h1 {...stylex.props(styles.title)}>Title</h1>
<p {...stylex.props(styles.text)}>text</p>
</div>
)
}
const styles = stylex.create({
title: {
color: colors.primary
},
text: {
color: colors.black
}
})
colors.primaryのようにして変数にアクセスします。
また、ダークモード対応のmediaクエリなどを利用したい場合は、定義ファイルで次のように設定することも可能です。
import * as stylex from '@stylexjs/stylex'
defineVarsで作成したCSS変数グループのプロパティ値をオーバーライドするテーマを作成することができます。
テーマの作成にはcreateTheme()
関数を使い、第一引数に定義したCSS変数グループ(オーバーライドしたい1組のdefineVars)、第二引数にオーバーライドのプロパティをオブジェクトで書きます。
上の例に挙げたcolorsをテーマでオーバーライドする例を挙げます。
import * as stylex from '@stylexjs/stylex';
import { colors } from './basic.stylex';
ここで、テーマではグループ内の全ての変数をオーバーライドする必要があります。
なお、変数と違いテーマは必ずしも独立したファイルに書く必要はなく、コンポーネントファイル内に記述することもできます。
テーマを使う側では、propsの第一引数にテーマを渡すと、CSS変数で定義したプロパティ値だけテーマによって上書きされます。
import * as stylex from "@stylexjs/stylex"
import { colors } from "./basic.stylex"
import { dracula } from "./theme"
export default function MyComponent() {
return (
<div {...stylex.props(dracula, styles.div)}>
<h1 {...stylex.props(dracula, styles.title)}>Title</h1>
<p {...stylex.props(dracula, styles.text)}>text</p>
</div>
)
}
const styles = stylex.create({
title: {
color: colors.primary
},
text: {
color: colors.secondary
},
div: {
backgroundColor: colors.bg
},
})
上の方で「propsに複数のセレクタがあった場合は後ろの引数のセレクタが優先される」と述べましたが、テーマだけは後ろのうちCSS変数で定義された部分だけを上書きします。
それでは、propsにテーマ、変数、その後ろに全く関係のないプロパティの3つを指定した場合はどれが優先されるでしょうか。
import * as stylex from "@stylexjs/stylex"
import { colors } from "./basic.stylex"
import { dracula } from "./theme"
export default function MyComponent() {
return (
<h1 {...stylex.props(dracula, styles.title, styles.warn)}>Title</h1>
)
}
const styles = stylex.create({
title: {
color: colors.primary
},
warn: {
color: "yellow"
}
})
答えは、一番後ろのセレクタです。つまり上の例では黄色い文字が出力されます。
@properyルールは、CSS変数に型や初期値、継承の有無を指定できるルールです。
詳しくは以下のURLをご覧ください。
ただし、CSS変数の型定義は高度な使用例で、大半のユースケースでは必要ないと公式ドキュメントに記載があります。
それでも役に立つ場面がいくつかあるので、公式ドキュメントから例を紹介します。
以下のCSS変数があったとします。
import * as stylex from '@stylexjs/stylex'
export const tokens = stylex.defineVars({
primaryText: 'black',
secondaryText: '#333',
borderRadius: '4px',
angle: '0deg',
int: '2',
})
これに対する型は、stylex.types.xxxで指定します。
import * as stylex from '@stylexjs/stylex'
export const tokens = stylex.defineVars({
primaryText: stylex.types.color('black'),
secondaryText: stylex.types.color('#333'),
borderRadius: stylex.types.length('4px'),
angle: stylex.types.angle('0deg'),
int: stylex.types.integer(2),
})
integer型のCSS変数を用いると、floor関数(小数点切り捨て)であったり、round関数(四捨五入)をシミュレートできます。
const gradStyles = stylex.create({
gradient: {
// floor関数
[tokens.int]: `calc(16 / 9)`, // 1
// round関数
[tokens.int]: `calc((16 / 9) + 0.5)` // 2
},
})
通常はCSSだけでグラデーションをアニメーションさせることはできませんが、angle型のCSS変数を使用し、これにアニメーションの設定をすると実現できます。
const rotate = stylex.keyframes({
from: { [tokens.angle]: '0deg' },
to: { [tokens.angle]: '360deg' },
});
stylex.create()内で定義したオブジェクトのそれぞれのプロパティの型は、StyleXStyles型で定義できます。
import * as stylex from "@stylexjs/stylex"
import type {StyleXStyles} from '@stylexjs/stylex'
export default function MyComponent() {
return (
<ChildComponent title={style.title}/>
)
}
function ChildComponent ({
title
}: {
title?: StyleXStyles
}) {
return <h1 {...stylex.props(title)}></h1>
}
const style = stylex.create({
title: {
color: "red"
},
text: {
color: "blue"
}
})
また、StyleXStyles型は、create()内で定義した複数のキーを配列として受け取ることも可能です。
import * as stylex from "@stylexjs/stylex"
import type {StyleXStyles} from '@stylexjs/stylex'
export default function MyComponent() {
return (
<ChildComponent style={[style.textColor, style.textSize]}/>
)
}
function ChildComponent ({
style
}: {
style?: StyleXStyles
}) {
return <h1 {...stylex.props(style)}>something</h1>
}
const style = stylex.create({
textColor: {
color: "red"
},
textSize: {
fontSize: 16
}
})
StyleXStyles型のジェネリクスとして、受け付けるプロパティのみを定義したオブジェクトを渡せば、そこで定義したプロパティのみを許可するStyleXStyles型が作成できます。
type Props = {
style?: StyleXStyles<{
color?: string
fontSize?: number
backgroundColor?: string
}>
}
上の例では、color、font-size、background-color以外のプロパティが定義されているStlyeXのオブジェクトは型エラーとなります。
先ほどのプロパティ名だけではなく、プロパティの値の制限も可能です。方法は、プロパティのそれぞれの型に許可する値のリテラル型の共用体を渡すだけです。
type Props = {
style?: StyleXStyles<{
color?: "red" | "blue"
fontSize?: 16 | 18 | 20
}>
}
先ほどは受け付けるプロパティをStyleXStyles型のジェネリクスで列挙しましたが、逆に受け付けないプロパティリストを作って制限できる型として、StyleXStylesWithout型があります。
type Props = {
style?: StyleXStylesWithout<{
position: unknown
display: unknown
margin: unknown
padding: unknown
}>
}
上のコードで列挙した4つのプロパティを定義したstyleに対して型エラーがでます。
以上、StyleXの基本的な使い方についてまとめました。非常にシンプルなAPIなので、覚えることも少なくすぐにでも実用できるかと思います。執筆時点(2024年4月)ではRSCで使えるCSS-in-JSはあまりないので、StyleXは有力なスタイリングの手段となり得ます。
関連記事
最新の記事
カテゴリー一覧
アーカイブ
目次