mcpField
B
beautiful-ui

beautiful-ui

github.com/apetcuデザイン・UI253 回インストール9 週間前に更新
リポジトリ

beautiful-ui について

beautiful-uiは、Next.jsとTailwind CSS v4でダークモード優先のグラスモーフィズムUIを作るデザインスキルです。再利用可能なコンポーネントパターンに加え、視覚設計、アクセシビリティ、アニメーション、パフォーマンスの指針を提供します。

主な機能

  1. 01ダークモード優先のガラステーマ
  2. 02ガラスカード・ボタン・入力欄のパターン
  3. 03Next.jsのレイアウトとページ遷移
  4. 04タイポグラフィと配色の指針
  5. 05アクセシビリティと性能のチェックリスト

活用シーン

  1. 01ダークなグラスモーフィズムUIの設計
  2. 02再利用可能なガラスUIコンポーネントの作成
  3. 03アニメーションとアクセシビリティの確認

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/apetcu/skills --skill beautiful-ui

インストール後、Claude Code で/beautiful-uiを入力して読み込まれたか確認します。

同梱ファイルreferences/animation-recipes.mdreferences/glass-patterns.md

詳細

美しい UI

Next.js + Tailwind CSS v4 を使用して、上質なグラスモーフィズムのインターフェースを構築します。ダークモードを基本とし、光をたたえたガラスの表面、流れるようなアニメーション、細部まで行き届いたビジュアルの仕上げを備えています。

デザインの基本原則

すべてのインターフェースで、次の譲れない原則に従います。

  1. ダークモードをデフォルトにする — まずダークモード向けにデザインし、ライトモードは任意とします
  2. ガラスを主要な表面にする — 豊かな背景の上に、背景ぼかしを使った半透明のレイヤーを重ねます
  3. 光を素材として扱う — グロー、グラデーション、輝度を使って奥行きを生み出します
  4. 意図を持って動きを付ける — すべてのアニメーションは、階層、フィードバック、または楽しさにつながるものにします
  5. 大胆なタイポグラフィを使う — 個性のあるフォントを選び、ありきたりなフォント(Inter、Roboto、Arial)は使いません

Tailwind v4 のダークグラステーマ

@import "tailwindcss";

@theme {
  /* Dark-first color system using OKLCH */
  --color-background: oklch(10% 0.015 270);
  --color-surface: oklch(14% 0.01 270);
  --color-elevated: oklch(18% 0.012 270);

  --color-foreground: oklch(95% 0.005 270);
  --color-foreground-muted: oklch(65% 0.01 270);
  --color-foreground-subtle: oklch(45% 0.01 270);

  /* Glass surface tokens */
  --color-glass: oklch(100% 0 0 / 0.06);
  --color-glass-hover: oklch(100% 0 0 / 0.1);
  --color-glass-active: oklch(100% 0 0 / 0.14);
  --color-glass-border: oklch(100% 0 0 / 0.12);
  --color-glass-border-hover: oklch(100% 0 0 / 0.2);

  /* Accent — a vibrant hue that glows on dark surfaces */
  --color-accent: oklch(72% 0.19 250);
  --color-accent-hover: oklch(78% 0.17 250);
  --color-accent-muted: oklch(72% 0.19 250 / 0.15);
  --color-accent-foreground: oklch(98% 0.005 270);

  /* Status colors (muted to match dark glass aesthetic) */
  --color-success: oklch(72% 0.17 155);
  --color-warning: oklch(78% 0.15 75);
  --color-destructive: oklch(65% 0.2 25);

  --color-border: oklch(100% 0 0 / 0.08);
  --color-ring: oklch(72% 0.19 250 / 0.5);

  /* Radius — generous for glass surfaces */
  --radius-sm: 0.5rem;
  --radius-md: 0.75rem;
  --radius-lg: 1rem;
  --radius-xl: 1.25rem;
  --radius-2xl: 1.5rem;

  /* Glass-specific animations */
  --animate-glass-in: glass-in 0.4s cubic-bezier(0.16, 1, 0.3, 1);
  --animate-glow-pulse: glow-pulse 3s ease-in-out infinite;
  --animate-fade-up: fade-up 0.5s cubic-bezier(0.16, 1, 0.3, 1);
  --animate-scale-in: scale-in 0.3s cubic-bezier(0.16, 1, 0.3, 1);
  --animate-shimmer: shimmer 2.5s linear infinite;
  --animate-float: float 6s ease-in-out infinite;

  @keyframes glass-in {
    from { opacity: 0; backdrop-filter: blur(0px); transform: translateY(8px) scale(0.98); }
    to { opacity: 1; backdrop-filter: blur(16px); transform: translateY(0) scale(1); }
  }

  @keyframes glow-pulse {
    0%, 100% { box-shadow: 0 0 20px oklch(72% 0.19 250 / 0.15); }
    50% { box-shadow: 0 0 40px oklch(72% 0.19 250 / 0.3); }
  }

  @keyframes fade-up {
    from { opacity: 0; transform: translateY(16px); }
    to { opacity: 1; transform: translateY(0); }
  }

  @keyframes scale-in {
    from { opacity: 0; transform: scale(0.95); }
    to { opacity: 1; transform: scale(1); }
  }

  @keyframes shimmer {
    0% { background-position: -200% 0; }
    100% { background-position: 200% 0; }
  }

  @keyframes float {
    0%, 100% { transform: translateY(0px); }
    50% { transform: translateY(-8px); }
  }
}

