Open PropsでCSS設計を効率化する実践ガイド


はじめに

Open Propsは、Adam Argyleが開発したCSS Custom Properties(カスタムプロパティ)のコレクションである。Tailwind CSSのようなユーティリティクラスではなく、CSS変数としてデザイントークンを提供するという独自のアプローチを取る。

Open Propsを導入すると、一貫性のあるデザインシステムの基盤を即座に利用できる。カラーパレット、タイポグラフィスケール、スペーシング、イージング関数、アニメーション、シャドウなど、デザインに必要なトークンが網羅されている。

2026年現在、Open Propsはnpmで月間100万ダウンロードを超え、CSS Custom Propertiesベースのデザインシステムとして広く採用されている。本記事では、Open Propsの導入から実践的な活用方法まで体系的に解説する。

対象読者

  • CSS設計の一貫性に課題を感じているフロントエンドエンジニア
  • デザインシステムの構築を検討しているチーム
  • Tailwind CSS以外のスタイリングアプローチを探している開発者

Open Propsの設計思想

Open Propsは以下の3つの原則に基づいている。

原則説明
CSS標準準拠フレームワーク非依存。ブラウザネイティブのCSS Custom Propertiesを使用
ゼロランタイムビルド時に不使用変数を除去可能。ランタイムコスト最小
段階的採用必要なカテゴリだけ選択的にインポート可能

インストールと基本設定

npmによるインストール

npm install open-props

CDNによる利用

開発・プロトタイピング時はCDNから直接読み込むことも可能である。

<!-- 全トークンを一括読み込み -->
<link rel="stylesheet" href="https://unpkg.com/open-props" />

<!-- ノーマライズ付き -->
<link rel="stylesheet" href="https://unpkg.com/open-props/normalize.min.css" />

選択的インポート

全トークンを読み込む必要はない。プロジェクトで使用するカテゴリだけをインポートすることでファイルサイズを最小化できる。

/* 必要なカテゴリのみインポート */
@import 'open-props/colors.min.css';
@import 'open-props/sizes.min.css';
@import 'open-props/fonts.min.css';
@import 'open-props/shadows.min.css';
@import 'open-props/easings.min.css';
@import 'open-props/animations.min.css';
@import 'open-props/borders.min.css';
@import 'open-props/aspects.min.css';
@import 'open-props/zindex.min.css';

PostCSSによるJIT(Just-In-Time)ビルド

未使用の変数を自動除去するには、PostCSSプラグインを使用する。

npm install postcss postcss-jit-props --save-dev
// postcss.config.js
const postcssJitProps = require('postcss-jit-props');
const OpenProps = require('open-props');

module.exports = {
  plugins: [
    postcssJitProps(OpenProps),
  ],
};

JITモードでは、CSSファイル内で実際に使用されているvar(--XXX)のみが出力に含まれる。数KB程度のOpen Props変数だけがバンドルされるため、パフォーマンスへの影響は最小限である。

カラーシステム

Open Propsのカラーシステムはoklch色空間を採用している。人間の知覚に基づいた均等な色空間で、一貫性のあるパレット生成が可能である。

カラーパレットの構造

各色は0(最も明るい)から12(最も暗い)の13段階で提供される。

/* Open Propsが提供するカラー変数の例 */
:root {
  /* Blue系 */
  --blue-0: oklch(0.97 0.01 240);   /* 最も明るい */
  --blue-1: oklch(0.93 0.03 240);
  --blue-2: oklch(0.88 0.06 240);
  --blue-3: oklch(0.82 0.09 240);
  --blue-4: oklch(0.74 0.13 240);
  --blue-5: oklch(0.66 0.17 240);
  --blue-6: oklch(0.57 0.2 240);
  --blue-7: oklch(0.49 0.2 240);    /* 中間 */
  --blue-8: oklch(0.42 0.19 240);
  --blue-9: oklch(0.36 0.16 240);
  --blue-10: oklch(0.3 0.13 240);
  --blue-11: oklch(0.24 0.09 240);
  --blue-12: oklch(0.18 0.06 240);  /* 最も暗い */

  /* 利用可能な色相 */
  /* gray, stone, red, pink, purple, violet, indigo, blue,
     cyan, teal, green, lime, yellow, orange, choco, brown, sand, jungle */
}

