🦜

FigmaのVariants機能を利用したプロトタイピングと実装

2022/02/11に公開約8,700字

はじめに

今年に入ってからデザインツールの「Figma」を使い始めました。UIデザインのツールとしては他に「Adobe XD」や「Sketch」等がありますが、Figma の特徴としてブラウザ上で動作することや高度な共同編集機能などが挙げられます。UIデザインをする上でもたくさんの便利な機能がありますが、今回はその中で Variants 機能を利用したコンポーネントの作成と、作成したプロトタイプを元に Vue.js と Tailwind CSS を使用して実装を行った内容について書いてみようと思います。

Variants機能

Variants 機能はコンポーネントの複数の状態をまとめて管理できる Figma の機能です。

https://help.figma.com/hc/en-us/articles/360056440594

Variants 機能を利用する利点として、

  • コンポーネントが整理されて Assets パネルがスッキリする
  • パネル操作から簡単にコンポーネントの状態を切り替えることができる
  • Figma と実装するコードのプロパティを統一して管理しやすい

などが考えられます。


以下より、簡単なボタンコンポーネントのプロトタイプを作成して Variants 機能の使い方について確認してみます。

バリエーション分のコンポーネントを用意する

まず、必要なプロパティと値のバリエーション分のコンポーネントを用意します。今回は以下のように作成しました。

  • Type:Primary / Outline
  • State:Default / Hover
  • Iconの有無:true / false
  • Size:Small / Middle / Large

各コンポーネント名を使用するプロパティの値にしておくと、Variants を作成した際に自動で名前がつけられます(後から変更することも可能です)。
以下のようにコンポーネント名の後に/で区切ってプロパティの値を記述していきます。

[コンポーネント名] / [Type] / [Icon有無] / [State] / [Size]
例:Button/Primary/false/Default/Small

用意したコンポーネントを1つにまとめる

次に、作成したコンポーネントを全て選択してVariantsパネルから「Combine as Variants」ボタンをクリックします。

コンポーネントが1つにまとめられて、Variantsパネルに設定したプロパティの値が登録されていることが確認できます。

プロパティの名前はVariantsパネルから変更することができます。

これで完成です。Assetsパネル等から作成したボタンコンポーネントを配置して、パネル操作でコンポーネントの状態を変更できるようになりました。

その他補足

  • Variants 化されたコンポーネントを選択した際に右下に表示されるボタンをクリックすると、新しいコンポーネントが追加され、これを修正することで後から新しいバリエーションを追加することもできます。

  • プロパティの値をtrue/false(またはon/off)とすると、トグルボタンで状態の切り替えができるようになります。

実装

次に、作成したプロトタイプを元に実装を行います。今回はフレームワークに Vue.js と Tailwind CSS を使用しました。

サンプルで作成したコードの全体は、以下リポジトリに置いてあります。

https://github.com/K-shigehito/variants-test

Buttonコンポーネントを作成して、Figma で設定したプロパティに対応するPropsを定義しています。それを元にクラスを動的にバインディングしました。アイコンは真偽値で表示/非表示を切り替えています。Vue.js はv3.2から利用できる<script setup>構文で記述しました。

Button.vue
<script setup lang="ts">
import { computed } from 'vue';
import ButtonIcon from './IconSearch.vue';

// Figmaで作成したプロパティに対応するPropsを定義
interface Props {
  type: 'primary' | 'outline';
  size: 'small' | 'middle' | 'large';
  icon: boolean;
}
const props = withDefaults(defineProps<Props>(), {
  type: 'primary',
  size: 'small',
  icon: false,
});

// ボタンクリック時のイベント送信用のハンドラー
interface Emits {
  (e: 'click'): void;
}
const emit = defineEmits<Emits>();
const handleClick = () => {
  emit('click');
};

// typeプロパティに対応するクラス
const typeClass = computed(() => {
  if (props.type === 'primary') {
    return 'bg-emerald-600 text-emerald-50 hover:bg-emerald-800';
  } else if (props.type === 'outline') {
    return 'border border-emerald-800 text-emerald-800 hover:bg-emerald-50';
  } else {
    return '';
  }
});

// sizeプロパティに対応するクラス
const sizeClass = computed(() => {
  if (props.size === 'small') {
    return 'text-sm px-3.5 py-1.5 rounded';
  } else if (props.size === 'middle') {
    return 'text-md px-5 py-2 rounded-md';
  } else if (props.size === 'large') {
    return 'text-lg px-6 py-2.5 rounded-lg';
  } else {
    return '';
  }
});

// sizeプロパティによってアイコンのサイズも変更
const iconSize = computed(() => {
  if (props.size === 'small') {
    return 16;
  } else if (props.size === 'middle') {
    return 21;
  } else if (props.size === 'large') {
    return 28;
  } else {
    return '';
  }
});
</script>

<template>
  <button
    class="flex items-center gap-2.5 font-roboto"
    :class="[typeClass, sizeClass]"
    @click="handleClick"
  >
    <slot />
    <!-- アイコンがある場合は表示する -->
    <IconSearch
      v-if="props.icon"
      :width="iconSize"
      :height="iconSize"
    />
  </button>
</template>

これを親コンポーネントから以下のように使用しています。

ButtonSample.vue(抜粋)
<Button
  type="primary"
  size="small"
  :icon="false"
  @click="handleClick"
>
  Button
</Button>

上記のコードと Figma で作成したコンポーネントのパネルを見比べてみると、プロパティや値が一致しているので、デザインデータとコードで統一したコンポーネントの管理ができそうです。

アイコンをコンポーネント化する

現状、アイコンは1種類で表示/非表示を切り替えていましたが、アイコンを変更できるように新たにアイコンコンポーネントを作成してみます。