/* Base layer */
@layer base {
  * { @apply border-border; }
  body {
    @apply bg-background text-foreground antialiased;
    font-feature-settings: "cv02", "cv03", "cv04", "cv11";
  }
}

/* Glass utility classes */
@utility glass {
  @apply bg-glass backdrop-blur-xl border border-glass-border;
}

@utility glass-hover {
  @apply hover:bg-glass-hover hover:border-glass-border-hover transition-all duration-200;
}

@utility glass-strong {
  @apply bg-glass backdrop-blur-2xl backdrop-saturate-150 border border-glass-border;
}

@utility glow {
  box-shadow: 0 0 20px oklch(72% 0.19 250 / 0.15),
              0 0 60px oklch(72% 0.19 250 / 0.05);
}

@utility glow-border {
  box-shadow: inset 0 0.5px 0 0 oklch(100% 0 0 / 0.15),
              0 0 20px oklch(72% 0.19 250 / 0.1);
}

@utility text-glow {
  text-shadow: 0 0 30px oklch(72% 0.19 250 / 0.5);
}

@utility noise {
  position: relative;
}
/* Apply noise::after with a tiny noise SVG for texture */

ガラスコンポーネントのパターン

ガラスカード(主要な表面)
import { cn } from "@/lib/utils";

export function GlassCard({
  children,
  className,
  glow = false,
  ...props
}: React.HTMLAttributes<HTMLDivElement> & { glow?: boolean }) {
  return (
    <div
      className={cn(
        "relative overflow-hidden rounded-2xl",
        "bg-white/[0.06] backdrop-blur-xl backdrop-saturate-150",
        "border border-white/[0.12]",
        "shadow-[0_8px_32px_rgba(0,0,0,0.4)]",
        glow && "shadow-[0_0_30px_oklch(72%_0.19_250/0.12)]",
        className
      )}
      {...props}
    >
      {/* Top highlight — simulates light hitting glass edge */}
      <div className="absolute inset-x-0 top-0 h-px bg-gradient-to-r from-transparent via-white/25 to-transparent" />
      <div className="relative z-10">{children}</div>
    </div>
  );
}
ガラスボタン
import { cva, type VariantProps } from "class-variance-authority";
import { cn } from "@/lib/utils";