実践的なカラー設計

プロジェクト固有のセマンティックカラーを、Open Propsのトークンから派生させる。

:root {
  /* プライマリカラー */
  --color-primary: var(--blue-7);
  --color-primary-light: var(--blue-4);
  --color-primary-dark: var(--blue-9);
  --color-primary-surface: var(--blue-1);

  /* セカンダリカラー */
  --color-secondary: var(--violet-7);
  --color-secondary-light: var(--violet-4);
  --color-secondary-dark: var(--violet-9);

  /* セマンティックカラー */
  --color-success: var(--green-7);
  --color-success-surface: var(--green-1);
  --color-warning: var(--yellow-6);
  --color-warning-surface: var(--yellow-1);
  --color-error: var(--red-7);
  --color-error-surface: var(--red-1);
  --color-info: var(--cyan-7);
  --color-info-surface: var(--cyan-1);

  /* テキストカラー */
  --color-text-primary: var(--gray-12);
  --color-text-secondary: var(--gray-8);
  --color-text-tertiary: var(--gray-6);
  --color-text-inverse: var(--gray-0);

  /* サーフェスカラー */
  --color-surface-1: white;
  --color-surface-2: var(--gray-1);
  --color-surface-3: var(--gray-2);
  --color-surface-4: var(--gray-3);

  /* ボーダーカラー */
  --color-border: var(--gray-3);
  --color-border-strong: var(--gray-5);
}

カラーパレットの活用例

/* ステータスバッジ */
.badge {
  display: inline-flex;
  align-items: center;
  gap: var(--size-1);
  padding: var(--size-1) var(--size-3);
  border-radius: var(--radius-round);
  font-size: var(--font-size-0);
  font-weight: var(--font-weight-6);
  line-height: 1;
}

.badge-success {
  background: var(--color-success-surface);
  color: var(--green-9);
  border: 1px solid var(--green-3);
}

.badge-warning {
  background: var(--color-warning-surface);
  color: var(--yellow-9);
  border: 1px solid var(--yellow-3);
}

.badge-error {
  background: var(--color-error-surface);
  color: var(--red-9);
  border: 1px solid var(--red-3);
}

.badge-info {
  background: var(--color-info-surface);
  color: var(--cyan-9);
  border: 1px solid var(--cyan-3);
}

タイポグラフィスケール

Open Propsは数学的に調和したタイポグラフィスケールを提供する。

フォントサイズ

/* Open Propsのフォントサイズ変数 */
:root {
  --font-size-00: 0.5rem;    /* 8px */
  --font-size-0: 0.75rem;    /* 12px */
  --font-size-1: 1rem;       /* 16px - 基準 */
  --font-size-2: 1.1rem;     /* 17.6px */
  --font-size-3: 1.25rem;    /* 20px */
  --font-size-4: 1.5rem;     /* 24px */
  --font-size-5: 2rem;       /* 32px */
  --font-size-6: 2.5rem;     /* 40px */
  --font-size-7: 3.5rem;     /* 56px */
  --font-size-8: 6rem;       /* 96px */

  /* Fluid(ビューポートに応じて可変) */
  --font-size-fluid-0: clamp(0.75rem, 2vw, 1rem);
  --font-size-fluid-1: clamp(1rem, 4vw, 1.5rem);
  --font-size-fluid-2: clamp(1.5rem, 6vw, 2.5rem);
  --font-size-fluid-3: clamp(2rem, 9vw, 3.5rem);
}

実践的なタイポグラフィ設計

/* タイポグラフィシステム */
body {
  font-family: var(--font-sans);
  font-size: var(--font-size-1);
  line-height: var(--font-lineheight-3);
  color: var(--color-text-primary);
}

h1 {
  font-size: var(--font-size-fluid-3);
  font-weight: var(--font-weight-9);
  line-height: var(--font-lineheight-0);
  letter-spacing: var(--font-letterspacing-1);
}

h2 {
  font-size: var(--font-size-fluid-2);
  font-weight: var(--font-weight-7);
  line-height: var(--font-lineheight-1);
  letter-spacing: var(--font-letterspacing-1);
}

