Nikkei Advent Calendar 2020 の 22 日目の記事です。
はじめに
法人向けサービスの UI/UX チームの清野です。
サービスをリリースしてから UI の変更を重ねるにつれて、デザインファイルと実装でズレが生じたり、デザインと実装の関係性がわかりにくくなったりする問題がよくあります。このような問題を防ぐために、カラーやタイポグラフィなどの情報をデザインファイルから取り出して、コード上でデザイントークンとして定義する方法を紹介します。
実現する方法の検討
実現する方法はいくつか考えられます。チーム体制やワークフロー、デザインファイルの構成によって、適している方法は変わってきます。
1. 一気通貫ツールにおまかせ
デザインファイルの情報をコード化するまで一気通貫で面倒を見てくれる便利なツールもあります。代表的なものが Diez で、Figma や Sketch などのデザインファイルから CSS や Swift など各プラットフォーム向けのコードを生成してくれます。この方法は実装コストが低い一方で、デザインファイルをツールが対応できるように作らなければならず、生成されるファイルの形式もツールの制約を受けます。少人数の新規プロジェクトなどには適していそうな印象です。
2. API かプラグイン経由でがんばる
デザインファイルや生成するファイルの形式に柔軟性をもたせたい場合は、API かプラグインでデザインファイルにアクセスして情報を取り出して、ファイルを生成する方法があります。ファイル生成の部分を自前で実装する場合は、プラットフォームごとに異なる単位や形式で生成する必要があるため、複数プラットフォーム (iOS と Android、CSS、JavaScript...) に対応する場合はつらくなってきます。1 プラットフォームならこれでもいいかもしれません。
3. API かプラグイン経由 + デザイントークンツール
単一のソースから複数プラットフォーム向けのコードを生成することに特化したツールもあって、代表的なものとして、Amazon の Style Dictionary や Salesforce の Theo があります。これらを使うと 2 の方法の面倒な部分がだいぶなくなり、設定などでカスタマイズもいろいろできます。ある程度ファイルの形式などに柔軟性を持たせつつ、実装コストを抑えた方法と言えます。Figma から Style Dictionary 形式のファイルをエクスポートしてくれるプラグインもあります。
今回はこの 3 つ目の方法を検証してみます。デザインツールは Figma、デザイントークンツールは Style Dictionary、デザインファイルからの情報取得は Figma の API 経由、話を単純にするためにデザイントークンはカラーのみを対象に考えます
Style Dictionary のファイル形式
Style Dictionary では単一のソースを JSON で定義すると、CSS や SCSS、JavaScript、Swift、XML などのプラットフォームごとのファイルを生成してくれます。
単一のソースの定義例
{
"color": {
"palettes": {
"Blue": {
"value": "#2f80ed",
"comment": "blue description"
},
"Green": {
"value": "#219653",
"comment": "green description"
},
"Red": {
"value": "#eb5757",
"comment": "red description"
}
}
}
}
まずは、Figma の API を使って、この形式の JSON を生成してみます。
Figma の API からカラーの情報を取得する
team の styles のエンドポイント
デザインファイルでは以下のように、Figma の library 機能を使って team 共通のカラーパレットが定義されているとします。
このカラーパレットのカラーコードや name、description を API 経由で取得したいところです。team library で定義された styles の情報をとれるエンドポイントを叩いてみます。
async function fetchFigma(path) {
const res = await fetch(`https://api.figma.com/v1/${path}`, {
method: 'GET',
headers: {
'X-Figma-Token': API_KEY,
},
})
if (!res.ok) {
throw new Error(`status: ${res.status} ${res.statusText}`)
}
const data = await res.json()
return data
}
async function getStyles() {
const data = await fetchFigma(`teams/${TEAM_ID}/styles`)
return data.meta.styles
}
;(async () => {
const styles = await getStyles()
console.log(styles)
})()
;[
{
key: 'xxx',
file_key: 'xxx',
node_id: '2:15',
style_type: 'FILL',
thumbnail_url: 'xxx',
name: 'blue', // あった!
description: 'blue description', // あった!
created_at: '2020-12-17T11:49:25.602Z',
updated_at: '2020-12-17T11:50:34.442Z',
user: {},
sort_position: '.',
},
]
name
と description
の情報はありましたが、カラーコードがありません…。
files のエンドポイント
team 共通のカラーパレットを使ってカラーを定義しているオブジェクトがどうなっているか確認するために、ファイルの情報をとれるエンドポイントを叩いてみます。
async function getFile() {
const file = await fetchFigma(`files/${FILE_KEY}`)
return file
}
async function getFrames(pageName) {
const file = await getFile()
const canvas = file.document.children.find(
c => c.name === pageName,
)
const frames = canvas.children
return frames
}
;(async () => {
const frames = await getFrames(STYLES_PAGE_NAME)
const paletteFrame = frames.find(f => {
return f.name === PALETTE_FRAME_NAME
})
console.log(paletteFrame)
})()
カラーパレットを使っているオブジェクトの中身を見てみると、こっちに RGB の情報がありました。
{
id: '121:6',
name: 'ColorPalettes',
type: 'FRAME',
blendMode: 'PASS_THROUGH',
children: [
{
id: '121:7',
name: 'Rectangle',
type: 'RECTANGLE',
blendMode: 'PASS_THROUGH',
absoluteBoundingBox: { x: 35, y: -19, width: 40, height: 40 },
constraints: { vertical: 'TOP', horizontal: 'LEFT' },
fills: [{
blendMode: 'NORMAL', type: 'SOLID', color: {
// あった!
r: 0.18431372940540314,
g: 0.501960813999176,
b: 0.929411768913269,
a: 1
}
}],
strokes: [],
strokeWeight: 0,
strokeAlign: 'CENTER',
strokeJoin: 'BEVEL',
strokeMiterAngle: null,
styles: { fill: '2:15' }, // node_idが定義されている
effects: [],
cornerRadius: 3
}
]
}
styles.fill
の値の 2:15
は、参照している fill
の node_id
を示していています。さっき styles の情報をとれる API から取得した情報を見てみると、node_id
が 2:15
になっていて、カラーパレットの node
と紐付いていることがわかります。
{
key: 'xxx',
file_key: 'xxx',
node_id: '2:15', // これだ!
style_type: 'FILL',
name: 'blue',
description: 'blue description',
}
Style Dictionary 形式でファイル出力
これでカラーの name
, description
, RGB値
を API 経由で取得することができました。これらの情報をマージして Style Dictionary 形式でファイルを出力します。
async function getPalettes() {
const frames = await getFrames(STYLES_PAGE_NAME)
const paletteFrame = frames.find(frame => {
return frame.name === PALETTE_FRAME_NAME
})
const nodes = paletteFrame.children
const styles = await getStyles()
const palettes = nodes
.filter(
node =>
node.type === 'RECTANGLE' &&
node.styles &&
node.styles.fill,
)
.reduce((dict, node) => {
const style = styles.find(
s => s.node_id === node.styles.fill,
)
const { r, g, b } = node.fills[0].color
return {
...dict,
[style.name]: {
value: {
r: r * 255,
g: g * 255,
b: b * 255,
},
comment: style.description,
},
}
}, {})
return {
color: { palettes },
}
}
;(async () => {
const palettes = await getPalettes()
const json = JSON.stringify(palettes, null, '\t')
const filePath = 'properties/color/palettes.json'
fs.writeFile(filePath, json, e => {
if (e) throw e
console.info(`✔︎ ${filePath}`)
})
})()
生成されたファイル
properties/color/palettes.json
{
"color": {
"palettes": {
"blue": {
"value": {
"r": 47.0000009983778,
"g": 128.0000075697899,
"b": 237.0000010728836
},
"comment": "blue description"
},
"green": {
"value": {
"r": 33.00000183284283,
"g": 150.0000062584877,
"b": 83.00000265240669
},
"comment": "green description"
},
"red": {
"value": {
"r": 235.0000011920929,
"g": 87.00000241398811,
"b": 87.00000241398811
},
"comment": "red description"
}
}
}
}
Style Dictionary で各プラットフォーム向けにファイル生成
次にデザインファイルから生成したファイルをもとに、各プラットフォーム向けのファイルを生成します。 Style Dictionary をインストールして、プロジェクト作成のコマンドを叩くとコンフィグファイルなどを自動生成してくれます。
$ yarn add --dev style-dictionary
$ npx style-dictionary init basic
コンフィグファイルに必要なプラットフォームやファイルの形式を記述します。API が充実しているので、出力の細かなカスタマイズも可能です。
ミニマムな設定の config.json
{
"source": ["properties/**/*.json"],
"platforms": {
"scss": {
"transformGroup": "scss",
"buildPath": "build/scss/",
"files": [
{
"destination": "_variables.scss",
"format": "scss/variables"
}
]
},
"ios-swift": {
"transformGroup": "ios-swift",
"buildPath": "build/ios-swift/",
"files": [
{
"destination": "StyleDictionary.swift",
"format": "ios-swift/class.swift",
"className": "StyleDictionary",
"filter": {}
}
]
}
}
}
properties/color/palettes.json
で定義したカラーを参照する形で、primary color などの値も定義しておきます。
properties/color/base.json
{
"color": {
"primary": {
"blue": {
"value": "{color.palettes.blue.value}"
}
},
"secondary": {
"red": {
"value": "{color.palettes.red.value}"
}
},
"tertiary": {
"green": {
"value": "{color.palettes.green.value}"
}
}
}
}
ビルドのコマンドを叩くと、各プラットフォーム向けのファイルを生成できました! Style Dictionary がプラットフォーム間のカラーの指定形式の差異を吸収してくれていることがわかります。
$ npx style-dictionary build
SCSS
$color-palettes-blue: #2f80ed; // blue description
$color-palettes-green: #219653; // green description
$color-palettes-red: #eb5757; // red description
$color-parimary-blue: #2f80ed;
$color-secondary-red: #eb5757;
$color-tertiary-green: #219653;
Swift
import UIKit
public class StyleDictionary {
public static let colorPalettesBlue = UIColor(red: 0.184, green: 0.502, blue: 0.929, alpha:1)
public static let colorPalettesGreen = UIColor(red: 0.129, green: 0.588, blue: 0.325, alpha:1)
public static let colorPalettesRed = UIColor(red: 0.922, green: 0.341, blue: 0.341, alpha:1)
public static let colorParimaryBlue = UIColor(red: 0.184, green: 0.502, blue: 0.929, alpha:1)
public static let colorSecondaryRed = UIColor(red: 0.922, green: 0.341, blue: 0.341, alpha:1)
public static let colorTertiaryGreen = UIColor(red: 0.129, green: 0.588, blue: 0.325, alpha:1)
}
おわりに
デザインファイルから生成したデザイントークンを実装で使うことで、デザインと実装の乖離が起こりにくくなるほか、デザインファイル内の命名とコードの変数名などを統一できるので、「ここのボタンのカラーは blue-400
」というようにデザイナーとエンジニアで共通言語を使ってコミュニケーションしやすくなります。
最近普及しているデザインツールはプラグインや API などエコシステムが充実しているので、エンジニアリングで解決できるデザインと実装の間にある課題が他にもいろいろあります。UI/UX チームでは、そのような課題に取り組む活動を今後も行っていきます。