const glassButtonVariants = cva(
  "inline-flex items-center justify-center gap-2 rounded-xl font-medium transition-all duration-200 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:pointer-events-none disabled:opacity-40",
  {
    variants: {
      variant: {
        default:
          "bg-white/[0.08] backdrop-blur-lg border border-white/[0.12] text-foreground hover:bg-white/[0.14] hover:border-white/[0.2] active:bg-white/[0.18]",
        accent:
          "bg-accent text-accent-foreground hover:bg-accent-hover shadow-[0_0_20px_oklch(72%_0.19_250/0.25)] hover:shadow-[0_0_30px_oklch(72%_0.19_250/0.35)]",
        ghost:
          "text-foreground-muted hover:text-foreground hover:bg-white/[0.06]",
        outline:
          "border border-white/[0.15] text-foreground hover:bg-white/[0.06] hover:border-white/[0.25]",
      },
      size: {
        sm: "h-9 px-3.5 text-sm",
        default: "h-11 px-5 text-sm",
        lg: "h-12 px-8 text-base",
        icon: "size-10",
      },
    },
    defaultVariants: { variant: "default", size: "default" },
  }
);

export function GlassButton({
  className,
  variant,
  size,
  ...props
}: React.ButtonHTMLAttributes<HTMLButtonElement> &
  VariantProps<typeof glassButtonVariants>) {
  return (
    <button
      className={cn(glassButtonVariants({ variant, size, className }))}
      {...props}
    />
  );
}
ガラス入力欄
export function GlassInput({
  className,
  ...props
}: React.InputHTMLAttributes<HTMLInputElement>) {
  return (
    <input
      className={cn(
        "flex h-11 w-full rounded-xl px-4 text-sm",
        "bg-white/[0.06] backdrop-blur-lg",
        "border border-white/[0.1]",
        "text-foreground placeholder:text-foreground-subtle",
        "focus:outline-none focus:border-white/[0.25] focus:bg-white/[0.1]",
        "focus:ring-2 focus:ring-accent/30",
        "transition-all duration-200",
        className
      )}
      {...props}
    />
  );
}

背景パターン

グラスモーフィズムの UI には、豊かな背景が必要です。ページごとに 1 つ使用します。

メッシュグラデーションの背景
export function MeshBackground() {
  return (
    <div className="fixed inset-0 -z-10 bg-background">
      {/* Primary gradient orb */}
      <div className="absolute top-[-20%] left-[-10%] h-[600px] w-[600px] rounded-full bg-[oklch(45%_0.15_270)] opacity-30 blur-[120px]" />
      {/* Secondary orb */}
      <div className="absolute bottom-[-10%] right-[-5%] h-[500px] w-[500px] rounded-full bg-[oklch(50%_0.18_200)] opacity-20 blur-[100px]" />
      {/* Accent orb */}
      <div className="absolute top-[40%] right-[20%] h-[300px] w-[300px] rounded-full bg-[oklch(60%_0.2_320)] opacity-15 blur-[80px]" />
      {/* Subtle noise texture */}
      <div className="absolute inset-0 opacity-[0.03]" style={{ backgroundImage: "url(\"data:image/svg+xml,%3Csvg viewBox='0 0 256 256' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='4' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)'/%3E%3C/svg%3E\")" }} />
    </div>
  );
}
ドットグリッドの背景
export function DotGridBackground() {
  return (
    <div className="fixed inset-0 -z-10 bg-background">
      <div
        className="absolute inset-0 opacity-[0.15]"
        style={{
          backgroundImage: "radial-gradient(oklch(100% 0 0 / 0.3) 1px, transparent 1px)",
          backgroundSize: "24px 24px",
        }}
      />
      {/* Radial fade so dots don't tile harshly */}
      <div className="absolute inset-0 bg-[radial-gradient(circle_at_center,transparent_0%,var(--color-background)_70%)]" />
    </div>
  );
}

アニメーションのパターン

一連のアニメーションには Framer Motion を使用します。単純なホバーやトランジションには CSS を使用します。

順番に表示するガラスカード
import { motion } from "framer-motion";

const container = {
  hidden: {},
  show: { transition: { staggerChildren: 0.08 } },
};

const item = {
  hidden: { opacity: 0, y: 20, filter: "blur(4px)" },
  show: {
    opacity: 1, y: 0, filter: "blur(0px)",
    transition: { duration: 0.5, ease: [0.16, 1, 0.3, 1] },
  },
};

