
はじめに
こんにちは。
弁護士ドットコム株式会社でクラウドサインという電子契約サービスのフロントエンドを担当しています、神戸と言います。
クラウドサインでは、delta-wing-ui という社内で利用するための UI コンポーネントライブラリを開発しています。
その delta-wing-ui について、今回はどのような設計観点で作成しているかを少しご紹介します。
delta-wing-uiとは

delta-wing-ui は、クラウドサインのフロントエンドで利用する UI コンポーネントライブラリです。
以下の技術スタックで動作しています。
- Vue.js 3.5
- Storybook 9
- TypeScript
- Vite
設計観点
delta-wing-ui のコンポーネントを設計する際、以下 9 つの点を守るようにしています。
- デザインシステムの仕様書に準拠していること
- コーディングルールを満たしていること
- モダンな書き方になっていること
- Web 標準のセマンティック要素を使用していること(アクセシビリティを考慮していること)
- slot と props の使い分けをしていること
- CSS プリプロセッサを使わずに実装していること
- 属性の継承を制御していること
- CSF(Component Story Format)3 で記述していること
- props の Description を付与していること
デザインシステムの仕様書に準拠していること
クラウドサインには、デザインシステムの仕様書が存在しています。 仕様書は、クラウドサインのデザインを統一するためだけでなく、開発効率やユーザー体験の向上を目的としています。 各コンポーネントの使い方、種類、状態が定義されているため、コンポーネントを作成する際はデザインシステムの仕様書に沿っているかを確認しながら開発しています。
コーディングルールを満たしていること
クラウドサインには、フロントエンドのコーディングルールが存在しています。 UI コンポーネントライブラリも同様にコーディングルールに従って開発しています。 開発者が意識せずともコーディングルールを守るために、できるだけリンターでコーディングルールを網羅するようにしています。これは AI による実装時のガードレールとしても役立っています。
モダンな書き方になっていること
delta-wing-ui の Vue.js のバージョンは、3.5 を使用しています。 下記は、モダンな書き方の一例です。
- script setup
- defineProps/defineEmits/defineModel/defineSlots/defineExpose
- useTemplateRef
- CSS v-bind(動的スタイル)
モダンな書き方をすることにより、コードの可読性の向上やバンドルサイズを減らすことに繋がります。
また型推論が効くことにより、型安全になります。さまざまな恩恵を受けることができるため、モダンな書き方になっているかを意識して実装するようにしています。
Web 標準のセマンティック要素を使用していること(アクセシビリティを考慮していること)
一部の例ですが HTML には、dialog、nav、details、summary などのセマンティック要素があります。 意味のある要素を適切に使用することは、アクセシビリティの向上に繋がります。
アクセシビリティを考慮した実装は、クラウドサインのサービスを誰もが安心して利用できるようにするためのマストだと考えています。 クラウドサインには、有志でアクセシビリティの改善に取り組んでいるチームが存在しています。必要に応じて適宜、相談をしながら、アクセシビリティが考慮できているかを意識して実装するようにしています。
slot と props の使い分けをしていること
slot を適切に使用することは、コンポーネントの柔軟性を向上させることができます。 複雑な HTML 構造に対応し、親コンポーネントから制御できる点は大きなメリットです。 しかし、柔軟性が高すぎてしまうとコンポーネントの管理が難しくなってしまい、 デザインシステムの仕様書に沿った UI とならなくなってしまう可能性があります。 そのようにならないために、基本的にはデータの受け渡し、動作の制御、状態の変更をしたい場合は、props を使用します。 もし複雑な HTML に対応したい場合には、slot を使用するようにしています。
以下は、slot と props の使い分けの一例です。
Props使用例
<script setup lang="ts">
// データの受け渡し、動作の制御、状態の変更
const { text, disabled, size } = defineProps<{
text: string // データ: 表示するテキスト
disabled: boolean // 動作の制御: ボタンの有効/無効
size: 'small' | 'large' // 見た目の制御: サイズバリエーション
}>()
</script>
<template>
...
<!-- シンプルな値をそのまま表示 -->
<button :class="`btn-${size}`" :disabled>
{{ text }}
</button>
</template>
Slot使用例
<script setup lang="ts">
// 複雑なHTML構造への対応
defineSlots<{
header: () => unknown // タイトル + アイコンなど
body: () => unknown // テキスト + リスト + 画像など
footer: () => unknown // 複数のボタン + リンクなど
}>()
</script>
<template>
...
<!-- 親から渡される複雑なHTMLを配置 -->
<div class="card">
<div class="card-header">
<slot name="header" />
</div>
<div class="card-body">
<slot name="body" />
</div>
<div class="card-footer">
<slot name="footer" />
</div>
</div>
</template>
CSS プリプロセッサを使わずに実装していること
もともと、複数のサービスで SCSS の記法を使用していました。 delta-wing-ui も同様に、最初の導入時期では、SCSS の記法を使用していました。
近年の CSS は、変数が使用できたり、ネスト記法が使用できたりと日々進化しています。 SCSS と CSS の良さのそれぞれを知ったうえで、delta-wing-ui では CSS のみでも効率よく書けると判断しました。
属性の継承を制御していること
基底のコンポーネントなので、予期しない属性がルート要素に適用されてしまうことを防ぐようにしています。
以下、制御に至った理由です。
- class, style などが暗黙にルートへ流れ込み、意図しないデザインシステムの仕様書の上書きを招くため
- 受け取り可能な属性を props で明示しておくことで、どの属性が使用可能か分かるようにするため(オートコンプリートも効くようになる)
具体的には、inheritAttrs: false を使用しています。 ですが、グローバル属性の中でどの要素に対しても適用できるようにしておきたいものもあります。 そのため、許容する属性のみを設定できるよう、以下のようなコンポーザブル関数を作成しました。
import { computed, useAttrs } from 'vue'
export const useInheritableAttrs = () => {
const filterGlobalInheritableAttrs = (
attrs: Record<string, unknown>
): Record<string, unknown> => {
const htmlAttrKeys: readonly string[] = ['id', 'role', 'tabindex']
// HTML グローバル属性のキーをチェック
const isHtmlAttrKey = (key: string): boolean => htmlAttrKeys.includes(key)
// aria-* 属性のキーをチェック
const isAriaAttr = (key: string): boolean => key.startsWith('aria-')
// data-* 属性のキーをチェック
const isDataAttr = (key: string): boolean => key.startsWith('data-')
const isAllowedAttr = (key: string): boolean =>
isHtmlAttrKey(key) || isAriaAttr(key) || isDataAttr(key)
return Object.fromEntries(
Object.entries(attrs).filter(
([key, value]) => value !== undefined && isAllowedAttr(key)
)
)
}
const attrs = useAttrs()
const inheritableAttrs = computed(() => filterGlobalInheritableAttrs(attrs))
return {
inheritableAttrs
}
}
<script setup lang="ts">
import { useInheritableAttrs } from '@/composables/useInheritableAttrs'
defineOptions({
inheritAttrs: false
})
const { inheritableAttrs } = useInheritableAttrs()
</script>
<template>
<input v-bind="inheritableAttrs" />
</template>
これにより、予期しない属性の継承を防ぐようにしています。
CSF(Component Story Format) 3 で記述していること
UI コンポーネントをブラウザで確認するために、Storybook を使用しています。 もともと、delta-wing-ui に移行前のプロダクトでは、Storybook のバージョンが古いときに作成されたもので CSF 2 で実装されていました。
CSF 2 と比較して、CSF 3 では以下のようなメリットがあります。
- より簡潔で読みやすいコードが書けるようになった
- Template を作成して、各ストーリーを bind する必要がなくなった
- Meta /Story 型が改善されたことにより、今までより型安全になった
このような恩恵を受けるために、CSF 3 の書き方で書くようにしています。
props の Description を付与していること
コンポーネントの使い方を明確にするため、tsdoc の形式で、vue ファイルに props の Description を書いています。
defineProps<{
/** アラートの表示 */
visible: boolean
/** アラートのタイプ */
type: AlertType
/** アラートのテーマ */
theme: AlertTheme
/** アラートのメッセージ */
message: string
/** アラートの更新日時 */
updatedAt: string
/** アラートの閉じるボタンの表示 */
dismissible: boolean
}>()

Storybook はエンジニアだけでなく、デザイナーもデザインレビューで確認することが多いです。 双方にとって、わかりやすい説明を書くことが重要だと考えています。
その他
もしその他で判断に悩んだ場合は適宜、相談をしながら判断しています(例: デザインシステムの仕様書に未定義のコンポーネントや仕様の取り扱いについてデザイナーと相談)
まとめ
delta-wing-ui は、デザインシステムの仕様書への準拠、アクセシビリティの考慮、モダンな実装手法の採用など、複数の設計観点を軸に開発を進めています。 これらの観点に沿って実装することで、開発効率の向上、コードの品質担保、そして誰もが使いやすい UI の提供を実現しています。 現在は有志のメンバーで開発しており、まだ改善できる部分もあります。 今後も技術の進歩やプロダクトの成長に合わせて、これらの設計観点をアップデートしていき、誰もが使いやすい UI の提供を目指していきたいと考えています。