😸

レバテックのデザインシステム「VoLT」のデザイントークン運用を公開します!

2024/04/17に公開

TL;DR

  • デザインシステム「VoLT」のデザイントークン(VoLT Design Tokens)の社内運用を開始
  • Tokens Studioを使用してデザイントークンを定義
  • token-transformerstyle-dictionaryを使用してJSON連携とコード変換を実行
  • デザイントークンを社内向けにnpm packageとして配布

はじめに

レバテック開発部でPdMとテックリードを担当している、ふるしょう(古庄)です。

今回はレバテックのデザインシステム「VoLT」が本格的に社内運用を始めたデザイントークンをどのように運用しているかを紹介します!

VoLTの誕生背景は、弊社CTO室のかわうそさんが先日公開した記事や、ビザスク社と開催した合同勉強会スライドにて発信しております!

https://zenn.dev/levtech/articles/efedca53668140

デザイントークン(Design Tokens)とは

デザイントークンとは、デザインシステムにおいて適切なUIを表現するためにに定義される、タイポ・色・スペーシングなど、デザイン要素の標準化された規則や最小単位の値・要素のことです。

デザイントークンは、世の中のさまざまなデザインシステムでも採用されている一般的な概念です。
デザイントークンの目的は「デザイナーとエンジニアの共通言語として、一貫性と柔軟なデザインを可能にすること」と言えます。

VoLTでは、「基本要素」としてデザイントークンが管理する要素を構造化してガイドラインを策定しました!


VoLTの基本要素ガイドライン

デザイントークンが定義されていないと次のような状況が起こりやすくデザインデータとソースコードの保守が大変になりがちです、、、><

  • デザインツールと開発ツールが同期されておらず、スタイリング用の独自変数を定義している
  • トークンにあたるスタイルのいくつかが固有値で設定されている

レバテックでは、デザイナーが定義しているデザイントークンが、システムに連携されていなかった問題を『Tokens Studio』を用いて解消しました。
また、上記のようなハードコードされた設計・値が減少し、スケーラブルで変更に強いプロダクト設計を可能にし、ソースコード・デザインデータ双方の保守運用コストを削減しました。
VoLTでは、JS(cjs,esm)/TS(d.ts)/SCSS形式でトークンを配布しているため、幅広い用途で利用可能です。

VoLT Design Tokensの技術構成

VoLT Design Tokensの技術構成は次のとおりです。

Tokens Studioにて定義したデザイントークン(JSON)をGitHubにPull Request(PR)を作成し、GitHub Actionのワークフロー内でtoken-transformer,style-dictionaryを用いてJS(cjs,esm)/TS(d.ts)/SCSS形式に変換する構成を採用しています。

VoLT Design Tokensのライフサイクル

