Yappli Tech Blog

株式会社ヤプリの開発メンバーによるブログです。最新の技術情報からチーム・働き方に関するテーマまで、日々の熱い想いを持って発信していきます。

デザインシステム構築に向けた Figma API によるCSSカスタムプロパティの自動生成

こんにちは。フロントエンドエンジニアの小林(baco16g)です。

この記事では、YappliのCMSにおけるデザインシステムを構築に向けた準備をしている話を、エンジニア視点でお伝えします。

デザインシステムが求められた背景

YappliのCMSは、2019年にシステム・デザインともに刷新されました。しかし、開発着手からは約4年の歳月が流れ、当初の実装ガイドラインはもはや形骸化し始めています。

実装ガイドラインが形骸化し、明確なルールが無い状態で異なるプロジェクトの実装が進んだ結果、実装されたプロダクトとデザインファイルに乖離が生まれてしまいました。例えば、下記のような乖離です。

  • TypographyやSpacingなどのデザイントークンの値が異なる
  • デザイントークンの命名が、実装とデザインファイルが異なる
  • デザインファイルに存在しないUIコンポーネントが実装には存在する
f:id:ykob_yapp:20220329195101p:plain
TypographyやSpacingの乖離の一例

このような実装とデザインファイルの乖離により、アウトプットのデザイン自体の品質と、それを是正するためのエンジニアとデザイナー間のコミュニケーションコストが増してしまいます。

CMSの開発に新たなプロダクトデザイナーがジョインした事をきっかけに課題が表面化し、デザインシステムが求められるようになりました。

note.com

まずはデザイントークンからSyncする

デザイントークンとは、色、余白、行間、フォント、フォントサイズ、シャドウなどの最小単位のスタイルの情報を定数として定義したものです。UIコンポーネントは、これらのデザイントークンを組み合わせて構築されるため、デザイントークンはデザインシステムを構築する上で欠かせない要素です。

今回は「Figmaからデザイントークンをどのように書き出したのか」をお伝えします。

Figmaからデザイントークンをどのように書き出したのか

Figma APIからJSON形式でFigma Stylesを取得して、CSSカスタムプロパティを自動定義する仕組みを作りました。

今回はFigma APIに型定義を加えた figma-api を使用しました。下記ではTypography一覧を取得する処理を抜粋して掲載しています。

www.npmjs.com

import { Api as FigmaAPI, Node } from 'figma-api';

export const listStyles = async (token: string, fileId: string) => {
  try {
    const api = new FigmaAPI({ personalAccessToken: token });
    
    // 指定したファイルIDのFigma Stylesを取得
    const styles = await api.getFileStyles(fileId).then(data => data.meta?.styles || []);
    const nodeIds = styles.filter(style => style.style_type === 'TEXT').map(t => t.node_id);
    const typographies = await getTypographies(
      api,
      fileId,
      nodeIds
    );
    return { typographies };
  } catch (e) {
    return null;
  }
};

/**
 * Figma StyleのNodeIDからNode情報を取得
 */
const getTypographies = async (
  api: InstanceType<typeof FigmaAPI>,
  fileId: string,
  nodeIds: string[]
) => {
  if (nodeIds.length === 0) return [];
  return api.getFileNodes(fileId, nodeIds).then(file =>
    Object.entries(file.nodes)
      .map(node => [node[0], node[1]?.document])
      .filter((node): node is [string, Node<'TEXT'>] => !!node[1])
      .map(mapTextNode);
  );
};

/**
 * Nodeのスタイル情報をマッピングして返す
 */
const mapTextNode = ([nodeId, node]: [string, Node<'TEXT'>]) => {
  return {
    nodeId,
    styleName: node.name,
    fontFamily: node.style.fontFamily,
    fontSize: `${node.style.fontSize / 10}rem`,
    fontWeight: node.style.fontWeight,
    lineHeight: Math.round(node.style.lineHeightPercentFontSize || 100) / 100,
  };
};

続いて、取得したFigma StylesをCSSファイルに出力します。

export const buildTypography = (typographies: Typography[]) => {
  const cssCustomProperties = typographies.reduce((acc, curr) => {
    const name = formatToStyleName(curr.styleName);
    return `${acc}
      --typography-${name}-font-family: "${curr.fontFamily}", sans-serif;
      --typography-${name}-font-size: ${curr.fontSize};
      --typography-${name}-font-weight: ${curr.fontWeight};
      --typography-${name}-line-height: ${curr.lineHeight};
      --typography-${name}: ${curr.fontWeight} ${curr.fontSize}/${curr.lineHeight} var(--typography-${name}-font-family);
    `;
  }, '');
  return formatScss(dedent`
    :root {
      ${cssCustomProperties}
    }
  `);
};

const formatToStyleName = (s: string) => {
  // CSSカスタムプロパティ名で扱える文字列にフォーマットします
}

最終的に出力されたファイルの抜粋がこちらです。

// ------------------------------------------------------
// THIS FILE WAS AUTOMATICALLY GENERATED (DO NOT MODIFY)
// ------------------------------------------------------

:root {
  --typography-body-01-long-font-family: 'Noto Sans JP', sans-serif;
  --typography-body-01-long: 400 1.6rem/1.5
    var(--typography-body-01-long-font-family);
}

:root {
  // ------------------------------------------------------
  // プリミティブトークン - 使用を推奨していません。エイリアストークンを使用してください。
  // ------------------------------------------------------
  --color-core-blue-04: #59bfee;
  --color-core-blue-05: #00a9e0;
  --color-core-blue-06: #35a6dc;
  --color-core-blue-07: #3892bc;

  // ------------------------------------------------------
  // エイリアストークン - 基本的にはこちらの使用してください。
  // ------------------------------------------------------
  --color-alias-brand: var(--color-core-blue-05);
  --color-alias-primary-default: var(--color-core-blue-06);
  --color-alias-primary-focus: var(--color-core-blue-07);
  --color-alias-primary-hover: var(--color-core-blue-04);
}

// 適用例
p {
  color: var(--color-alias-primary-default);
  font: var(--typography-body-01-long);
}

従来ではデザイントークンをSCSS変数で管理していましたが、Vueファイルのテンプレート内で利用する場合や将来的なダークモードの対応を見据えると、SCSS変数では柔軟な対応が難しいと判断し、今回を機にCSSカスタムプロパティへ変更しました。

また、Typographyは 一括指定プロパティのfontで利用することを想定しています。これは、MDN Web Docsの実装を参考にしました。

おわりに

冒頭でお伝えした通り、「デザインシステムを構築に向けた準備をしている話」なので、未だ実適用は完了していません。デザインファイルと実装で明確なデザイントークンを定義・運用することで、今後のプロダクトのデザイン品質向上と、大幅な開発の効率化が見込まれます。

まだまだ理想のデザインシステム構築を完了するまで多くの作業が残ってますが、プロダクトをより良くするために引き続き対応を進めていきます!