h3 {
  font-size: var(--font-size-fluid-1);
  font-weight: var(--font-weight-6);
  line-height: var(--font-lineheight-2);
}

h4 {
  font-size: var(--font-size-3);
  font-weight: var(--font-weight-6);
  line-height: var(--font-lineheight-2);
}

.text-small {
  font-size: var(--font-size-0);
  line-height: var(--font-lineheight-2);
}

.text-caption {
  font-size: var(--font-size-00);
  line-height: var(--font-lineheight-1);
  text-transform: uppercase;
  letter-spacing: var(--font-letterspacing-4);
  color: var(--color-text-tertiary);
}

/* コードブロック */
code, pre {
  font-family: var(--font-mono);
  font-size: var(--font-size-0);
}

pre {
  padding: var(--size-4);
  border-radius: var(--radius-2);
  background: var(--gray-12);
  color: var(--gray-2);
  overflow-x: auto;
  line-height: var(--font-lineheight-4);
}

Fluidタイポグラフィの活用

Fluidフォントサイズはclamp()を使い、ビューポート幅に応じてスムーズにサイズが変化する。メディアクエリによるブレークポイントベースの切り替えと異なり、すべての画面幅で最適なサイズが自動計算される。

/* Fluidタイポグラフィを使ったヒーローセクション */
.hero {
  padding: var(--size-fluid-4) var(--size-fluid-2);
  text-align: center;
}

.hero-title {
  font-size: var(--font-size-fluid-3);
  font-weight: var(--font-weight-9);
  line-height: var(--font-lineheight-0);
  background: linear-gradient(
    to right,
    var(--blue-7),
    var(--violet-7)
  );
  -webkit-background-clip: text;
  -webkit-text-fill-color: transparent;
  background-clip: text;
}

.hero-subtitle {
  font-size: var(--font-size-fluid-1);
  color: var(--color-text-secondary);
  max-inline-size: var(--size-content-2);
  margin-inline: auto;
  margin-block-start: var(--size-3);
}

スペーシングとサイズ

Open Propsは一貫したスペーシングスケールを提供する。

サイズ変数

/* Open Propsのサイズ変数 */
:root {
  --size-000: -0.5rem;
  --size-00: -0.25rem;
  --size-1: 0.25rem;   /* 4px */
  --size-2: 0.5rem;    /* 8px */
  --size-3: 1rem;      /* 16px */
  --size-4: 1.25rem;   /* 20px */
  --size-5: 1.5rem;    /* 24px */
  --size-6: 1.75rem;   /* 28px */
  --size-7: 2rem;      /* 32px */
  --size-8: 3rem;      /* 48px */
  --size-9: 4rem;      /* 64px */
  --size-10: 5rem;     /* 80px */
  --size-11: 7.5rem;   /* 120px */
  --size-12: 10rem;    /* 160px */
  --size-13: 15rem;    /* 240px */
  --size-14: 20rem;    /* 320px */
  --size-15: 30rem;    /* 480px */

  /* Fluidサイズ */
  --size-fluid-1: clamp(0.5rem, 1vw, 1rem);
  --size-fluid-2: clamp(1rem, 2vw, 1.5rem);
  --size-fluid-3: clamp(1.5rem, 3vw, 2rem);
  --size-fluid-4: clamp(2rem, 4vw, 3rem);
  --size-fluid-5: clamp(4rem, 6vw, 6rem);

  /* コンテンツ幅 */
  --size-content-1: 20ch;
  --size-content-2: 45ch;
  --size-content-3: 60ch;
}

レイアウトでの活用

/* カードコンポーネント */
.card {
  padding: var(--size-5);
  border-radius: var(--radius-3);
  background: var(--color-surface-1);
  box-shadow: var(--shadow-2);
  display: grid;
  gap: var(--size-3);
}

.card-header {
  display: flex;
  align-items: center;
  gap: var(--size-2);
}

.card-body {
  max-inline-size: var(--size-content-3);
  line-height: var(--font-lineheight-3);
}

.card-footer {
  display: flex;
  gap: var(--size-2);
  justify-content: flex-end;
  padding-block-start: var(--size-3);
  border-block-start: 1px solid var(--color-border);
}