VoLTでは、デザイントークンをフロントエンドで利用可能なコードとして配布しています。
デザイントークンの具体的なライフサイクルは次のとおりです。

  1. Tokens Studioを使用してデザイントークンを定義し、feature/*ブランチを指定してPR作成
  2. PR作成をトリガーにJSONの変更を検知してJS(cjs,esm)/TS(d.ts)/SCSS形式に変換
  3. PRマージをトリガーに最新バージョンをnpm packageとして配布
  4. 各フロントエンドシステムがVoLT Design Tokensをimportしてデザイントークンを利用

1. Tokens Studioを使用してデザイントークンを定義し、feature/*ブランチを指定してPR作成

こちらの記事を参考にデザインシステムPJのデザイナー・エンジニア双方で話し合い運用を策定しました。

2. PR作成をトリガーにJSONの変更を検知してJS(cjs,esm)/TS(d.ts)/SCSS形式に変換

このCIでは、主に次の2点を実行しています。

  • デザイントークン(JSON)の変更を検出
  • token-transformerとstyle-dictionaryを用いてコード変換

実行しているCIの一部抜粋

.github/workflows/tokens.ci.yml
name: tokens/ci

jobs:
  setup:
    runs-on: ubuntu-latest
    strategy:
      matrix:
        node-version: [20.x]
    steps:
      - uses: actions/checkout@v4
        with:
          ref: ${{ github.event.pull_request.head.sha }}
          fetch-depth: 0

      - name: Set up node
        uses: ./.github/workflows/composite/setup-node
        with:
          node-version: ${{ matrix.node-version }}
          working-directory: ./tokens
          node-auth-token: ${{ secrets.xxxxxxxxxxx }}

  build:
    needs:
      - setup
    runs-on: ubuntu-latest
    strategy:
      matrix:
        node-version: [20.x]
    steps:
      - uses: actions/checkout@v4
        with:
          ref: ${{ github.event.pull_request.head.sha }}
          fetch-depth: 0

      - name: Set up node
        uses: ./.github/workflows/composite/setup-node
        with:
          node-version: ${{ matrix.node-version }}
          working-directory: ./tokens
          node-auth-token: ${{ secrets.xxxxxxxxxxx }}

      - name: Check build
        run: yarn build
        working-directory: ./tokens

  generate-token-path-filter:
    needs:
      - setup
    runs-on: ubuntu-latest
    strategy:
      matrix:
        node-version: [20.x]
    outputs:
      src: ${{ steps.token-changes.outputs.src }}
    steps:
      - uses: actions/checkout@v4
        with:
          ref: ${{ github.event.pull_request.head.sha }}
          fetch-depth: 0

      - name: Set up node
        uses: ./.github/workflows/composite/setup-node
        with:
          node-version: ${{ matrix.node-version }}
          working-directory: ./tokens
          node-auth-token: ${{ secrets.xxxxxxxxxxx }}

      - uses: dorny/paths-filter@v3
        id: token-changes
        with:
          filters: |
            src:
              - 'tokens/figma-tokens.json'

  generate-token:
    needs:
      - generate-token-path-filter
    # tokens/fimag-tokens.json に変更があった時のみ発火する
    if: needs.generate-token-path-filter.outputs.src == 'true'
    runs-on: ubuntu-latest
    strategy:
      matrix:
        node-version: [20.x]
    steps:
      - uses: actions/checkout@v4
        with:
          ref: ${{ github.event.pull_request.head.sha }}
          fetch-depth: 0

      - name: Set up node
        uses: ./.github/workflows/composite/setup-node
        with:
          node-version: ${{ matrix.node-version }}
          working-directory: ./tokens
          node-auth-token: ${{ secrets.xxxxxxxxxxx }}

      - name: Generate Token
        run: yarn gen:token
        working-directory: ./tokens

      - name: Auto Commit Generated Token
        uses: stefanzweifel/git-auto-commit-action@v5
        with:
          commit_message: "Generate Token"

実際に作成されたPRの一部がこちらです!
白のカラー変数名を大文字に修正する軽微な変更ではありますが、Figmaのカラーパレット内のVariablesがデザイントークンとして定義されるので、これだけでもエンジニア・デザイナー双方にメリットが大きいです!

実際のPRより抜粋

style-dictionaryは以下のようにコード変換時の拡張が可能なので、フロントエンドエンジニアがデザイントークンをより使いやすくできて、とても便利でした!

tokens/style-dictionary.config.js
const tinycolor = require("tinycolor2");

const StyleDictionary = require("style-dictionary").extend({
  source: ["figma-tokens.output.json"],
  platforms: {
    scss: {
      buildPath: "src/",
      transformGroup: "scss",
      files: [
        {
          destination: "index.scss",
          format: "scss/map-deep",
          mapName: "css-tokens",
          options: {
            outputReferences: true,
          },
        },
      ],
      transforms: ["name/cti/kebab", "shadow/scss"],
    },
    ts: {
      buildPath: "src/",
      transformGroup: "js",
      files: [
        {
          format: "javascript/es6",
          destination: "index.js",
        },
        {
          format: "typescript/es6-declarations",
          destination: "index.d.ts",
          options: {
            outputStringLiterals: true,
          },
        },
      ],
    },
  },
});

StyleDictionary.registerTransform({
  name: "shadow/scss",
  type: "value",
  matcher: (prop) => {
    return prop.path[0] === "boxShadow";
  },
  transformer: (prop) => {
    const [
      { x: x1, y: y1, blur: blur1, spread: spread1, color: color1 },
      { x: x2, y: y2, blur: blur2, spread: spread2, color: color2 },
    ] = prop.original.value;
    const rgbColor1 = tinycolor(color1).toRgbString();
    const rgbColor2 = tinycolor(color2).toRgbString();
    return `${x1}px ${y1}px ${blur1}px ${spread1}px ${rgbColor1}, ${x2}px ${y2}px ${blur2}px ${spread2}px ${rgbColor2}`;
  },
});

StyleDictionary.buildAllPlatforms();

module.exports = StyleDictionary;

3. PRマージをトリガーに最新バージョンをnpm packageとして配布

PRのマージをトリガーに、tsupを使用してライブラリをビルドし、ビルド成功後に自動的にリリースするステップをCIに組み込んでいます。
これにより、新しいバージョンのタグがリポジトリに追加され、リリースノートを生成した後にnpm packageとして配布しています。

また、VoLTはVue.js × React × Monorepoの構成で作成しており、デザイントークンは共通パッケージとして参照する必要がありました。
Nuxt3ではcjsではSSRが正しく動作しないため、style-dictionaryで出力したjsをライブラリ用にバンドルするようにしました。

https://nuxt.com/docs/guide/concepts/esm

tokens/tsup.config.ts
import { defineConfig } from 'tsup';

export default defineConfig({
  entry: [__dirname + '/src/index.js'],
  outDir: 'dist',
  tsconfig: 'tsconfig.build.json',
  minify: true,
  target: 'es2020',
  format: ['cjs', 'esm'],
  clean: true,
  dts: true,
});

4. 各フロントエンドシステムがVoLT Design Tokensをimportしてデザイントークンを利用

このサンプルではインラインスタイルとして各トークンを設定していますが、そのほかにもCSS-in-JSなどで有効に使うことができます。

src/components/Button/Button.tsx
import {
  CommonSemanticBrandLtBrandPrimary,
  CommonSemanticCommonSurfaceWhite,
  FontSize24,
} from "@lv-levtech/volt-tokens";

export const Button = ({ children, ...rest }: Props) => {
  const style: CSSProperties = {
    fontSize: FontSize24,
    color: CommonSemanticCommonSurfaceWhite,
    backgroundColor: CommonSemanticBrandLtBrandPrimary,
  };

  return (
    <button className={"example-Button"} style={style} {...rest}>
      {children}
    </button>
  );
};

さいごに

デザイナーとエンジニア双方でデザイントークンと向き合って運用フローを構築したおかげで、デザイントークンの変更点をエンジニアが検知しやすくなったり、ソースコード・デザインデータ双方の保守運用コスト削減を実感しています。

デザインシステムに取り組んでいる・取り組もうとしている開発組織の方々と、今後積極的に勉強会や交流会を開催していきたいので、興味のある方はぜひレバテック開発部の公式XのDMからご連絡ください!

参考文献

GitHubで編集を提案
レバテック開発部

Discussion