Figma に戻って、ボタンの時と同様にアイコンのバリエーションをすべて作成して Variants 化します。


これをボタンコンポーネント内のアイコンと差し替えると、入れ子のような形で コンポーネントの切り替えができるようになります。

実装側も同様に、アイコンコンポーネントを作成してPropsからアイコン名を受け取って切り替えができるように修正しました。

Button.vue
 <script setup lang="ts">
 // ...略
 interface Props {
   type: 'primary' | 'outline';
   size: 'small' | 'middle' | 'large';
-  icon: boolean;
+  icon?: string;
 }
 const props = withDefaults(defineProps<Props>(), {
   type: 'primary',
   size: 'small',
-  icon: false,
+  icon: '',
 });
 // ...略
 </script>

 <template>
   <button
     class="flex items-center gap-2.5 font-roboto"
     :class="[typeClass, sizeClass]"
     @click="handleClick"
   >
     <slot />
-    <IconSearch
-      v-if="props.icon"
-      :width="iconSize"
-      :height="iconSize"
-    />
+    <ButtonIcon
+      v-if="props.icon"
+      :icon="props.icon"
+      :width="iconSize"
+      :height="iconSize"
+    />
   </button>
 </template>

アイコンは Figma で作成したものを SVG 書き出ししてコンポーネント化しています。それを汎用的に利用するためのBaseIconコンポーネントとボタン内で使用する為のButtonIconコンポーネントを作成しました。

アイコンコンポーネント

ボタン内で使用するためのButtonIconコンポーネント

ButtonIcon.vue
<script setup lang="ts">
import BaseIcon from "./icons/BaseIcon.vue";
import IconSearch from "./icons/IconSearch.vue";
import IconEdit from "./icons/IconEdit.vue";
import IconFavorite from "./icons/IconFavorite.vue";

interface Props {
  icon: string;
  width: number | string;
  height: number | string;
}
const props = withDefaults(defineProps<Props>(), {
  icon: "",
  width: 10,
  height: 10,
});
</script>

<template>
  <BaseIcon :width="props.width" :height="props.height">
    <IconSearch v-if="props.icon === 'search'" />
    <IconEdit v-else-if="props.icon === 'edit'" />
    <IconFavorite v-else-if="props.icon === 'favorite'"></IconFavorite>
  </BaseIcon>
</template>

各 SVG アイコンを汎用的に使用するためのBaseIconコンポーネント

BaseIcon.vue
<script setup lang="ts">
interface Props {
  iconName?: string;
  width: number | string;
  height: number | string;
  iconColor?: string;
}
const props = withDefaults(defineProps<Props>(), {
  iconName: '',
  width: 20,
  height: 20,
  iconColor: 'currentColor',
});
</script>
<template>
  <svg
    xmlns="http://www.w3.org/2000/svg"
    :width="props.width"
    :height="props.height"
    x="0px"
    y="0px"
    viewBox="0 0 20 20"
    :aria-labelledby="props.iconName"
    role="presentation"
  >
    <title :id="props.iconName" lang="en">{{ props.iconName }} icon</title>
    <g :fill="props.iconColor">
      <slot />
    </g>
  </svg>
</template>

各アイコンコンポーネント(例:Searchアイコン)

IconSearch.vue
<template>
  <path
    fill-rule="evenodd"
    clip-rule="evenodd"
    d="M13.1824 8.21453C13.1824 10.9537 10.961 13.1786 8.21526 13.1786C5.46956 13.1786 3.24814 10.9537 3.24814 8.21453C3.24814 5.4754 5.46956 3.2505 8.21526 3.2505C10.961 3.2505 13.1824 5.4754 13.1824 8.21453ZM12.4599 14.0492C11.2688 14.917 9.80184 15.4291 8.21526 15.4291C4.23038 15.4291 1 12.199 1 8.21453C1 4.23005 4.23038 1 8.21526 1C12.2001 1 15.4305 4.23005 15.4305 8.21453C15.4305 9.80095 14.9184 11.2678 14.0505 12.4588L19 17.4077L17.4112 19L12.4599 14.0492Z"
    fill="currentColor"
  />
</template>

これで親コンポーネントからアイコン名でアイコンを変更できるようになりました。

ButtonSample.vue(抜粋)
 <Button
   type="primary"
   size="small"
-  :icon="false"
+  icon="search"
   @click="handleClick"
 >
   Button
 </Button>

まとめ

Variants 機能を利用すると、コンポーネントが整理されて利用しやすくなるのでとてもよいなと感じました。ただ、1つのコンポーネントにプロパティをたくさん持たせると、かえってごちゃごちゃしてしまう場合もあると思うので、丁度よい大きさで切り分けるのが良いかもしれないとも感じました(今回の場合、アイコンありは別コンポーネントにするなど)。
また、デザインデータとコード側とで同じプロパティ名を使用することで、コンポーネントの管理が統一できるのもよいなと感じました。
一方、1人でデザインと実装を行っている場合は正直二度手間のように感じることもあると思うので、Variants をどこまで作り込むかは場合によるのかもしれません。

Figma、まだ始めたばかりですがかなり楽しいです! どんどん使っていろんな機能に慣れていけたら良いなと思います。

参考

  • Create and use variants
    Variants 機能の解説記事です(公式)。

  • Figma Variants Playground
    Variants 機能のチュートリアルです。こちらで実際に操作しながら試してみるとだいたいの感じがつかめました。

  • Vue.js ビギナーズガイド 3.x対応
    Vue 3、TypeScript、Tailwind CSS、Vite 等を使用したコンポーネント開発が学べる良書です。今回もこちらの構成でサンプルを作成しました。

GitHubで編集を提案

Discussion

ログインするとコメントできます