Pigment CSSの素振り
- zero-runtime CSS-in-JS
- 2024/11/02時点でv0.0.25なので、かなりexperimental
- WyW-in-JSをベースとしている。
- WyW-in-JSについては、poteboyさんの以下の資料が詳しい
- https://speakerdeck.com/poteboy/xian-dai-csshuremuwakunonei-bu-shi-zhuang-tosonoshi-zu-mi?slide=13
...というくらいの情報を持っているが、まだ全貌がわかっていないので、ドキュメント(README)から読んでいく。
Getting started
- Next.jsとVite、その他のバンドラをサポート予定
- 解決したい課題感は、一般的なゼロランタイムCSS-in-JSと同じ。
- ビルド時間は伸びるが、ランタイムでのパフォーマンスを向上する
- Next.jsとViteでの使い方が載っている。
Basic usage
スタイリングはcss
関数で行う。
import { css } from '@pigment-css/react';
const visuallyHidden = css({
border: 0,
clip: 'rect(0 0 0 0)',
height: '1px',
margin: -1,
overflow: 'hidden',
padding: 0,
position: 'absolute',
whiteSpace: 'nowrap',
width: '1px',
});
function App() {
return <div className={visuallyHidden}>I am invisible</div>;
}
コールバック関数でtheme
変数にアクセスできる。
- その他に利用できる変数を確認する
const title = css(({ theme }) => ({
color: theme.colors.primary,
fontSize: theme.spacing.unit * 4,
fontFamily: theme.typography.fontFamily,
}));
JSXスタイル
import { styled } from '@pigment-css/react';
const Heading = styled('div')({
fontSize: '4rem',
fontWeight: 'bold',
padding: '10px 0px',
});
function App() {
return <Heading>Hello</Heading>;
}
いくつかこれまでのCSS-in-JSと異なる点が書かれている。
-
Propsはランタイムでしかアクセスできないため、CSSプロパティの宣言内ではPropsにアクセスできない。
-
必要なパターンすべてを宣言すること
-
CSSトークンの宣言が可能
-
2つ目と3つ目がよくわからなかった。後で戻る
Propsに応じたスタイルの制御
ビルド時にPropsの取りうる値が判明する場合は、props
とstyle
のプロパティを持つvariants
オブジェクトによってスタイルを事前定義できる。PandaCSSのrecipeみたいな感じ。
interface ButtonProps {
size?: 'small' | 'large';
}
const Button = styled('button')<ButtonProps>({
border: 'none',
padding: '0.75rem',
// ...other styles
variants: [
{
props: { size: 'large' },
style: { padding: '1rem' },
},
{
props: { size: 'small' },
style: { padding: '0.5rem' },
},
],
});
<Button>Normal button</Button>; // padding: 0.75rem
<Button size="large">Large button</Button>; // padding: 1rem
<Button size="small">Small button</Button>; // padding: 0.5rem
const Button = styled('button')({
border: 'none',
padding: '0.75rem',
// ...other base styles
variants: [
{
props: { variant: 'contained', color: 'primary' },
style: { backgroundColor: 'tomato', color: 'white' },
},
],
});
// `backgroundColor: 'tomato', color: 'white'`
<Button variant="contained" color="primary">
Submit
</Button>;
variants
はbooleanを返す関数でも定義できる。
const Button = styled('button')({
border: 'none',
padding: '0.75rem',
// ...other base styles
variants: [
{
props: (props) => props.variant !== 'contained',
style: { backgroundColor: 'transparent' },
},
],
});
別のクロージャの中でprops
関数は実行できない。
const Button = styled('button')({
border: 'none',
padding: '0.75rem',
// ...other base styles
variants: ['red', 'blue', 'green'].map((item) => ({
props: (props) => {
// ❌ Cannot access `item` in this closure
return props.color === item && !props.disabled;
},
style: { backgroundColor: 'tomato' },
})),
});
かわりにPlainなオブジェクトを利用する。
const Button = styled('button')({
border: 'none',
padding: '0.75rem',
// ...other base styles
variants: ['red', 'blue', 'green'].map((item) => ({
props: { color: item, disabled: false },
style: { backgroundColor: 'tomato' },
})),
});
反対にビルド時に値が決まり切らない(例:ユーザー入力から値を決める)場合、次のアプローチをとる。
1つはCSS変数を定義して、インラインスタイルでCSS変数の値を設定する。現代CSSパワー。
const Heading = styled('h1')({
color: 'var(--color)',
});
function Heading() {
const [color, setColor] = React.useState('red');
return <Heading style={{ '--color': color }} />;
}
もう1つはコールバック関数で値を指定する。内部的には上記のアプローチをPigment CSSがやってくれているのと同じそう。
const Heading = styled('h1')({
color: ({ isError }) => (isError ? 'red' : 'black'),
});
Styled ComponentをCSSセレクタとして利用できる。
const Wrapper = styled.div({
[`& ${Heading}`]: {
color: 'blue',
},
});
これで、Heading
コンポーネントをターゲットにcolor
属性を適用することができる。
Styled Componentを再度styled
に渡して、ベースのスタイルとして利用することもできる、
const ExtraHeading = styled(Heading)({
// ... overridden styled
});
メディアクエリやコンテナクエリもサポートされている。
import { css, styled } from '@pigment-css/react';
const styles = css({
fontSize: '2rem',
'@media (min-width: 768px)': {
fontSize: '3rem',
},
'@container (max-width: 768px)': {
fontSize: '1.5rem',
},
});
const Heading = styled('h1')({
fontSize: '2rem',
'@media (min-width: 768px)': {
fontSize: '3rem',
},
'@container (max-width: 768px)': {
fontSize: '1.5rem',
},
});
Good to knowにこんなの書いてあった。へ〜
Pigment CSS uses Emotion behind the scenes for turning tagged templates and objects into CSS strings.
keyframeはkeyframes
関数で生成できる。
import { keyframes } from '@pigment-css/react';
const fadeIn = keyframes`
from {
opacity: 0;
}
to {
opacity: 1;
}
`;
function Example1() {
return <div style={{ animation: `${fadeIn} 0.5s` }}>I am invisible</div>;
}
グローバルスタイルはglobalCss
で定義する。
import { globalCss } from '@pigment-css/react';
globalCss`
body {
margin: 0;
padding: 0;
}
`;
Theming
テーマファイルはconfigの中で定義するオブジェクトで、ランタイムJSには一切残らない。
extendTheme
を利用すると、CSS変数を生成できる。
生成した値は、テーマのオブジェクトに含まれるvars
からアクセスできる。
import { withPigment, extendTheme } from '@pigment-css/nextjs-plugin';
export default withPigment(
{
// ...nextConfig
},
{
theme: extendTheme({
colors: {
primary: 'tomato',
secondary: 'cyan',
},
spacing: {
unit: 8,
},
typography: {
fontFamily: 'Inter, sans-serif',
},
}),
},
);
生成されるCSS変数にPrefixをつけることができる。
extendTheme({
cssVarPrefix: 'pigment',
});
カラー設定
-
colorSchemes.light.colors.background
のような形式でカラーパレットを定義できる。 - デフォルトでは
colorSchemes
プロパティが定義されると、prefers-color-schemeを使ってカラーモードを切り替える。特定のクラス名付与によるダークモードへの切り替えを行いたいケースでは、getSelector
を用いて切り替えロジックを変更できる。
extendTheme({
colorSchemes: {
light: { ... },
dark: { ... },
},
+ getSelector: (colorScheme) => colorScheme ? `.theme-${colorScheme}` : ':root',
});
applyStyles
を使うことで、テーマに合わせたスタイリングができる。
const Heading = styled('h1')(({ theme }) => ({
color: theme.colors.primary,
fontSize: theme.spacing.unit * 4,
fontFamily: theme.typography.fontFamily,
...theme.applyStyles('dark', {
color: theme.colors.primaryLight,
}),
}));
themeに対する型定義は次のようにマージする。
// any file that is included in your tsconfig.json
import type { ExtendTheme } from '@pigment-css/react/theme';
declare module '@pigment-css/react/theme' {
interface ThemeTokens {
// the structure of your theme
}
interface ThemeArgs {
theme: ExtendTheme<{
colorScheme: 'light' | 'dark';
tokens: ThemeTokens;
}>;
}
}
sx prop
お馴染みのsx
propsも使える。
TSで使う場合は、以下の定義が必要。
type Theme = {
// your theme type
};
declare global {
namespace React {
interface HTMLAttributes<T> {
sx?:
| React.CSSProperties
| ((theme: Theme) => React.CSSProperties)
| ReadonlyArray<React.CSSProperties | ((theme: Theme) => React.CSSProperties)>;
}
}
}
Right-to-left support
directionのサポートが書かれている。現代っぽい
Building resusable components for UI libraries
variants
を使って再利用可能なコンポーネントを作る。
- slotのあたりがどう作用するのかよくわからなかった。
その後読んだらわかった。パッケージの利用側がスタイルをオーバーラーイドする際のキーとなるみたい。
export default withPigment(
{ ...nextConfig },
{
theme: {
components: {
PigmentStat: {
styleOverrides: {
root: {
backgroundColor: 'tomato',
},
value: {
color: 'white',
},
unit: {
color: 'white',
},
},
},
},
},
},
);
How Pigment CSS works
READMEとは別に挙動に関するドキュメントがあった。