Q
v0.3

Design Tokens

Stablev0.3.0

Qwik UIのデザインシステムを構成するカラー、スペーシング、シャドウなどの基本要素。CSS変数として定義されており、テーマ切り替えに対応しています。

Surface

Content

Accent

Semantic

Caution / Red

Alert / Orange

Success / Green

Link / Blue

TypeScript で使用する

Qwikコンポーネントで Design Tokens を活用する実装例

コンポーネントでの使用例

TSXButton.tsx43 lines
12345678910111213141516171819202122232425262728293031323334353637383940414243import { component$ } from '@builder.io/qwik';
import type { ButtonHTMLAttributes } from '@builder.io/qwik';

interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
  variant?: 'primary' | 'secondary' | 'danger';
  size?: 'sm' | 'md' | 'lg';
  fullWidth?: boolean;
}

export const Button = component$<ButtonProps>((props) => {
  const {
    variant = 'primary',
    size = 'md',
    fullWidth = false,
    class: className,
    ...rest
  } = props;

  const baseStyles = 'inline-flex items-center justify-center font-medium rounded-lg transition-all';
  
  const variantStyles = {
    primary: 'bg-accent text-white hover:bg-accent-hover active:bg-accent-pressed',
    secondary: 'bg-surface-elevated text-content border border-border hover:bg-surface-hover',
    danger: 'bg-caution-red text-white hover:opacity-90'
  };

  const sizeStyles = {
    sm: 'px-3 py-1.5 text-sm',
    md: 'px-4 py-2 text-base',
    lg: 'px-6 py-3 text-lg'
  };

  return (
    <button
      {...rest}
      class={`${baseStyles} ${variantStyles[variant]} ${sizeStyles[size]} ${
        fullWidth ? 'w-full' : ''
      } ${className || ''}`}
    >
      <Slot />
    </button>
  );
});

カスタムフックでの使用

TypeScriptuseTheme.ts53 lines
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253import { useSignal, useVisibleTask$ } from '@builder.io/qwik';

export const useTheme = () => {
  const theme = useSignal<'light' | 'dark'>('light');
  const systemPreference = useSignal<'light' | 'dark'>('light');

  useVisibleTask$(() => {
    // システムのテーマ設定を検出
    const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
    systemPreference.value = mediaQuery.matches ? 'dark' : 'light';
    
    // ローカルストレージから保存されたテーマを取得
    const savedTheme = localStorage.getItem('theme') as 'light' | 'dark' | null;
    if (savedTheme) {
      theme.value = savedTheme;
    } else {
      theme.value = systemPreference.value;
    }

    // システムテーマの変更を監視
    const handleChange = (e: MediaQueryListEvent) => {
      systemPreference.value = e.matches ? 'dark' : 'light';
      if (!localStorage.getItem('theme')) {
        theme.value = systemPreference.value;
      }
    };

    mediaQuery.addEventListener('change', handleChange);
    
    return () => {
      mediaQuery.removeEventListener('change', handleChange);
    };
  });

  const setTheme = (newTheme: 'light' | 'dark' | 'system') => {
    if (newTheme === 'system') {
      localStorage.removeItem('theme');
      theme.value = systemPreference.value;
    } else {
      localStorage.setItem('theme', newTheme);
      theme.value = newTheme;
    }
    
    // ルート要素にdata-theme属性を設定
    document.documentElement.dataset.theme = theme.value;
  };

  return {
    theme: theme.value,
    setTheme,
    systemPreference: systemPreference.value
  };
};

Tailwind CSS 設定

Design Tokens を Tailwind CSS で使用するための設定方法

tailwind.config.js の設定