export function CardGrid({ cards }: { cards: CardData[] }) {
  return (
    <motion.div
      variants={container}
      initial="hidden"
      animate="show"
      className="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3"
    >
      {cards.map((card) => (
        <motion.div key={card.id} variants={item}>
          <GlassCard className="p-6">
            {/* card content */}
          </GlassCard>
        </motion.div>
      ))}
    </motion.div>
  );
}
ホバーに反応するガラス
<motion.div
  whileHover={{
    scale: 1.02,
    boxShadow: "0 0 30px oklch(72% 0.19 250 / 0.2)",
  }}
  whileTap={{ scale: 0.98 }}
  transition={{ type: "spring", stiffness: 400, damping: 25 }}
  className="glass rounded-2xl p-6 cursor-pointer"
>
  {children}
</motion.div>

モーションの仕様

インタラクション継続時間イージング
ホバー状態150msease-out
ボタンの押下100msease-out
カードの表示400-500mscubic-bezier(0.16, 1, 0.3, 1)
モーダルを開く300mscubic-bezier(0.16, 1, 0.3, 1)
ページ遷移400mscubic-bezier(0.16, 1, 0.3, 1)
スタッガーの遅延60-100ms—
グローパルス3sease-in-out(無限)
フロート6sease-in-out(無限)

インタラクティブ要素のスプリング設定: { stiffness: 400, damping: 25, mass: 0.8 }

パフォーマンスに関するルール:

  • transform、opacity、filter、box-shadow(GPU アクセラレーション対象)のみをアニメーションさせます
  • prefers-reduced-motion に従い、必須ではない動きをすべて無効にします
  • UI のインタラクションは 500ms 未満にします

タイポグラフィ

個性と特徴のあるフォントを選びます。ディスプレイフォントと洗練された本文フォントを組み合わせます。

ダークグラスの美しさを引き立てる組み合わせ:

  • スペースエイジ:Orbitron(ディスプレイ)+ DM Sans(本文)
  • エディトリアルグロー:Playfair Display(ディスプレイ)+ Outfit(本文)
  • ネオジオメトリック:Sora(ディスプレイ)+ Plus Jakarta Sans(本文)
  • モノスペースハッカー:JetBrains Mono(ディスプレイ)+ Geist Sans(本文)
  • エレガントな精密さ:Cormorant Garamond(ディスプレイ)+ Geist Sans(本文)

Next.js でのフォント読み込み:

import { Sora, Plus_Jakarta_Sans } from "next/font/google";

const display = Sora({ subsets: ["latin"], variable: "--font-display" });
const body = Plus_Jakarta_Sans({ subsets: ["latin"], variable: "--font-body" });

// In layout.tsx body:
<body className={`${display.variable} ${body.variable} font-body`}>
@theme inline {
  --font-display: var(--font-display), system-ui;
  --font-body: var(--font-body), system-ui;
}

スケール(比率 1.25):

  • text-xs:0.64rem — キャプション、バッジ
  • text-sm:0.8rem — ラベル、メタデータ
  • text-base:1rem — 本文
  • text-lg:1.25rem — 小見出し
  • text-xl:1.563rem — セクションタイトル
  • text-2xl:1.953rem — ページタイトル
  • text-3xl:2.441rem — ヒーロー(モバイル)
  • text-4xl:3.052rem — ヒーロー(デスクトップ)
  • text-5xl:3.815rem — ディスプレイ

見出し:字間を詰め(-0.02em から -0.04em)、太字にします。 本文:デフォルトの字間、通常のウェイト、leading-relaxed を使います。

カラー戦略

ダークグラスモーフィズムでは、深みのある背景と光を放つアクセントのコントラストが重要です。

アクセントカラーの候補(テーマ内の --color-accent を入れ替えます):

  • エレクトリックブルー:oklch(72% 0.19 250) — テクノロジー、現代的、信頼感
  • バイオレットグロー:oklch(68% 0.2 290) — 創造性、上質さ
  • シアンネオン:oklch(80% 0.16 195) — 未来的、シャープ
  • エメラルド:oklch(72% 0.17 160) — みずみずしさ、自然
  • ローズゴールド:oklch(72% 0.14 15) — 温かみ、贅沢さ
  • アンバー:oklch(80% 0.15 75) — 温かみ、活気

