「コーポレートサイト、ちゃんとしすぎてない?」
シンプルで洗練されたデザイン。確かに悪くない。でも、playpark = 遊び場という社名なのに、遊び心が足りない。そんなモヤモヤ、ありませんでしたか?
私たちはありました。というか、普通のカードUIに飽きたんです。正直に言うと。
「じゃあ、泡が出るビーカー作ろうか」
...冗談で言ったはずなのに、気づいたら本当に作ってました。本記事では、その暴走の記録を共有します。(いや、真面目にUI設計した話です)
コンセプト:「遊び場」から「実験室」への連想
playpark という社名には「遊び場」という意味が込められています。
でも、ただの遊び場じゃない。「自己責任で好きなように遊んでいい子供の遊び場」というニュアンスなんです。お客様の課題を解決する「実験」を、責任を持ちながら楽しむ場所。
遊び場 → 実験 → 実験室 → ビーカー → 泡
...という連想ゲームの結果、こうなりました。(論理的でしょ?)
従来のコーポレートサイト vs playpark Lab UI
| 要素 | 従来のコーポレートサイト | playpark Lab UI |
|---|---|---|
| ボーダー | 実線(堅い) | 破線(実験ノートっぽい) |
| カラー | モノトーン(無難) | ビビッドカラー(化学反応っぽい) |
| アニメーション | フェードイン(眠い) | 泡立ち、浮遊(うるさい?) |
| ホバー | 色が変わる(普通) | 傾いて光る(やりすぎ?) |
「やりすぎ?」と社内から言われました。
そのまま通しました。
結果、意外と評判良かったです。(安心した)
作ったもの一覧:10種類のコンポーネント
装飾系(一番楽しかったやつ)
| コンポーネント | 何これ |
|---|---|
| Beaker | 泡が出るビーカー。液体が波打つ。なぜ作った。 |
| BubblesBackground | 背景に泡が浮遊。ビールサイトじゃないよ。 |
| FloatingParticles | 謎のパーティクルが漂う。雰囲気重視。 |
| CursorParticles | マウスドラッグでパーティクル発生。子供ウケする。 |
Beaker は完全に趣味です。でも、「なんか面白いサイトだな」と思ってもらえたら勝ちじゃないですか。(自己正当化)
デコレーション系(手書き風)
| コンポーネント | 用途 |
|---|---|
| ArrowDecoration | 手書き風矢印。「ここ見て!」的な。 |
| CircleHighlight | 手書き風丸囲み。採点してる感。 |
| StickyNote | 付箋。傾いてる。テープで貼ってある風。 |
| LiquidSpillDivider | 液体こぼれセクション区切り。掃除したい。 |
| ScribbleUnderline | 手書き風下線。赤ペン先生。 |
実験ノートの落書き感を出したかったんです。「ちゃんとしすぎ」の対極。
フォーム系(ちゃんと使えるやつ)
| コンポーネント | 特徴 |
|---|---|
| LabButton | 6種類のバリエーション。reactionが派手で好き。 |
| LabCard | ホバーで傾く+光る。shadcn/uiのCardを魔改造。 |
| LabInput | 破線ボーダー。フォーカスでグロー。かわいい。 |
| LabTextarea | 同上。高さが自動で伸びる。 |
shadcn/ui をベースに魔改造しています。既存のデザインシステムと共存できるように。(ここは真面目)
技術的に頑張ったところ
「遊び心」と言いつつ、中身は真面目です。(ここ大事)
1. SSR対応のアニメーション
Next.js App Router で一番ハマったやつ。Math.random() を SSR で使うと、サーバーとクライアントで値が違ってハイドレーションエラー祭りになります。
この経験、ありませんか?
// ダメなやつ
const bubbles = Array.from({ length: 10 }, () => ({
x: Math.random() * 100, // ← サーバーとクライアントで値が違う
}));
// 良いやつ:Seeded random
function seededRandom(seed: number): number {
const x = Math.sin(seed * 9999) * 10000;
return x - Math.floor(x);
}
解決策: SSR 時は決定的な値を使い、クライアントでのみランダム化。
地味だけど、これがないと本番で「なんか泡の位置が毎回違う」とか言われます。(言われました)
2. 「マウント済み」の安全な検出
useState で isMounted を管理すると警告が出る問題。React 18 以降で厳しくなったやつ。
// 警告出るやつ
const [isMounted, setIsMounted] = useState(false);
useEffect(() => setIsMounted(true), []);
// 警告出ないやつ
function useIsMounted() {
return useSyncExternalStore(
() => () => {},
() => true, // Client
() => false // Server
);
}
useSyncExternalStore、こういう時に使うんだなと学びました。(React 18 の恩恵)
ちなみに、このパターンを知らないと「なんか警告出るけど動いてるからいいや」ってなりがち。後で痛い目見ます。(見ました)
3. SVG SMIL + CSS アニメーションの使い分け
Beaker コンポーネントで地味に悩んだやつ。
{
/* 液体の波打ち → SVG SMIL(SVG内部の属性操作に強い) */
}
<rect fill={liquidColor}>
<animate attributeName="y" values="..." dur="2s" />
</rect>;
{
/* 泡の上昇 → CSS(DOMの位置操作に強い) */
}
<div className="animate-beaker-bubble" />;
「全部 Framer Motion でいいじゃん」と思いました?
SVG 内部のアニメーションは SMIL が軽いんです。Framer Motion で SVG の y 属性をアニメーションさせると、DOM 操作が重くなる。適材適所。
4. CSS変数でテーマ対応
カラーパレットは CSS 変数で管理。ダークモード対応もこれで楽になります。(まだやってないけど)
:root {
--color-primary: oklch(0.65 0.24 260);
--color-reaction-pink: oklch(0.75 0.18 0);
--color-reaction-yellow: oklch(0.85 0.16 85);
/* 化学反応っぽい色を揃えた(自己満足) */
}
OKLCH 色空間を使っているのは、彩度と明度の調整がしやすいから。「ビビッドだけど目に痛くない」バランスを取るのに便利です。
パフォーマンス、ちゃんと考えた
「遊び心」を言い訳に重いサイトを作るわけにはいきません。
モバイルでは無効化
CursorParticles は PC のみ。スマホでマウスストーキングしても意味ないし、バッテリー食うし。
useEffect(() => {
const checkMobile = () => {
setIsMobile(
window.matchMedia('(pointer: coarse)').matches || window.innerWidth < 768
);
};
// ...
}, []);
if (isMobile) return null; // さようなら
pointer: coarse でタッチデバイスを検出しています。window.innerWidth だけだとタブレットを見逃す。
パーティクル数の上限
無限に増えるとブラウザが死ぬので、上限30個。古いやつから消える。
if (updated.length > maxParticles) {
return updated.slice(-maxParticles); // 老害は退場
}
30個という数字は「見た目がちょうどいい」と「パフォーマンス」のバランス。60fps を維持できる限界を探りました。(目視で)
イベントのスロットリング
マウス移動イベント、毎フレーム処理したら死ぬ。50ms でスロットリング。
throttleTimer = setTimeout(() => {
createParticles(e.clientX, e.clientY);
throttleTimer = null;
}, 50); // 50ms = 20fps、これで十分
アニメーション自体は 60fps で動くけど、パーティクル生成は 20fps で十分。人間の目には滑らかに見えます。(たぶん)
使用例:お問い合わせフォーム
実際にサイトで使ってるコード。こんな感じで組み合わせています。
import {
BubblesBackground,
LabButton,
LabCard,
LabCardContent,
LabCardHeader,
LabCardTitle,
LabInput,
LabTextarea,
} from '@/components/lab';
export function ContactForm() {
return (
<div className="relative">
<BubblesBackground count={10} />
<LabCard tilt={2} glowColor="blue">
<LabCardHeader>
<LabCardTitle>お問い合わせ</LabCardTitle>
</LabCardHeader>
<LabCardContent>
<form className="space-y-4">
<LabInput placeholder="お名前" glowColor="blue" />
<LabInput type="email" placeholder="メール" glowColor="pink" />
<LabTextarea placeholder="内容" glowColor="green" />
<LabButton variant="reaction">送信する</LabButton>
</form>
</LabCardContent>
</LabCard>
</div>
);
}
背景に泡が浮いてて、フォームが傾いてて、ボタンがグラデーション。
うるさい?
でも、普通のフォームより記憶に残りませんか。「あの泡が出るサイト」って覚えてもらえたら、それはもうブランディングです。(自己弁護)
まとめ:遊び心、意外と難しい
playpark Lab UI は、遊び心と技術的な堅牢性を両立させる実験でした。
- 科学実験室モチーフ → 破線、泡、手書き風
- SSR 対応 → Seeded random、useSyncExternalStore
- パフォーマンス → モバイル無効化、スロットリング
「コーポレートサイトだから堅くする」という固定観念、捨ててみませんか?
「やりすぎ」と言われても、自分が良いと思ったらそのまま通す。それが playpark スタイルです。
...ただし、本当にやりすぎると社内から「目がチカチカする」と言われるので、ほどほどに。
あわせて読みたい
- ブログ運用を完全自動化 — GitHubリポジトリから記事・サムネイル・SNS投稿まで — ブログ運用の自動化事例
→ 気軽に相談する


