12
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

Ateam Finergy Inc. × Ateam Wellness Inc. Advent Calendar 2023

Day 7

Figma Variablesで指定したトークンをTailwindCSSのユーティリティクラスとして使用する

Last updated at Posted at 2023-12-06

この記事はAteam Finergy Inc. × Ateam Wellness Inc. Advent Calendar 2023の7日目の投稿です。
本日は@themeaningof8がお送りします。

エイチームフィナジーではCSSライブラリとしてTailwindCSSを主に運用しています。
TailwindCSS自体が制約ベースなので実装上の一貫性の担保はしやすいのですが、Figmaでのプロトタイプ作成時に幅や余白が手作業での管理となっていたので、今回Figma Variablesを定義するJSONファイルからTailwindCSSの設定ファイルを生成するスクリプトを作成しました。

やったこと

tokens.jsonをSingle Source Of Truth(信頼できる唯一の情報源)として管理し、Figma、TailwindCSSで使用できるようにしました。

スクリーンショット 2023-12-04 14.52.29.png

手順

tokens.jsonの作成

Figma Variablesとしてインポートしたり、TailwindCSSの設定ファイルとして変換するためのJSONファイルを作成します。フォーマットはDesign Tokens Format Module形式を参考に作成しています。https://design-tokens.github.io/community-group/format/

また、この後のTailwindCSSで利用できるファイルに変換するために、以下のようなtypeに分類しています。

type 内容
color HEXコードで記載。色のトークンを指定する際に使用。
number 任意の数値を記載。特にremに変換しない数値のトークンに使用。
dimension 任意の数値を記載。px→remに変換が必要なトークンに使用。
{
  "colors": {
    "gray": {
      "100": {
        "$value": "#FAFAFA",
        "$type": "color"
      },
      "200": {
        "$value": "#EEEDED",
        "$type": "color"
      },
      "300": {
        "$value": "#DFDDDD",
        "$type": "color"
      },
      "400": {
        "$value": "#C3C1C1",
        "$type": "color"
      },
      "500": {
        "$value": "#9B9897",
        "$type": "color"
      },
      "600": {
        "$value": "#7D7978",
        "$type": "color"
      },
      "700": {
        "$value": "#4E4B4B",
        "$type": "color"
      },
      "800": {
        "$value": "#343232",
        "$type": "color"
      },
      "900": {
        "$value": "#1A1919",
        "$type": "color"
      }
    },
    ...
  },
  "maxWidth": {
    "0": {
      "$type": "dimension",
      "$value": "0px"
    },
    ...
  },
  "opacity": {
    "0": {
      "$type": "number",
      "$value": "0"
    },
    ...
  },
}

TailwindCSSで利用できるファイルに変更する

今回TailwindCSSのユーティリティクラスとして以下のトークンを使用できるようにしました。

'borderRadius','borderWidth','colors','ex-colors','maxWidth','opacity','screens','spacing',

以下のスクリプトを実行するとTailwindCSSの設定で読み込むためのJSファイルが作成されるようになります。
今回はStyle Dictionaryを使用して作成しました。

import StyleDictionary from 'style-dictionary'
import type { Options, TransformedToken } from 'style-dictionary'

StyleDictionary.registerTransform({
  name: 'size/pxToRem',
  type: 'value',
  matcher: (token) => {
    return token.type === 'dimension'
  },
  transformer: (token: TransformedToken, options: Options) => {
    function getBasePxFontSize(options: Options) {
      return (options && options.basePxFontSize) || 16
    }
    const baseFont = getBasePxFontSize(options)
    const floatVal = parseFloat(token.value)
    if (floatVal === 0) {
      return '0'
    }
    return `${floatVal / baseFont}rem`
  },
})

StyleDictionary.registerTransformGroup({
  name: 'css',
  transforms: ['attribute/cti', 'name/cti/kebab', 'color/hsl'],
})

StyleDictionary.registerTransformGroup({
  name: 'javascript',
  transforms: ['attribute/cti', 'size/pxToRem'],
})

type TokenObject = Record<string, Record<string, string>>