JavaScripttailwind.config.js110 lines
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110/** @type {import('tailwindcss').Config} */
module.exports = {
  content: [
    "./src/**/*.{js,ts,jsx,tsx}",
    "./node_modules/@aid-on/design-system/**/*.js"
  ],
  theme: {
    extend: {
      colors: {
        // Surface colors
        'surface': 'var(--surface)',
        'surface-elevated': 'var(--surface-elevated)',
        'surface-hover': 'var(--surface-hover)',
        'surface-pressed': 'var(--surface-pressed)',
        
        // Content colors
        'content': 'var(--content)',
        'content-secondary': 'var(--content-secondary)',
        'content-tertiary': 'var(--content-tertiary)',
        'content-disabled': 'var(--content-disabled)',
        
        // Accent colors
        'accent': 'var(--accent)',
        'accent-hover': 'var(--accent-hover)',
        'accent-pressed': 'var(--accent-pressed)',
        'accent-muted': 'var(--accent-muted)',
        
        // Semantic colors
        'caution-red': 'var(--caution-red)',
        'alert-orange': 'var(--alert-orange)',
        'success-green': 'var(--success-green)',
        'link-blue': 'var(--link-blue)',
        
        // UI colors
        'border': 'var(--border)',
        'divider': 'var(--divider)',
        'overlay': 'var(--overlay)'
      },
      spacing: {
        'xs': 'var(--spacing-xs)',
        'sm': 'var(--spacing-sm)',
        'md': 'var(--spacing-md)',
        'lg': 'var(--spacing-lg)',
        'xl': 'var(--spacing-xl)',
        '2xl': 'var(--spacing-2xl)'
      },
      borderRadius: {
        'sm': 'var(--radius-sm)',
        'md': 'var(--radius-md)',
        'lg': 'var(--radius-lg)',
        'xl': 'var(--radius-xl)'
      },
      boxShadow: {
        'sm': 'var(--shadow-sm)',
        'md': 'var(--shadow-md)',
        'lg': 'var(--shadow-lg)',
        'xl': 'var(--shadow-xl)'
      },
      fontFamily: {
        'sans': ['Inter', 'system-ui', 'sans-serif'],
        'mono': ['Cascadia Code', 'Fira Code', 'monospace']
      }
    }
  },
  plugins: [
    // カスタムプラグインでCSS変数を定義
    function({ addBase }) {
      addBase({
        ':root': {
          '--surface': '#ffffff',
          '--surface-elevated': '#f9fafb',
          '--surface-hover': '#f3f4f6',
          '--surface-pressed': '#e5e7eb',
          '--content': '#111827',
          '--content-secondary': '#4b5563',
          '--content-tertiary': '#9ca3af',
          '--content-disabled': '#d1d5db',
          '--accent': '#6366f1',
          '--accent-hover': '#4f46e5',
          '--accent-pressed': '#4338ca',
          '--accent-muted': '#e0e7ff',
          '--caution-red': '#ef4444',
          '--alert-orange': '#f97316',
          '--success-green': '#10b981',
          '--link-blue': '#3b82f6',
          '--border': '#e5e7eb',
          '--divider': '#f3f4f6',
          '--overlay': 'rgba(0, 0, 0, 0.5)'
        },
        '[data-theme="dark"]': {
          '--surface': '#1f2937',
          '--surface-elevated': '#374151',
          '--surface-hover': '#4b5563',
          '--surface-pressed': '#6b7280',
          '--content': '#f9fafb',
          '--content-secondary': '#e5e7eb',
          '--content-tertiary': '#9ca3af',
          '--content-disabled': '#6b7280',
          '--accent': '#818cf8',
          '--accent-hover': '#a5b4fc',
          '--accent-pressed': '#6366f1',
          '--accent-muted': '#312e81',
          '--border': '#374151',
          '--divider': '#4b5563',
          '--overlay': 'rgba(0, 0, 0, 0.7)'
        }
      });
    }
  ]
};

CSS での直接使用

CSSglobal.css74 lines
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374@import '@aid-on/design-system/dist/tokens.css';

/* カスタムコンポーネントのスタイル */
.custom-card {
  background-color: var(--surface-elevated);
  border: 1px solid var(--border);
  border-radius: var(--radius-lg);
  padding: var(--spacing-lg);
  box-shadow: var(--shadow-md);
}

.custom-card:hover {
  background-color: var(--surface-hover);
  box-shadow: var(--shadow-lg);
  transform: translateY(-2px);
  transition: all 0.2s ease;
}

.custom-card-title {
  color: var(--content);
  font-size: 1.25rem;
  font-weight: 600;
  margin-bottom: var(--spacing-sm);
}

.custom-card-description {
  color: var(--content-secondary);
  font-size: 0.875rem;
  line-height: 1.5;
}

/* ダークモード対応 */
[data-theme="dark"] .custom-card {
  background-color: var(--surface);
  border-color: var(--border);
}

[data-theme="dark"] .custom-card:hover {
  background-color: var(--surface-elevated);
}

/* アニメーション付きボタン */
.animated-button {
  background: linear-gradient(135deg, var(--accent), var(--accent-hover));
  color: white;
  padding: var(--spacing-sm) var(--spacing-lg);
  border-radius: var(--radius-md);
  border: none;
  font-weight: 500;
  cursor: pointer;
  transition: all 0.3s ease;
  position: relative;
  overflow: hidden;
}

.animated-button::before {
  content: '';
  position: absolute;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
  background: rgba(255, 255, 255, 0.2);
  transform: translateX(-100%);
  transition: transform 0.3s ease;
}

.animated-button:hover::before {
  transform: translateX(0);
}

.animated-button:active {
  transform: scale(0.98);
}