ルール:

  • インターフェースごとに主役となるアクセントを 1 つに絞ります。副次的なアクセントは彩度を落とすか、不透明度を下げます
  • ガラスの表面には、背景のグラデーションオーブから周囲の色が映り込みます
  • ガラス上のテキストは WCAG AA(4.5:1)に適合させます。主要なコンテンツには text-foreground-subtle ではなく text-foreground を使います
  • ステータスカラー(成功/警告/破壊的)は、アクセントと競合しないように抑えます

Next.js 固有の仕様

ガラスシェルを使ったレイアウト
// app/layout.tsx
export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html lang="en" className="dark">
      <body className={`${display.variable} ${body.variable} font-body antialiased`}>
        <MeshBackground />
        <div className="relative z-10 min-h-screen">
          <GlassNavbar />
          <main className="mx-auto max-w-7xl px-4 py-8 sm:px-6 lg:px-8">
            {children}
          </main>
        </div>
      </body>
    </html>
  );
}
ガラスナビゲーションバー
export function GlassNavbar() {
  return (
    <nav className="sticky top-0 z-50 border-b border-white/[0.08] bg-background/60 backdrop-blur-xl backdrop-saturate-150">
      <div className="mx-auto flex h-16 max-w-7xl items-center justify-between px-4 sm:px-6 lg:px-8">
        {/* Logo + nav items */}
      </div>
    </nav>
  );
}
ページ遷移
// components/page-transition.tsx
"use client";
import { motion, AnimatePresence } from "framer-motion";
import { usePathname } from "next/navigation";

export function PageTransition({ children }: { children: React.ReactNode }) {
  const pathname = usePathname();
  return (
    <AnimatePresence mode="wait">
      <motion.div
        key={pathname}
        initial={{ opacity: 0, y: 12, filter: "blur(4px)" }}
        animate={{ opacity: 1, y: 0, filter: "blur(0px)" }}
        exit={{ opacity: 0, y: -8, filter: "blur(4px)" }}
        transition={{ duration: 0.35, ease: [0.16, 1, 0.3, 1] }}
      >
        {children}
      </motion.div>
    </AnimatePresence>
  );
}

品質チェックリスト

ガラス表現とビジュアル
  • 背景にグラデーションのオーブまたはメッシュを使用する — 単色一色にはしない
  • ガラス面に backdrop-blur-xl、ボーダー、上部のハイライトを適用する
  • ホバー/フォーカス時にアクセントカラーが目に見えるように発光する
  • 奥行きを出すため、不透明度を低く設定したノイズ/粒状テクスチャのオーバーレイを追加する
  • 純白または純黒の面は使用しない — 常に色味を加える
アニメーション
  • ページ読み込み時に、順番に表示される入場アニメーションを設定する
  • インタラクティブ要素に、スプリング物理を使ったホバー時/アクティブ時の状態を設定する
  • モーダル/オーバーレイに、拡大+フェードインのアニメーションを適用する
  • prefers-reduced-motion を尊重する
タイポグラフィ
  • ディスプレイフォントに特徴的なものを使う(Inter/Roboto/Arial は避ける)
  • 見出しの字間は詰め、本文の行送りにはゆとりを持たせる
  • 可変 CSS を使い、next/font 経由でフォントを読み込む
アクセシビリティ
  • ガラス面上のテキストのコントラスト比を 4.5:1 以上にする(背景効果を適用した状態でテストする)
  • フォーカスリングを見えるようにする(ring-accent/50 を使用する)
  • タッチターゲットを 44px 以上にする
  • すべてのインタラクティブ要素をキーボードで操作できるようにする
  • 画像に代替テキストを、アイコンボタンに aria-label を設定する
パフォーマンス
  • アニメーションには GPU プロパティのみを使用する(transform、opacity、filter)
  • 適切なサイズ指定で next/image を使用する
  • フォントを事前読み込みし、レイアウトシフトが起きないようにする
  • backdrop-blur は表示されている要素に限定する(重ねがけを避ける)

参考資料

追加のパターンについては、以下を参照してください。

  • references/glass-patterns.md — 高度なグラスモーフィズムのコンポーネントレシピ
  • references/animation-recipes.md — Framer Motion のオーケストレーションパターン

コメント