Open26

Pigment CSSの素振り

ikuma-tikuma-t

https://github.com/mui/pigment-css

...というくらいの情報を持っているが、まだ全貌がわかっていないので、ドキュメント(README)から読んでいく。

ikuma-tikuma-t
ikuma-tikuma-t

スタイリングは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,
}));
ikuma-tikuma-t

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の取りうる値が判明する場合は、propsstyleのプロパティを持つ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'),
});
ikuma-tikuma-t

Styled ComponentをCSSセレクタとして利用できる。

const Wrapper = styled.div({
  [`& ${Heading}`]: {
    color: 'blue',
  },
});

これで、Headingコンポーネントをターゲットにcolor属性を適用することができる。


Styled Componentを再度styledに渡して、ベースのスタイルとして利用することもできる、

const ExtraHeading = styled(Heading)({
  // ... overridden styled
});
ikuma-tikuma-t

メディアクエリやコンテナクエリもサポートされている。

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',
  },
});
ikuma-tikuma-t

Good to knowにこんなの書いてあった。へ〜

Pigment CSS uses Emotion behind the scenes for turning tagged templates and objects into CSS strings.

ikuma-tikuma-t

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>;
}
ikuma-tikuma-t

グローバルスタイルはglobalCssで定義する。

import { globalCss } from '@pigment-css/react';

globalCss`
  body {
    margin: 0;
    padding: 0;
  }
`;
ikuma-tikuma-t
ikuma-tikuma-t

テーマファイルはconfigの中で定義するオブジェクトで、ランタイムJSには一切残らない。

ikuma-tikuma-t

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',
      },
    }),
  },
);
ikuma-tikuma-t

生成されるCSS変数にPrefixをつけることができる。

extendTheme({
  cssVarPrefix: 'pigment',
});
ikuma-tikuma-t

カラー設定

  • colorSchemes.light.colors.backgroundのような形式でカラーパレットを定義できる。
  • デフォルトではcolorSchemesプロパティが定義されると、prefers-color-schemeを使ってカラーモードを切り替える。特定のクラス名付与によるダークモードへの切り替えを行いたいケースでは、getSelectorを用いて切り替えロジックを変更できる。
 extendTheme({
   colorSchemes: {
     light: { ... },
     dark: { ... },
   },
+  getSelector: (colorScheme) => colorScheme ? `.theme-${colorScheme}` : ':root',
 });
ikuma-tikuma-t

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,
  }),
}));
ikuma-tikuma-t

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;
    }>;
  }
}
ikuma-tikuma-t

sx prop

お馴染みのsxpropsも使える。

https://github.com/mui/pigment-css?tab=readme-ov-file#sx-prop

ikuma-tikuma-t

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)>;
    }
  }
}
ikuma-tikuma-t
ikuma-tikuma-t

variantsを使って再利用可能なコンポーネントを作る。

  • slotのあたりがどう作用するのかよくわからなかった。
ikuma-tikuma-t

その後読んだらわかった。パッケージの利用側がスタイルをオーバーラーイドする際のキーとなるみたい。

export default withPigment(
  { ...nextConfig },
  {
    theme: {
      components: {
        PigmentStat: {
          styleOverrides: {
            root: {
              backgroundColor: 'tomato',
            },
            value: {
              color: 'white',
            },
            unit: {
              color: 'white',
            },
          },
        },
      },
    },
  },
);