/* セクションスペーシング */
.section {
  padding-block: var(--size-fluid-5);
  padding-inline: var(--size-fluid-2);
}

.section-title {
  font-size: var(--font-size-fluid-2);
  margin-block-end: var(--size-fluid-3);
}

/* グリッドレイアウト */
.grid-auto {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 20rem), 1fr));
  gap: var(--size-5);
}

アニメーションとイージング

Open Propsは実用的なアニメーションとイージング関数のコレクションを提供する。

イージング関数

/* Open Propsのイージング変数 */
:root {
  --ease-1: cubic-bezier(0.25, 0, 0.5, 1);
  --ease-2: cubic-bezier(0.25, 0, 0.4, 1);
  --ease-3: cubic-bezier(0.25, 0, 0.3, 1);        /* 推奨デフォルト */
  --ease-4: cubic-bezier(0.25, 0, 0.2, 1);
  --ease-5: cubic-bezier(0.25, 0, 0.1, 1);

  --ease-in-1: cubic-bezier(0.25, 0, 1, 1);
  --ease-in-2: cubic-bezier(0.5, 0, 1, 1);
  --ease-in-3: cubic-bezier(0.7, 0, 1, 1);

  --ease-out-1: cubic-bezier(0, 0, 0.75, 1);
  --ease-out-2: cubic-bezier(0, 0, 0.5, 1);
  --ease-out-3: cubic-bezier(0, 0, 0.3, 1);       /* 推奨デフォルト */

  --ease-in-out-1: cubic-bezier(0.1, 0, 0.9, 1);
  --ease-in-out-2: cubic-bezier(0.3, 0, 0.7, 1);
  --ease-in-out-3: cubic-bezier(0.5, 0, 0.5, 1);

  --ease-elastic-in-1: cubic-bezier(0.5, -0.25, 0.75, 1);
  --ease-elastic-out-1: cubic-bezier(0.25, 0, 0.5, 1.25);

  --ease-spring-1: linear(/* ... spring function ... */);
}

キーフレームアニメーション

/* Open Propsが提供するアニメーション */
:root {
  --animation-fade-in: fade-in 0.5s var(--ease-3);
  --animation-fade-out: fade-out 0.5s var(--ease-3);
  --animation-slide-in-up: slide-in-up 0.5s var(--ease-3);
  --animation-slide-in-down: slide-in-down 0.5s var(--ease-3);
  --animation-slide-out-up: slide-out-up 0.5s var(--ease-3);
  --animation-scale-up: scale-up 0.5s var(--ease-3);
  --animation-scale-down: scale-down 0.5s var(--ease-3);
  --animation-shake-x: shake-x 0.75s var(--ease-out-5);
  --animation-spin: spin 2s linear infinite;
  --animation-ping: ping 1s var(--ease-out-3) infinite;
  --animation-pulse: pulse 2s var(--ease-in-out-3) infinite;
  --animation-bounce: bounce 2s var(--ease-elastic-out-1) infinite;
}

実践的なアニメーション実装

/* インタラクティブなボタン */
.btn {
  display: inline-flex;
  align-items: center;
  gap: var(--size-2);
  padding: var(--size-2) var(--size-5);
  border: none;
  border-radius: var(--radius-2);
  font-size: var(--font-size-1);
  font-weight: var(--font-weight-6);
  cursor: pointer;
  transition:
    background-color 0.2s var(--ease-3),
    box-shadow 0.2s var(--ease-3),
    transform 0.1s var(--ease-3);
}

.btn:hover {
  box-shadow: var(--shadow-3);
  transform: translateY(-1px);
}

.btn:active {
  transform: translateY(0);
  box-shadow: var(--shadow-1);
}

.btn-primary {
  background: var(--color-primary);
  color: white;
}

.btn-primary:hover {
  background: var(--color-primary-dark);
}

/* ページ遷移アニメーション */
.page-enter {
  animation: var(--animation-fade-in), var(--animation-slide-in-up);
}

.page-exit {
  animation: var(--animation-fade-out), var(--animation-slide-out-up);
}

/* カード出現アニメーション(スタガー) */
.card-grid > * {
  animation: var(--animation-fade-in);
  animation-fill-mode: both;
}

