NIKKEI TECHNOLOGY AND CAREER

Figmaと実装をリンクさせる

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 経由、話を単純にするためにデザイントークンはカラーのみを対象に考えます

Figma API から Style Dictionary を介してデザイントークン生成

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 共通のカラーパレットが定義されているとします。

Figma の 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: '.',
    },
]

namedescription の情報はありましたが、カラーコードがありません…。

files のエンドポイント

team 共通のカラーパレットを使ってカラーを定義しているオブジェクトがどうなっているか確認するために、ファイルの情報をとれるエンドポイントを叩いてみます。

Figmaのファイル構成

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 は、参照している fillnode_id を示していています。さっき styles の情報をとれる API から取得した情報を見てみると、node_id2: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 チームでは、そのような課題に取り組む活動を今後も行っていきます。

清野光平
ENGINEER清野光平

Entry

各種エントリーはこちらから

キャリア採用
Entry
新卒採用
Entry
短期インターン
Entry