StyleDictionary.registerFormat({
  name: 'typescript/custom',
  formatter: ({ dictionary }) => {
    const result: TokenObject = {}
    dictionary.allProperties.map((property: TransformedToken) => {
      if (typeof property.attributes?.item === 'string') {
        const type = property.attributes.type
        if (type) {
          result[type] = result[type] || {}
          result[type][property.attributes.item] = property.value
        }
      } else {
        result[property.name] = property.value
      }
    })
    return `export default ${JSON.stringify(
      result,
      null,
      2
    )} satisfies Record<string, string | Record<string, string>> \n`
  },
})

StyleDictionary.registerParser({
  pattern: /\.json$|\.tokens\.json$|\.tokens$/,
  parse: ({ contents }) => {
    const output = contents
      .replace(/["|']?\$value["|']?:/g, '"value":')
      .replace(/["|']?\$?type["|']?:/g, '"type":')
      .replace(/["|']?\$?description["|']?:/g, '"comment":')
      .replace(/(\d)_(?=\d)/g, '$1.')
    return JSON.parse(output)
  },
})

const getDestination = ({
  name,
  extension = 'json',
}: {
  name: string
  extension: string
}) => {
  return [name, 'tokens', extension].join('.')
}

const categories = [
  'borderRadius',
  'borderWidth',
  'colors',
  'maxWidth',
  'opacity',
  'screens',
  'spacing',
] as const

type TokenCategoryType = (typeof categories)[number]

const getSdJsConfig = (category: TokenCategoryType) => {
  return {
    source: ['./tailwindcss/tokens.json'],
    platforms: {
      js: {
        buildPath: './tailwindcss/tokens/javascript/',
        transformGroup: 'javascript',
        files: [
          {
            destination: getDestination({ name: category, extension: 'ts' }),
            format: 'typescript/custom',
            filter: (token: TransformedToken) => {
              return token.attributes?.category === category
            },
          },
        ],
      },
    },
  }
}

const getSdCssConfig = () => {
  return {
    source: ['./tailwindcss/tokens.json'],
    platforms: {
      css: {
        buildPath: './tailwindcss/tokens/css/',
        transformGroup: 'css',
        files: [
          {
            destination: getDestination({ name: 'ex-color', extension: 'css' }),
            format: 'css/variables',
            filter: (token: TransformedToken) => {
              return token.attributes?.category === 'ex-color'
            },
            options: {
              outputReferences: true,
            },
          },
        ],
      },
    },
  }
}

// Build default tokens from config
console.log('👷 Building default tokens')

categories.map((category: TokenCategoryType) => {
  console.log(`\n👷 Building ${category} tokens`)
  StyleDictionary.extend(getSdJsConfig(category)).buildAllPlatforms()
})

console.log(`\n👷 Building css tokens`)
StyleDictionary.extend(getSdCssConfig()).buildAllPlatforms()

スクリプトを実行すると、(以下はborderRadius)tokensディレクトリにtokens.jsonで指定したカテゴリのオブジェクトが生成されるので、こちらをTailwindCSSの設定ファイルで展開してあげます。

export default {
  '0': '0px',
  '2': '2px',
  '4': '4px',
  '8': '8px',
  DEFAULT: '1px',
} satisfies Record<string, string | Record<string, string>>

tailwind.config.tsファイルに設定

生成されたtsファイルをtailwind.config.tsファイル内に展開してあげます。

import type { Config } from 'tailwindcss'

import borderRadius from './tokens/javascript/borderRadius.tokens'
import borderWidth from './tokens/javascript/borderWidth.tokens'
import maxWidth from './tokens/javascript/maxWidth.tokens'
import opacity from './tokens/javascript/opacity.tokens'
import screens from './tokens/javascript/screens.tokens'

export default {
  content: [],
  theme: {
    extend: {
      borderRadius,
      borderWidth,
      maxWidth,
      opacity,
      screens,
      ...
    },
  },
} satisfies Config

結論

デザインデータの作成の際にTailwindCSSと同じデザイントークンを使用することが担保されたので、デザインデータの作成者とコーダーが異なる場合でも制作での認識の齟齬が起こりにくくなりました。

12
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
12
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?