.card-grid > *:nth-child(1) { animation-delay: 0ms; }
.card-grid > *:nth-child(2) { animation-delay: 100ms; }
.card-grid > *:nth-child(3) { animation-delay: 200ms; }
.card-grid > *:nth-child(4) { animation-delay: 300ms; }
.card-grid > *:nth-child(5) { animation-delay: 400ms; }
.card-grid > *:nth-child(6) { animation-delay: 500ms; }

/* ローディングスピナー */
.spinner {
  width: var(--size-7);
  height: var(--size-7);
  border: 3px solid var(--gray-3);
  border-top-color: var(--color-primary);
  border-radius: var(--radius-round);
  animation: var(--animation-spin);
}

/* 通知バッジのパルス */
.notification-dot {
  width: var(--size-2);
  height: var(--size-2);
  border-radius: var(--radius-round);
  background: var(--red-7);
  animation: var(--animation-pulse);
}

ダークモード対応(Adaptive Design)

Open Propsはライト/ダークモードに対応したAdaptive Propsを提供する。prefers-color-schemeに応じて自動的にトークン値が切り替わる。

Adaptive Propsの導入

/* Adaptiveバージョンをインポート */
@import 'open-props/colors.min.css';
@import 'open-props/colors-hsl.min.css';

/* または個別にAdaptiveトークンを設定 */
:root {
  color-scheme: light dark;

  /* ライトモードのデフォルト */
  --surface-1: var(--gray-0);
  --surface-2: var(--gray-1);
  --surface-3: var(--gray-2);
  --surface-4: var(--gray-3);
  --text-1: var(--gray-12);
  --text-2: var(--gray-8);
  --text-3: var(--gray-6);
  --border-default: var(--gray-3);
  --shadow-color: 220 3% 15%;
}

/* ダークモード */
@media (prefers-color-scheme: dark) {
  :root {
    --surface-1: var(--gray-12);
    --surface-2: var(--gray-11);
    --surface-3: var(--gray-10);
    --surface-4: var(--gray-9);
    --text-1: var(--gray-1);
    --text-2: var(--gray-4);
    --text-3: var(--gray-6);
    --border-default: var(--gray-8);
    --shadow-color: 220 40% 2%;
  }
}

ダークモード対応コンポーネント

/* テーマ対応のカードコンポーネント */
.card {
  background: var(--surface-1);
  color: var(--text-1);
  border: 1px solid var(--border-default);
  border-radius: var(--radius-3);
  padding: var(--size-5);
  box-shadow:
    0 1px 2px hsl(var(--shadow-color) / 0.1),
    0 4px 8px hsl(var(--shadow-color) / 0.05);
  transition:
    background-color 0.3s var(--ease-3),
    border-color 0.3s var(--ease-3),
    box-shadow 0.3s var(--ease-3);
}

.card:hover {
  box-shadow:
    0 2px 4px hsl(var(--shadow-color) / 0.15),
    0 8px 16px hsl(var(--shadow-color) / 0.1);
}

/* サイドバー */
.sidebar {
  background: var(--surface-2);
  border-inline-end: 1px solid var(--border-default);
  padding: var(--size-4);
  width: var(--size-15);
}

.sidebar-item {
  display: flex;
  align-items: center;
  gap: var(--size-2);
  padding: var(--size-2) var(--size-3);
  border-radius: var(--radius-2);
  color: var(--text-2);
  text-decoration: none;
  transition: background-color 0.15s var(--ease-3);
}

.sidebar-item:hover {
  background: var(--surface-3);
  color: var(--text-1);
}

.sidebar-item.active {
  background: var(--color-primary-surface);
  color: var(--color-primary);
  font-weight: var(--font-weight-6);
}

手動テーマ切替

ユーザーがテーマを手動で切り替えられるようにする場合は、data-theme属性を活用する。

/* 手動テーマ切替対応 */
[data-theme="light"] {
  --surface-1: var(--gray-0);
  --surface-2: var(--gray-1);
  --text-1: var(--gray-12);
  --text-2: var(--gray-8);
}

