レバテックのデザインシステム「VoLT」のデザイントークン運用を公開します!
TL;DR
- デザインシステム「VoLT」のデザイントークン(VoLT Design Tokens)の社内運用を開始
- Tokens Studioを使用してデザイントークンを定義
- token-transformerとstyle-dictionaryを使用してJSON連携とコード変換を実行
- デザイントークンを社内向けにnpm packageとして配布
はじめに
レバテック開発部でPdMとテックリードを担当している、ふるしょう(古庄)です。
今回はレバテックのデザインシステム「VoLT」が本格的に社内運用を始めたデザイントークンをどのように運用しているかを紹介します!
VoLTの誕生背景は、弊社CTO室のかわうそさんが先日公開した記事や、ビザスク社と開催した合同勉強会スライドにて発信しております!
デザイントークン(Design Tokens)とは
デザイントークンとは、デザインシステムにおいて適切なUIを表現するためにに定義される、タイポ・色・スペーシングなど、デザイン要素の標準化された規則や最小単位の値・要素のことです。
デザイントークンは、世の中のさまざまなデザインシステムでも採用されている一般的な概念です。
デザイントークンの目的は「デザイナーとエンジニアの共通言語として、一貫性と柔軟なデザインを可能にすること」と言えます。
VoLTでは、「基本要素」としてデザイントークンが管理する要素を構造化してガイドラインを策定しました!
VoLTの基本要素ガイドライン
デザイントークンが定義されていないと次のような状況が起こりやすくデザインデータとソースコードの保守が大変になりがちです、、、><
- デザインツールと開発ツールが同期されておらず、スタイリング用の独自変数を定義している
- トークンにあたるスタイルのいくつかが固有値で設定されている
レバテックでは、デザイナーが定義しているデザイントークンが、システムに連携されていなかった問題を『Tokens Studio』を用いて解消しました。
また、上記のようなハードコードされた設計・値が減少し、スケーラブルで変更に強いプロダクト設計を可能にし、ソースコード・デザインデータ双方の保守運用コストを削減しました。
VoLTでは、JS(cjs,esm)/TS(d.ts)/SCSS形式でトークンを配布しているため、幅広い用途で利用可能です。
VoLT Design Tokensの技術構成
VoLT Design Tokensの技術構成は次のとおりです。
- style-dictionary
- token-transformer
- tinycolor2
- tsup
- GitHub Actions
Tokens Studioにて定義したデザイントークン(JSON)をGitHubにPull Request(PR)を作成し、GitHub Actionのワークフロー内でtoken-transformer,style-dictionaryを用いてJS(cjs,esm)/TS(d.ts)/SCSS形式に変換する構成を採用しています。
VoLT Design Tokensのライフサイクル
VoLTでは、デザイントークンをフロントエンドで利用可能なコードとして配布しています。
デザイントークンの具体的なライフサイクルは次のとおりです。
-
Tokens Studioを使用してデザイントークンを定義し、
feature/*
ブランチを指定してPR作成 - PR作成をトリガーにJSONの変更を検知してJS(cjs,esm)/TS(d.ts)/SCSS形式に変換
- PRマージをトリガーに最新バージョンをnpm packageとして配布
- 各フロントエンドシステムがVoLT Design Tokensをimportしてデザイントークンを利用
Tokens Studioを使用してデザイントークンを定義し、feature/*
ブランチを指定してPR作成
1. こちらの記事を参考にデザインシステムPJのデザイナー・エンジニア双方で話し合い運用を策定しました。
2. PR作成をトリガーにJSONの変更を検知してJS(cjs,esm)/TS(d.ts)/SCSS形式に変換
このCIでは、主に次の2点を実行しています。
- デザイントークン(JSON)の変更を検出
- token-transformerとstyle-dictionaryを用いてコード変換
実行しているCIの一部抜粋
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は以下のようにコード変換時の拡張が可能なので、フロントエンドエンジニアがデザイントークンをより使いやすくできて、とても便利でした!
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
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などで有効に使うことができます。
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からご連絡ください!
参考文献
レバテック開発部の公式テックブログです! レバテック開発部 Advent Calendar 2024 実施中: qiita.com/advent-calendar/2024/levtech
Discussion