🥶

vanilla-extractのrecipeをテキストの体裁管理に活用する

2023/01/02に公開

h1~h4,pタグなどの、ウェブページ全体で汎用的に使うテキストのスタイリングって地味に困りませんか?(唐突)

1パターンで良ければそれぞれクラスセレクターを作って当てればいいだけの話ですが、部分的にスタイルを変えたいこともあるかと思います。

そんなときに便利なのがvanilla-extract (以下VEX) のRecipe。
ということで今回はVEXのrecipeを活用したテキストの体裁(スタイル)管理方法をご紹介します。

※実際のところ大した事はやってないのですが、自分がVEXを触りだしたときにRecipeやらSprinklesやら色々と機能があって、全体感をつかむのに結構苦労(Docsも英語ですし)した経験があるので、もし同じような人がいれば私の知見が役に立つかも、、ということで記事にまとめました!

この記事でやること

  • VEXのrecipeを使って、h1~h4,pタグなど基本的なテキストのスタイルを一括定義&柔軟に管理できるCSSファイルを作成する。
  • vanilla-extract、Recipeの概要をざっくり説明

以上2点です。

できたもの

リポジトリ

https://github.com/siino-xyz/vex-fontdefinition-sample

URL

https://vex-fontdefinition-example.web.app/

概要

今回はあくまでデモなので、Viteをベースに、React + TS + vanilla-extractでサクッと環境を立ち上げました。
ウェブフォントはfontsource(npm)を利用し、GoogleFontsをローカルホストする形で導入しています(これ爆アド過ぎるだろ)
https://fontsource.org/

vanilla-extract-cssってなに?

VEXは、”zero-runtimeかつ型安全なcss-modules”という感じのCSSライブラリです。ただそれだけではなく、今回利用するrecipeのような便利機能も複数用意してくれています。

recipeってなに?

recipeは、何度も使用しそうなCSSの記述を変数にまとめておき、Utilとして使い回すことができるvexの便利機能です。
vex本体とは別のnpmパッケージとして用意されています。(当然スタンドアロンでは使えないけど)

例えば「基本的なスタイル・レイアウトは同じだが、色だけ別々にしたい...」といった場合が良くあるかと思いますが、recipeではこういったスタイルの差分も含めて一括で管理することができます。まさにスタイリングレシピの作り置きができるというわけです。
今回はこいつを利用して、テキストの体裁を一括で管理するレシピを作っていきます。

install command
npm install @vanilla-extract/recipes

https://vanilla-extract.style/documentation/packages/recipes/

本編(実装フェーズ)

0.グローバルなCSS変数の定義ファイル作成

本命のrecipeを作成する前に、まずはプロジェクト全体で使い回したいグローバルな値を定義しておくテーマファイルglobalVars.css.tsを作成します。
作成にはvexが用意しているcreateGlobalTheme()関数を利用しています。

ソースコードを見る
globalVars.css.ts
import { createGlobalTheme } from "@vanilla-extract/css"

export const grid: number = 4
const px = (value: string | number) => `${value}px`

export const globalVars = createGlobalTheme(":root", {
  fonts: {
    zenKakuGothicNew: 'Zen Kaku Gothic New, "Hiragino Kaku Gothic ProN","Hiragino Sans",Meiryo,sans-serif',
    sourCodePro: 'Source Code Pro, "Helvetica Neue",Arial,"Hiragino Kaku Gothic ProN","Hiragino Sans",Meiryo,sans-serif'
  },
  fontSizes: {
    xs: "0.75rem",
    sm: "0.875rem",
    base: "1rem",
    lg: "1.125rem",
    xl: "1.3rem",
    "2xl": "1.5rem",
    "3xl": "1.875rem",
    "4xl": "2.25rem",
    "5xl": "3rem",
    "6xl": "3.75rem",
    "7xl": "4.5rem",
    "8xl": "6rem",
    "9xl": "8rem"
  },
  fontWeights: {
    thin: "100",
    extralight: "200",
    light: "300",
    normal: "400",
    medium: "500",
    semibold: "600",
    bold: "700",
    extrabold: "800",
    black: "900"
  },
  lineHeights: {
    none: "1",
    tight: "1.25",
    snug: "1.375",
    normal: "1.5",
    relaxed: "1.625",
    loose: "2"
  },
  letterSpacings: {
    tighter: "-0.05em",
    tight: "-0.025em",
    normal: "0em",
    wide: "0.025em",
    wider: "0.05em",
    widest: "0.1em"
  },
  grid: px(grid),
  space: {
    none: "0",
    auto: "auto",
    2: px(2),
    4: px(1 * grid),
    6: px(6),
    8: px(2 * grid),
    12: px(3 * grid),
    16: px(4 * grid),
    20: px(5 * grid),
    24: px(6 * grid),
    28: px(7 * grid),
    32: px(8 * grid),
    48: px(12 * grid),
    64: px(16 * grid),
    96: px(24 * grid)
  }
})

export const { fonts, fontSizes, fontWeights, lineHeights, letterSpacings, space } = globalVars

1.recipeファイル作成

globalVars.css.tsで定義した値を利用しながら、本命のrecipeファイルを作成していきます。
以下のリストは、recipeファイルの基本的な構造を示したものです。

  • base: {}
    • 共通化したいスタイリングを記述
  • variants: {}
    • 共通化せず、都度指定したいスタイリングを記述
  • defaultVariants: {}
    • 呼び出し先でvariantsを指定をしない場合の初期値を指定
ソースコードを見る
Typography.css.ts
import { recipe, RecipeVariants } from "@vanilla-extract/recipes"
import { 
fontSizes,
fontWeights,
letterSpacings,
lineHeights,
fonts
} from "../globalvars.css"