[data-theme="dark"] {
  --surface-1: var(--gray-12);
  --surface-2: var(--gray-11);
  --text-1: var(--gray-1);
  --text-2: var(--gray-4);
}
<html data-theme="light">
<body>
  <button onclick="toggleTheme()">テーマ切替</button>

  <script>
    function toggleTheme() {
      const html = document.documentElement;
      const current = html.getAttribute('data-theme');
      const next = current === 'light' ? 'dark' : 'light';
      html.setAttribute('data-theme', next);
      localStorage.setItem('theme', next);
    }

    // 保存済みテーマを復元
    const saved = localStorage.getItem('theme');
    if (saved) {
      document.documentElement.setAttribute('data-theme', saved);
    }
  </script>
</body>
</html>

Custom Propertyの合成テクニック

Open Propsのトークンを組み合わせて、プロジェクト固有のデザインシステムを構築する手法を解説する。

トークンの派生

:root {
  /* Open Propsのトークンから派生させる */
  --btn-padding-inline: var(--size-5);
  --btn-padding-block: var(--size-2);
  --btn-radius: var(--radius-2);
  --btn-font-size: var(--font-size-1);
  --btn-font-weight: var(--font-weight-6);
  --btn-transition: 0.2s var(--ease-3);

  --input-padding-inline: var(--size-3);
  --input-padding-block: var(--size-2);
  --input-radius: var(--radius-2);
  --input-border-color: var(--color-border);
  --input-focus-ring: 0 0 0 3px var(--blue-3);

  --card-padding: var(--size-5);
  --card-radius: var(--radius-3);
  --card-shadow: var(--shadow-2);
  --card-gap: var(--size-3);
}

コンポーネントライブラリの構築

/* フォーム入力コンポーネント */
.input {
  width: 100%;
  padding: var(--input-padding-block) var(--input-padding-inline);
  border: 1px solid var(--input-border-color);
  border-radius: var(--input-radius);
  font-size: var(--font-size-1);
  font-family: inherit;
  background: var(--surface-1);
  color: var(--text-1);
  transition: border-color 0.15s var(--ease-3),
              box-shadow 0.15s var(--ease-3);
}

.input:focus {
  outline: none;
  border-color: var(--color-primary);
  box-shadow: var(--input-focus-ring);
}

.input::placeholder {
  color: var(--text-3);
}

.input:invalid {
  border-color: var(--color-error);
}

/* テキストエリア */
.textarea {
  min-height: 6rem;
  resize: vertical;
}

/* ラベル */
.label {
  display: block;
  font-size: var(--font-size-0);
  font-weight: var(--font-weight-6);
  color: var(--text-2);
  margin-block-end: var(--size-1);
}

/* フォームグループ */
.form-group {
  display: grid;
  gap: var(--size-1);
}

.form-group + .form-group {
  margin-block-start: var(--size-4);
}

/* エラーメッセージ */
.form-error {
  font-size: var(--font-size-0);
  color: var(--color-error);
  display: flex;
  align-items: center;
  gap: var(--size-1);
}

シャドウの活用

/* Open Propsのシャドウ変数 */
:root {
  --shadow-1: 0 1px 2px 0 hsl(var(--shadow-color) / 0.05);
  --shadow-2: 0 1px 3px 0 hsl(var(--shadow-color) / 0.1),
              0 1px 2px -1px hsl(var(--shadow-color) / 0.1);
  --shadow-3: 0 4px 6px -1px hsl(var(--shadow-color) / 0.1),
              0 2px 4px -2px hsl(var(--shadow-color) / 0.1);
  --shadow-4: 0 10px 15px -3px hsl(var(--shadow-color) / 0.1),
              0 4px 6px -4px hsl(var(--shadow-color) / 0.1);
  --shadow-5: 0 20px 25px -5px hsl(var(--shadow-color) / 0.1),
              0 8px 10px -6px hsl(var(--shadow-color) / 0.1);
  --shadow-6: 0 25px 50px -12px hsl(var(--shadow-color) / 0.25);
}

/* エレベーションシステム */
.elevation-1 { box-shadow: var(--shadow-1); }
.elevation-2 { box-shadow: var(--shadow-2); }
.elevation-3 { box-shadow: var(--shadow-3); }
.elevation-4 { box-shadow: var(--shadow-4); }
.elevation-5 { box-shadow: var(--shadow-5); }

