⛈️

PrimeVue ベースの UI コンポーネントライブラリ Volt を使ってみよう

はじめに

https://volt.primevue.org/

Volt とは PrimeVueUnstyled モードを利用した新しい UI ライブラリです。
Volt はスタイリングを完全にコントロールできる PrimeVue ベースの UI ライブラリであり、UI コンポーネントはアプリケーションのコードベースに存在します。

Volt の各コンポーネントは Unstyled オプションを有効にした PrimeVue コンポーネントをラップしており、コンポーネントのスタイリングを完全にコントロールできるようになります。
このとき、Volt では Tailwind CSS を利用してスタイリングを行います。

Volt のコンポーネントはアプリケーションのコードベースに追加され、直接編集できます。
具体的には、コンポーネント追加のコマンドを実行することによって src/volt にコンポーネントの .vue ファイルがダウンロードされます。

今回はドキュメントに沿って Volt を導入し、コンポーネントを表示してみます。

Volt を導入する

Vue.js + Vite の構成で、アプリケーションの作成から Volt コンポーネントを 1 つ表示するところまでを示します。
コマンド例でのパッケージマネージャーは npm を利用します。

Node.js: v20.13.1
vue: v3.5.13
tailwindcss: v4.0.17
primevue: v4.3.3

今回の検証に利用したリポジトリは以下になります。

https://github.com/petaxa/hello-volt

Vue アプリケーションを作成する

Vue.js のドキュメントより、create-vue を利用してアプリケーションの作成します。

npm create vue@latest

今回のコード例では、TypeScript のみが有効になっています:

[+] TypeScript
[ ] JSX Support
[ ] Router (SPA development)
[ ] Pinia (state management)
[ ] Vitest (unit testing)
[ ] End-to-End Testing
[ ] ESLint (error prevention)
[ ] Prettier (code formatting)

Tailwind CSS を導入する

パッケージのインストール

Tailwind CSS に関連する以下のパッケージをインストールします。

  • tailwindcss: Tailwind CSS 本体
  • @tailwindcss/vite: Vite 向けのプラグイン
  • tailwindcss-primeui: PrimeVue 向けのプラグイン
  • tailwind-merge: Volt が提供するコード内で利用
npm install tailwindcss @tailwindcss/vite tailwindcss-primeui tailwind-merge

Vite プラグインの設定

先ほどインストールした Tailwind CSS の Vite プラグイン(@tailwindcss/vite)を vite.config.ts に追加します。

vite.config.ts
  import { fileURLToPath, URL } from 'node:url'

  import { defineConfig } from 'vite'
  import vue from '@vitejs/plugin-vue'
  import vueDevTools from 'vite-plugin-vue-devtools'
+ import tailwindcss from '@tailwindcss/vite'

  // https://vite.dev/config/
  export default defineConfig({
    plugins: [
      vue(),
      vueDevTools(),
+     tailwindcss()
    ],
    resolve: {
      alias: {
        '@': fileURLToPath(new URL('./src', import.meta.url))
      },
    },
  })

CSS ファイルへの設定

CSS ファイルへ Tailwind CSS と PrimeVue 向けプラグインのインポート文を追記します。