export const Typography = recipe({
  base: {
  //ベーススタイルは今回は使いませんでした。
  },
  variants: {
    fontSizes: {
      heading1: {
        fontSize: `clamp(${fontSizes["4xl"]}, 6.5vw, ${fontSizes["5xl"]})`
      },
      heading2: {
        fontSize: `clamp(${fontSizes["3xl"]}, 5.1vw, ${fontSizes["4xl"]})`
      },
      heading3: {
        fontSize: `clamp(${fontSizes["2xl"]}, 4.5vw, ${fontSizes["3xl"]})`
      },
      heading4: {
        fontSize: `clamp(${fontSizes.xl}, 3.5vw, ${fontSizes["2xl"]})`
      },
      p: {
        fontSize: `clamp(${fontSizes.sm}, 2.5vw, ${fontSizes.base})`
      },
      small: {
        fontSize: `clamp(${fontSizes.xs}, 2vw, ${fontSizes.sm})`
      }
    },
     fontWeight: {
      thin: {
        fontWeight: fontWeights.thin
      },
      extralight: {
        fontWeight: fontWeights.extralight
      },
      light: {
        fontWeight: fontWeights.light
      },
      normal: {
        fontWeight: fontWeights.normal
      },
      medium: {
        fontWeight: fontWeights.medium
      },
      semibold: {
        fontWeight: fontWeights.semibold
      },
      bold: {
        fontWeight: fontWeights.bold
      },
      extrabold: {
        fontWeight: fontWeights.extrabold
      },
      black: {
        fontWeight: fontWeights.black
      }
    },
    fontFamily: {
      zenKakuGothicNew: {
        fontFamily: fonts.zenKakuGothicNew
      },
      sourceCodePro: {
        fontFamily: fonts.sourCodePro
      }
    },
    /* ~~~~~
	    長いので中略。上記のような形で各CSSプロパティを定義しています。
	    (Githubからソースを参照できます。)
    ~~~~~*/
  },
  defaultVariants: { //初期値を定義
    fontFamily: "zenKakuGothicNew",
    letterSp: "wide",
    fontWeight: "normal",
    lineHeight: "sung"
  }
})

/*variantsをPropsとして扱いたいときには、
型情報をエクスポートします。(今回は使ってない)*/
export type TypographyVariants = RecipeVariants<typeof Typography>

余談ですが、fontSizeのレスポンシブ対応にはclamp()関数を利用しています。メディアクエリはできる限り使いたくない派です。(ただvwの値ちゃんと計算してないから微妙ッス)

2.tsxファイルにスタイルを当てていく

Typography.css.tsで作成したrecipeを早速使っていきます。
使い方は簡単で、className内にて関数を呼び出すだけです。
ちなみに今回のプロジェクトはリセットCSSを入れてるので、スタイルを当てなければ全てデフォルトサイズとなります。(HTMLのセマンティクスと文字サイズなどのスタイル情報は分離できてます)

ソースコードを見る
App.tsx
import "./styles/preflight.css"
import { Typography } from "./styles/recipe/typography.css"
import { styles } from "./styles/App.css"
import "@fontsource/source-code-pro/400.css"
import "@fontsource/source-code-pro/600.css"
import "@fontsource/source-code-pro/700.css"
import "@fontsource/zen-kaku-gothic-new/400.css"
import "@fontsource/zen-kaku-gothic-new/500.css"
import "@fontsource/zen-kaku-gothic-new/700.css"

const App = () => {
  return (
    <div className={styles.fontdemo}>
      <p className={styles.title}>vanilla-extract font definition example</p>
      <div className={styles.fontdemo_item}>
        <h1
          className={Typography({
            fontSizes: "heading1",
            fontWeight: "bold"
          })}
        >
          ヘッディング壱
        </h1>
        <h1
          className={Typography({
            fontSizes: "heading1",
            fontWeight: "bold",
            fontFamily: "sourceCodePro"
          })}
        >
          Heading1
        </h1>
      </div>
     /* ~~~~~
	    長いので中略。
	    (Githubからソースを参照できます。)
    ~~~~~*/
    <div className={styles.fontdemo_item}>
        <p
          className={Typography({
            fontSizes: "small",
            fontWeight: "normal"
          })}
        >
          あけましておでめとうございます。ー 最も小さいテキスト
        </p>
        <p
          className={Typography({
            fontSizes: "small",
            fontWeight: "normal",
            fontFamily: "sourceCodePro"
          })}
        >
          Happy new year. - This is smallest.
        </p>
      </div>
    </div>
  )
}

export default App

出来上がった画面のスクショ。

余談:CSS-in-JSが嫌なら...

上記のようなCSS-in-JS(TS)形式だとTSXファイルがCSSまみれになって可読性が落ちますし、将来vexを剥がすとなったときに大変苦労します。こういった懸念がある場合は、recipeで作った変数を別のcss.tsファイル経由で読み込みます。以下のようにすることで通常のCSSクラスとしてreicpeを活用することが可能です。
自分は大抵コンポーネント/ページごとに専属のCSSファイルを作るので、こっちのやり方をよく使います。

App.css.ts
import { style } from "@vanilla-extract/css"
import { space } from "./globalvars.css"
import { Typography } from "./recipe/typography.css"

export const styles = {
  heading_hoge: style([
  {padding: `${space[16]} 0`},
  //ここで通常のスタイリングと一緒に定義しちゃう。
  Typography({
      letterSp: "wider",
      fontFamily: "sourceCodePro"
    })
  ]),
}

Done.

駆け足ですが、VEXの便利機能であるRecipeを活用して、テキストの体裁を管理するUtilを作ってみました。解散ッ!!!

Discussion