/* ホバーでエレベーション上昇 */
.interactive-card {
  box-shadow: var(--shadow-2);
  transition: box-shadow 0.2s var(--ease-3),
              transform 0.2s var(--ease-3);
}

.interactive-card:hover {
  box-shadow: var(--shadow-4);
  transform: translateY(-2px);
}

フレームワーク統合

Reactでの活用

// styles/tokens.css
// @import 'open-props/colors.min.css';
// @import 'open-props/sizes.min.css';
// ...

// components/Alert.jsx
import './Alert.css';

export function Alert({ variant = 'info', title, children, onClose }) {
  return (
    <div className={`alert alert-${variant}`} role="alert">
      <div className="alert-header">
        <strong className="alert-title">{title}</strong>
        {onClose && (
          <button className="alert-close" onClick={onClose} aria-label="閉じる">
            &times;
          </button>
        )}
      </div>
      <div className="alert-body">{children}</div>
    </div>
  );
}
/* components/Alert.css */
.alert {
  padding: var(--size-3) var(--size-4);
  border-radius: var(--radius-2);
  border-inline-start: 4px solid;
  animation: var(--animation-slide-in-down);
}

.alert-info {
  background: var(--cyan-1);
  border-color: var(--cyan-7);
  color: var(--cyan-10);
}

.alert-success {
  background: var(--green-1);
  border-color: var(--green-7);
  color: var(--green-10);
}

.alert-warning {
  background: var(--yellow-1);
  border-color: var(--yellow-7);
  color: var(--yellow-10);
}

.alert-error {
  background: var(--red-1);
  border-color: var(--red-7);
  color: var(--red-10);
}

.alert-header {
  display: flex;
  justify-content: space-between;
  align-items: center;
  margin-block-end: var(--size-1);
}

.alert-title {
  font-weight: var(--font-weight-6);
}

.alert-close {
  background: none;
  border: none;
  font-size: var(--font-size-4);
  cursor: pointer;
  color: inherit;
  opacity: 0.7;
  padding: 0;
  line-height: 1;
}

.alert-close:hover {
  opacity: 1;
}

.alert-body {
  font-size: var(--font-size-0);
  line-height: var(--font-lineheight-3);
}

Vueでの活用

<!-- components/Tooltip.vue -->
<template>
  <div class="tooltip-wrapper" @mouseenter="show" @mouseleave="hide">
    <slot />
    <div v-if="visible" class="tooltip" :class="`tooltip-${placement}`">
      {{ text }}
    </div>
  </div>
</template>

<script setup>
import { ref } from 'vue';

defineProps({
  text: { type: String, required: true },
  placement: { type: String, default: 'top' }
});

const visible = ref(false);
const show = () => visible.value = true;
const hide = () => visible.value = false;
</script>

<style scoped>
.tooltip-wrapper {
  position: relative;
  display: inline-block;
}

.tooltip {
  position: absolute;
  padding: var(--size-1) var(--size-3);
  background: var(--gray-11);
  color: var(--gray-1);
  font-size: var(--font-size-0);
  border-radius: var(--radius-2);
  white-space: nowrap;
  pointer-events: none;
  animation: var(--animation-fade-in);
  z-index: var(--layer-important);
}

.tooltip-top {
  bottom: calc(100% + var(--size-1));
  left: 50%;
  transform: translateX(-50%);
}

.tooltip-bottom {
  top: calc(100% + var(--size-1));
  left: 50%;
  transform: translateX(-50%);
}
</style>

Tailwind CSSとの比較と併用

Open PropsとTailwind CSSは異なるアプローチでCSSの課題を解決する。

アプローチの違い

観点Open PropsTailwind CSS
手法CSS変数(デザイントークン)ユーティリティクラス
HTMLへの影響なし(CSS内で使用)クラス名が長くなる
学習コストCSS知識がそのまま使える独自のクラス名体系を覚える必要がある
カスタマイズCSS変数を上書きするだけtailwind.config.jsで設定
ファイルサイズJITで数KBPurgeCSS適用で数KB
フレームワーク依存なしPostCSS必須
コンポーネントスタイリングCSSファイルに記述HTML内にインライン的に記述
IDE支援CSS標準のオートコンプリートTailwind CSS IntelliSense拡張