assets/base.css
+ @import 'tailwindcss';
+ @import 'tailwindcss-primeui';

  /* color palette from <https://github.com/vuejs/theme> */
  :root {
    --vt-c-white: #ffffff;
    --vt-c-white-soft: #f8f8f8;
    --vt-c-white-mute: #f2f2f2;

    --vt-c-black: #181818;
    --vt-c-black-soft: #222222;
    --vt-c-black-mute: #282828;

    --vt-c-indigo: #2c3e50;
  ...

PrimeVue を導入

パッケージをインストールし、Vue プラグインをインストールするコードを記述します。

npm install primevue
main.ts
  import './assets/main.css'

  import { createApp } from 'vue'
  import App from './App.vue'
+ import PrimeVue from 'primevue/config'

- createApp(App).mount('#app')
+ const app = createApp(App)
+ app.use(PrimeVue, {
+   unstyled: true,
+ });
+
+ app.mount('#app');

CSS Variables の定義

tailwind-primeui が利用する CSS Variables のデフォルト値を定義します。
このステップは、将来的には primeui tailwind plugin が暗黙的に行うようになるようです。[1]

CSS Variables はドキュメントに記載されているものを src/assets/base.css に追記します。
今回は既に記述されているセレクタ内に追記する形で実装します。

assets/base.css
  @import 'tailwindcss';
  @import 'tailwindcss-primeui';

  /* color palette from <https://github.com/vuejs/theme> */
  :root {
    --vt-c-white: #ffffff;
    --vt-c-white-soft: #f8f8f8;
    --vt-c-white-mute: #f2f2f2;

    --vt-c-black: #181818;
    --vt-c-black-soft: #222222;
    --vt-c-black-mute: #282828;

    --vt-c-indigo: #2c3e50;

    --vt-c-divider-light-1: rgba(60, 60, 60, 0.29);
    --vt-c-divider-light-2: rgba(60, 60, 60, 0.12);
    --vt-c-divider-dark-1: rgba(84, 84, 84, 0.65);
    --vt-c-divider-dark-2: rgba(84, 84, 84, 0.48);

    --vt-c-text-light-1: var(--vt-c-indigo);
    --vt-c-text-light-2: rgba(60, 60, 60, 0.66);
    --vt-c-text-dark-1: var(--vt-c-white);
    --vt-c-text-dark-2: rgba(235, 235, 235, 0.64);

+   --p-primary-50: #ecfdf5;
+   --p-primary-100: #d1fae5;
+   --p-primary-200: #a7f3d0;
+   --p-primary-300: #6ee7b7;
+   --p-primary-400: #34d399;
+   --p-primary-500: #10b981;
+   --p-primary-600: #059669;
+   --p-primary-700: #047857;
+   --p-primary-800: #065f46;
+   --p-primary-900: #064e3b;
+   --p-primary-950: #022c22;
+   --p-surface-0: #ffffff;
+   --p-surface-50: #fafafa;
+   --p-surface-100: #f4f4f5;
+   --p-surface-200: #e4e4e7;
+   --p-surface-300: #d4d4d8;
+   --p-surface-400: #a1a1aa;
+   --p-surface-500: #71717a;
+   --p-surface-600: #52525b;
+   --p-surface-700: #3f3f46;
+   --p-surface-800: #27272a;
+   --p-surface-900: #18181b;
+   --p-surface-950: #09090b;
+   --p-content-border-radius: 6px;
  }

  /* semantic color variables for this project */
  :root {
    --color-background: var(--vt-c-white);
    --color-background-soft: var(--vt-c-white-soft);
    --color-background-mute: var(--vt-c-white-mute);

    --color-border: var(--vt-c-divider-light-2);
    --color-border-hover: var(--vt-c-divider-light-1);

    --color-heading: var(--vt-c-text-light-1);
    --color-text: var(--vt-c-text-light-1);

    --section-gap: 160px;

+   --p-primary-color: var(--p-primary-500);
+   --p-primary-contrast-color: var(--p-surface-0);
+   --p-primary-hover-color: var(--p-primary-600);
+   --p-primary-active-color: var(--p-primary-700);
+   --p-content-border-color: var(--p-surface-200);
+   --p-content-hover-background: var(--p-surface-100);
+   --p-content-hover-color: var(--p-surface-800);
+   --p-highlight-background: var(--p-primary-50);
+   --p-highlight-color: var(--p-primary-700);
+   --p-highlight-focus-background: var(--p-primary-100);
+   --p-highlight-focus-color: var(--p-primary-800);
+   --p-text-color: var(--p-surface-700);
+   --p-text-hover-color: var(--p-surface-800);
+   --p-text-muted-color: var(--p-surface-500);
+   --p-text-hover-muted-color: var(--p-surface-600);
  }

  @media (prefers-color-scheme: dark) {
    :root {
      --color-background: var(--vt-c-black);
      --color-background-soft: var(--vt-c-black-soft);
      --color-background-mute: var(--vt-c-black-mute);

      --color-border: var(--vt-c-divider-dark-2);
      --color-border-hover: var(--vt-c-divider-dark-1);

      --color-heading: var(--vt-c-text-dark-1);
      --color-text: var(--vt-c-text-dark-2);

+     --p-primary-color: var(--p-primary-400);
+     --p-primary-contrast-color: var(--p-surface-900);
+     --p-primary-hover-color: var(--p-primary-300);
+     --p-primary-active-color: var(--p-primary-200);
+     --p-content-border-color: var(--p-surface-700);
+     --p-content-hover-background: var(--p-surface-800);
+     --p-content-hover-color: var(--p-surface-0);
+     --p-highlight-background: color-mix(in srgb, var(--p-primary-400), transparent 84%);
+     --p-highlight-color: rgba(255, 255, 255, 0.87);
+     --p-highlight-focus-background: color-mix(in srgb, var(--p-primary-400), transparent 76%);
+     --p-highlight-focus-color: rgba(255, 255, 255, 0.87);
+     --p-text-color: var(--p-surface-0);
+     --p-text-hover-color: var(--p-surface-0);
+     --p-text-muted-color: var(--p-surface-400);
+     --p-text-hover-muted-color: var(--p-surface-300);
    }
  }

  *,
  *::before,
  *::after {
    box-sizing: border-box;
    margin: 0;
    font-weight: normal;
  }

  body {
    min-height: 100vh;
    color: var(--color-text);
    background: var(--color-background);
    transition:
      color 0.5s,
      background-color 0.5s;
    line-height: 1.6;
    font-family:
      Inter,
      -apple-system,
      BlinkMacSystemFont,
      'Segoe UI',
      Roboto,
      Oxygen,
      Ubuntu,
      Cantarell,
      'Fira Sans',
      'Droid Sans',
      'Helvetica Neue',
      sans-serif;
    font-size: 15px;
    text-rendering: optimizeLegibility;
    -webkit-font-smoothing: antialiased;
    -moz-osx-font-smoothing: grayscale;
  }

Volt コンポーネントのダウンロード

Volt コンポーネントは volt-vue コマンドを実行することで追加できます。
npx volt-vue コンポーネント名 で指定したコンポーネントをダウンロードできます。
また、コンポーネント名を all とするとすべてのコンポーネントを一括でダウンロードします。
コマンドを実行すると、PrimeVue の公式リポジトリから最新のコンポーネントファイルがダウンロードされます。[2]

コンポーネント名はドキュメントを確認してください。
GitHub リポジトリにすべてのコンポーネントの Vue ファイルが存在するため、これらも参考にできます。

今回は Button コンポーネントをダウンロードし、後続の手順で表示します。

npx volt-vue add Button

コマンドの実行が完了すると src/volt/Button.vue が追加されています。

Volt コンポーネントの表示

Vue の SFC で Volt コンポーネントを利用してみます。
今回は src/components/TheWelcome.vue の先頭に先ほどダウンロードした Button.vue を表示させます。

src/components/TheWelcome.vue
  <script setup lang="ts">
  import WelcomeItem from './WelcomeItem.vue'
  import DocumentationIcon from './icons/IconDocumentation.vue'
  import ToolingIcon from './icons/IconTooling.vue'
  import EcosystemIcon from './icons/IconEcosystem.vue'
  import CommunityIcon from './icons/IconCommunity.vue'
  import SupportIcon from './icons/IconSupport.vue'
+ import Button from '@/volt/Button.vue'

  const openReadmeInEditor = () => fetch('/__open-in-editor?file=README.md')
  </script>

  <template>
+   <Button label="Hello World" />
    <WelcomeItem>
      <template #icon>
        <DocumentationIcon />
      </template>
      <template #heading>Documentation</template>
  ...

これにより表示ができているはずです。
Dev サーバーを起動して確認してみます。

npm run dev

create-vue 作成直後のページの先頭に先ほど追加した Volt の Button コンポーネントが表示されている

追加された Volt コンポーネントを眺めてみる

アプリケーションコードにダウンロードしたコンポーネントファイルの中身を見てみます。
src/volt/Button.vue は以下のようなファイルです。

src/volt/Button.vue
<template>
    <Button
        unstyled
        :pt="theme"
        :ptOptions="{
            mergeProps: ptViewMerge
        }"
    >
        <template v-for="(_, slotName) in $slots" v-slot:[slotName]="slotProps">
            <slot :name="slotName" v-bind="slotProps ?? {}" />
        </template>
    </Button>
</template>

<script setup lang="ts">
import Button, { type ButtonPassThroughOptions, type ButtonProps } from 'primevue/button';
import { ref } from 'vue';
import { ptViewMerge } from './utils';

interface Props extends /* @vue-ignore */ ButtonProps {}
defineProps<Props>();

const theme = ref<ButtonPassThroughOptions>({
    root: `inline-flex cursor-pointer select-none items-center justify-center overflow-hidden relative
        px-3 py-2 gap-2 rounded-md disabled:pointer-events-none disabled:opacity-60 transition-colors duration-200
        bg-primary enabled:hover:bg-primary-emphasis enabled:active:bg-primary-emphasis-alt text-primary-contrast
        border border-primary enabled:hover:border-primary-emphasis enabled:active:border-primary-emphasis-alt
        focus-visible:outline focus-visible:outline-1 focus-visible:outline-offset-2 focus-visible:outline-primary
        p-vertical:flex-col p-fluid:w-full p-fluid:p-icon-only:w-10
        p-icon-only:w-10 p-icon-only:px-0 p-icon-only:gap-0
        p-icon-only:p-rounded:rounded-full p-icon-only:p-rounded:h-10
        p-small:text-sm p-small:px-[0.625rem] p-small:py-[0.375rem]
        p-large:text-[1.125rem] p-large:px-[0.875rem] p-large:py-[0.625rem]
        p-raised:shadow-sm p-rounded:rounded-[2rem]
        p-outlined:bg-transparent enabled:hover:p-outlined:bg-primary-50 enabled:active:p-outlined:bg-primary-100
        p-outlined:border-primary-200 enabled:hover:p-outlined:border-primary-200 enabled:active:p-outlined:border-primary-200
        p-outlined:text-primary enabled:hover:p-outlined:text-primary enabled:active:p-outlined:text-primary
        dark:p-outlined:bg-transparent dark:enabled:hover:p-outlined:bg-primary/5 dark:enabled:active:p-outlined:bg-primary/15
        dark:p-outlined:border-primary-700 dark:enabled:hover:p-outlined:border-primary-700 dark:enabled:active:p-outlined:border-primary-700
        dark:p-outlined:text-primary dark:enabled:hover:p-outlined:text-primary dark:enabled:active:p-outlined:text-primary
        p-text:bg-transparent enabled:hover:p-text:bg-primary-50 enabled:active:p-text:bg-primary-100
        p-text:border-transparent enabled:hover:p-text:border-transparent enabled:active:p-text:border-transparent
        p-text:text-primary enabled:hover:p-text:text-primary enabled:active:p-text:text-primary
        dark:p-text:bg-transparent dark:enabled:hover:p-text:bg-primary/5 dark:enabled:active:p-text:bg-primary/15
        dark:p-text:border-transparent dark:enabled:hover:p-text:border-transparent dark:enabled:active:p-text:border-transparent
        dark:p-text:text-primary dark:enabled:hover:p-text:text-primary dark:enabled:active:p-text:text-primary
    `,
    loadingIcon: ``,
    icon: `p-right:order-1 p-bottom:order-2`,
    label: `font-medium p-icon-only:invisible p-icon-only:w-0
        p-small:text-sm p-large:text-[1.125rem]`,
    pcBadge: {
        root: `min-w-4 h-4 leading-4 bg-primary-contrast rounded-full text-primary text-xs font-bold`
    }
});
</script>
  • PrimeVue のコンポーネントを unstyled プロパティを付けて利用している
    • PrimeVue を導入 で Unstyled モードの設定をしなくても(おそらく)変わらず[3]動作する
  • theme 変数の root に登録されている Tailwind CSS のクラス名を変更することでスタイリングを変更できる

おわりに

公式ドキュメントでも触れられていますが、PrimeVue と Volt はそれぞれ異なる要件を満たすために作成されています。
Volt はコンポーネントのスタイリングを完全にコントロールできますが、必要最低限のコンポーネントのみを実装しています。
また、PrimeVue に Tailwind CSS を導入する使い方から一歩進んで、Volt ではコンポーネントを Tailwind CSS でテーマ化できます。

- PrimeVue Volt
コンポーネントの位置 node_modules アプリケーションコードベース
スタイリング @primevue/themes とデザイントークン Tailwind CSS のユーティリティ
ダウンロード元 NPM レジストリ GitHub のリポジトリ
コンポーネント数 90 個程度 50 個程度

用途に合わせて使い分けることができそうです。

脚注
  1. With a future update of the primeui tailwind plugin, this step will be done implicitly. ↩︎

  2. this tool automatically fetches the latest components from the official PrimeVue repository ↩︎

  3. Unstyled モードをオフにしても見た目には変化がありませんでした。機能面の細かい部分については検証していません。 ↩︎

Vue・Nuxt 情報が集まる広場 / Plaza for Vue・Nuxt.

Discussion