併用パターン

Open PropsをTailwind CSSのデザイントークンとして活用する方法がある。

// tailwind.config.js
module.exports = {
  theme: {
    extend: {
      colors: {
        primary: {
          light: 'var(--blue-4)',
          DEFAULT: 'var(--blue-7)',
          dark: 'var(--blue-9)',
        },
        surface: {
          1: 'var(--surface-1)',
          2: 'var(--surface-2)',
          3: 'var(--surface-3)',
        },
      },
      spacing: {
        'op-1': 'var(--size-1)',
        'op-2': 'var(--size-2)',
        'op-3': 'var(--size-3)',
        'op-4': 'var(--size-4)',
        'op-5': 'var(--size-5)',
      },
      borderRadius: {
        'op-2': 'var(--radius-2)',
        'op-3': 'var(--radius-3)',
      },
      boxShadow: {
        'op-2': 'var(--shadow-2)',
        'op-3': 'var(--shadow-3)',
        'op-4': 'var(--shadow-4)',
      },
    },
  },
};
<!-- TailwindのユーティリティクラスでOpen Propsのトークンを利用 -->
<div class="bg-surface-1 p-op-5 rounded-op-3 shadow-op-2">
  <h2 class="text-primary font-semibold">タイトル</h2>
  <p class="text-gray-600 mt-op-2">本文テキスト</p>
</div>

どちらを選ぶべきか

状況推奨
新規プロジェクト・小〜中規模Open Props(シンプルで軽量)
大規模チーム・統一スタイルガイド必須Tailwind CSS(厳密なルール適用が容易)
デザインシステムの基盤構築Open Props(CSS標準準拠で長期安定)
ラピッドプロトタイピングTailwind CSS(HTMLだけで完結)
フレームワーク非依存が必要Open Props(ビルドツール不要でも使用可)
既存CSSとの段階的統合Open Props(既存CSSに変数を追加するだけ)

Open Props Normalizeの活用

Open Propsは独自のCSS Normalizeを提供しており、モダンブラウザ向けに最適化されたリセットスタイルが含まれている。

@import 'open-props/normalize.min.css';

このNormalizeは以下の特徴を持つ。

/* Normalizeの一部(概念的な内容) */

/* ボックスサイジングの統一 */
*, *::before, *::after {
  box-sizing: border-box;
}

/* スムーススクロール(motion-safe時のみ) */
@media (prefers-reduced-motion: no-preference) {
  html {
    scroll-behavior: smooth;
  }
}

/* レスポンシブ画像 */
img, picture, video, canvas, svg {
  display: block;
  max-inline-size: 100%;
  block-size: auto;
}

/* フォーム要素のフォント継承 */
input, button, textarea, select {
  font: inherit;
}

/* テキストの折り返し */
p, h1, h2, h3, h4, h5, h6 {
  overflow-wrap: break-word;
}

/* リストスタイルのリセット */
ul[role="list"],
ol[role="list"] {
  list-style: none;
  padding: 0;
}

関連記事

まとめ

Open Propsは、CSS Custom Propertiesを活用したデザイントークンライブラリとして、フロントエンド開発に一貫性と効率性をもたらす。本記事の内容を整理する。

トピックポイント
カラーシステムoklch色空間ベース。13段階のパレットからセマンティックカラーを派生
タイポグラフィ数学的に調和したスケール。Fluid対応で全画面幅に最適化
スペーシング一貫した間隔スケール。Fluidサイズも提供
アニメーション実用的なイージング関数とキーフレームアニメーション
ダークモードAdaptive Props + prefers-color-schemeで自動切替
フレームワーク統合React/Vue/Svelte等、フレームワーク非依存で利用可能
Tailwind CSS併用トークンとして統合し、ユーティリティクラスから参照可能
パフォーマンスJITビルドで未使用変数を除去。ランタイムコストゼロ

Open Propsの最大の利点は、CSS標準に準拠している点にある。特定のビルドツールやフレームワークに依存せず、ブラウザネイティブの仕組みだけで動作する。デザインシステムの基盤として採用し、プロジェクト固有のトークンをその上に構築するアプローチを推奨する。