Radial Tool Dock ラジアル・ツールドック
A floating radial utility menu that fans out around a central trigger with spring-inspired transforms, SVG iconography, Escape key closure, and ARIA state handling. フローティングボタンから扇状に展開する円形ツールドック。Escapeキー閉鎖やARIA属性に対応。
ライブデモ Live Demo
Activate dock or press Esc to collapse
概要・用途・特徴Overview, Usage & Features
何ができるかWhat it does
A floating radial utility menu that fans out around a central trigger with spring-inspired transforms, SVG iconography, Escape key closure, and ARIA state handling.
フローティングボタンから扇状に展開する円形ツールドック。Escapeキー閉鎖やARIA属性に対応。
どこで使うかWhere to use
Portfolio, product showcase, landing page.
ポートフォリオ、製品紹介、ランディングページ。
特徴Key features
A floating radial utility menu that fans out around a central trigger with spring-inspired transforms, SVG iconography, Escape key closure, and ARIA state handling.
フローティングボタンから扇状に展開する円形ツールドック。Escapeキー閉鎖やARIA属性に対応。
調整可能パラメータ Adjustable Parameters
| Parameter | Default | Description |
|---|---|---|
--rd-accent | #00f2fe | Change this CSS custom property to match your brand color. |
実装コード Implementation Code
<div class="demo-stage rd-root"><div class="rd-container"><nav class="rd-nav" aria-label="Quick Actions Radial Menu"><button type="button" class="rd-trigger" aria-expanded="false" aria-label="Toggle creative toolbox"><span class="rd-trigger-icon" aria-hidden="true"><svg class="rd-icon rd-icon-plus" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/></svg></span></button><div class="rd-items-wrap" aria-hidden="true"><button type="button" class="rd-item rd-item-1" tabindex="-1" aria-label="Brush tool"><svg class="rd-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><path d="M18 13.5L8 3.5L4 7.5L14 17.5M18 13.5L20 15.5C21 16.5 21 18 20 19C19 20 17.5 20 16.5 19L14 17.5"/></svg><span class="rd-tooltip">BRUSH</span></button><button type="button" class="rd-item rd-item-2" tabindex="-1" aria-label="Color palette tool"><svg class="rd-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><circle cx="12" cy="12" r="10"/><circle cx="8" cy="10" r="1.5" fill="currentColor"/><circle cx="12" cy="7" r="1.5" fill="currentColor"/><circle cx="16" cy="10" r="1.5" fill="currentColor"/></svg><span class="rd-tooltip">PALETTE</span></button><button type="button" class="rd-item rd-item-3" tabindex="-1" aria-label="Vector curve tool"><svg class="rd-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><path d="M3 19C7 19 8 5 12 5C16 5 17 19 21 19"/><circle cx="12" cy="5" r="2"/><circle cx="3" cy="19" r="2"/><circle cx="21" cy="19" r="2"/></svg><span class="rd-tooltip">CURVES</span></button><button type="button" class="rd-item rd-item-4" tabindex="-1" aria-label="Export layer tool"><svg class="rd-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><path d="M21 15V19C21 20.1 20.1 21 19 21H5C3.9 21 3 20.1 3 19V15"/><polyline points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/></svg><span class="rd-tooltip">EXPORT</span></button></div></nav><div class="rd-status" aria-live="polite"><span class="rd-badge">STANDBY</span><p class="rd-hint">Activate dock or press Esc to collapse</p></div></div></div>
<script>
(() => { document.querySelectorAll('.rd-root').forEach((root) => { const nav = root.querySelector('.rd-nav'); const trigger = root.querySelector('.rd-trigger'); const wrap = root.querySelector('.rd-items-wrap'); const items = root.querySelectorAll('.rd-item'); const badge = root.querySelector('.rd-badge'); if (!nav || !trigger || !items.length) return; const setOpen = (open) => { nav.classList.toggle('rd-open', open); trigger.setAttribute('aria-expanded', String(open)); if (wrap) wrap.setAttribute('aria-hidden', String(!open)); items.forEach((it) => it.setAttribute('tabindex', open ? '0' : '-1')); if (badge) badge.textContent = open ? 'DOCK ACTIVE' : 'STANDBY'; if (open) items[0].focus(); }; trigger.addEventListener('click', () => { setOpen(!nav.classList.contains('rd-open')); }); root.addEventListener('keydown', (e) => { if (e.key === 'Escape' && nav.classList.contains('rd-open')) { setOpen(false); trigger.focus(); } }); }); })();
document.querySelectorAll(".rd-root").forEach(root=>root.querySelectorAll(".rd-item").forEach(btn=>btn.addEventListener("click",()=>{root.querySelector(".rd-badge").textContent=btn.querySelector(".rd-tooltip").textContent+" SELECTED";})));
</script>
.rd-root{--rd-bg:#080c14;--rd-accent:#00f2fe;--rd-glow:#4facfe;--rd-surface:#101726;display:flex;align-items:center;justify-content:center;min-height:380px;padding:32px 16px;background:radial-gradient(ellipse at center,#152036 0%,var(--rd-bg) 85%);color:#f1f5f9;font-family:system-ui,-apple-system,sans-serif;box-sizing:border-box;width:100%}.rd-root *,.rd-root *::before,.rd-root *::after{box-sizing:border-box}.rd-root .rd-container{position:relative;display:flex;flex-direction:column;align-items:center;gap:36px;width:100%;max-width:420px}.rd-root .rd-nav{position:relative;width:80px;height:80px;display:flex;align-items:center;justify-content:center}.rd-root .rd-trigger{width:68px;height:68px;border-radius:50%;background:linear-gradient(135deg,var(--rd-accent),var(--rd-glow));border:2px solid rgba(255,255,255,0.4);color:#080c14;cursor:pointer;display:flex;align-items:center;justify-content:center;box-shadow:0 12px 28px rgba(0,242,254,0.35);z-index:10;transition:transform 0.25s cubic-bezier(0.34,1.56,0.64,1),box-shadow 0.25s}.rd-root .rd-trigger:hover{transform:scale(1.08);box-shadow:0 16px 36px rgba(0,242,254,0.5)}.rd-root .rd-trigger:focus-visible{outline:3px solid #ffffff;outline-offset:3px}.rd-root .rd-icon-plus{width:28px;height:28px;transition:transform 0.3s cubic-bezier(0.34,1.56,0.64,1)}.rd-root .rd-nav.rd-open .rd-icon-plus{transform:rotate(135deg)}.rd-root .rd-items-wrap{position:absolute;inset:0;pointer-events:none;z-index:5}.rd-root .rd-nav.rd-open .rd-items-wrap{pointer-events:auto}.rd-root .rd-item{position:absolute;top:50%;left:50%;width:48px;height:48px;margin:-24px 0 0 -24px;border-radius:50%;background:var(--rd-surface);border:1px solid rgba(255,255,255,0.18);color:#e2e8f0;display:flex;align-items:center;justify-content:center;cursor:pointer;opacity:0;transform:scale(0.3) translate(0,0);transition:transform 0.35s cubic-bezier(0.34,1.56,0.64,1),opacity 0.25s,background 0.2s,box-shadow 0.2s}.rd-root .rd-item:hover{background:#1e293b;color:var(--rd-accent);box-shadow:0 0 16px rgba(0,242,254,0.4)}.rd-root .rd-item:focus-visible{outline:2px solid var(--rd-accent);opacity:1}.rd-root .rd-icon{width:20px;height:20px}.rd-root .rd-tooltip{position:absolute;bottom:-22px;font-size:9px;font-weight:800;letter-spacing:1px;background:rgba(8,12,20,0.9);padding:2px 6px;border-radius:4px;border:1px solid rgba(255,255,255,0.1);opacity:0;pointer-events:none;transition:opacity 0.15s;white-space:nowrap}.rd-root .rd-item:hover .rd-tooltip{opacity:1}.rd-root .rd-nav.rd-open .rd-item{opacity:1}.rd-root .rd-nav.rd-open .rd-item-1{transform:translate(-84px,-48px) scale(1)}.rd-root .rd-nav.rd-open .rd-item-2{transform:translate(-32px,-92px) scale(1)}.rd-root .rd-nav.rd-open .rd-item-3{transform:translate(32px,-92px) scale(1)}.rd-root .rd-nav.rd-open .rd-item-4{transform:translate(84px,-48px) scale(1)}.rd-root .rd-status{text-align:center}.rd-root .rd-badge{font-size:10px;font-weight:800;letter-spacing:2px;color:var(--rd-accent);background:rgba(0,242,254,0.1);padding:4px 10px;border-radius:999px;border:1px solid rgba(0,242,254,0.3)}.rd-root .rd-hint{font-size:12px;color:#94a3b8;margin:8px 0 0 0}@media (prefers-reduced-motion: no-preference){.rd-root .rd-trigger{animation:rd-pulse 3s infinite alternate}}@keyframes rd-pulse{0%{box-shadow:0 8px 20px rgba(0,242,254,0.25)}100%{box-shadow:0 14px 34px rgba(0,242,254,0.45)}}
@media not all and (prefers-reduced-motion: no-preference){.rd-root *{transition:none!important;animation:none!important}}
.rd-root .rd-container{padding-top:78px}.rd-root .rd-item:focus-visible .rd-tooltip{opacity:1}
import { useState, useEffect, useRef } from "react";
import "./N-011.css";
function RadialToolDock() {
const [open, setOpen] = useState(false);
const [tool, setTool] = useState("");
const rootRef = useRef(null);
useEffect(() => {
if (open) rootRef.current?.querySelector(".rd-item")?.focus();
}, [open]);
return <div className="demo-stage rd-root" ref={rootRef} onKeyDown={(event) => {
if (event.key === "Escape" && open) {
setOpen(false);
rootRef.current?.querySelector(".rd-trigger")?.focus();
}
}}><div className="rd-container"><nav aria-label="Quick Actions Radial Menu" className={"rd-nav" + (open ? " rd-open" : "")}><button type="button" className="rd-trigger" aria-label="Toggle creative toolbox" aria-expanded={open} onClick={() => {
setTool("");
setOpen((value) => !value);
}}><span className="rd-trigger-icon" aria-hidden="true"><svg className="rd-icon rd-icon-plus" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5"><line x1="12" y1="5" x2="12" y2="19" /><line x1="5" y1="12" x2="19" y2="12" /></svg></span></button><div className="rd-items-wrap" aria-hidden={!open}><button type="button" className="rd-item rd-item-1" aria-label="Brush tool" tabIndex={open ? 0 : -1} onClick={() => setTool("BRUSH")}><svg className="rd-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" aria-hidden="true"><path d="M18 13.5L8 3.5L4 7.5L14 17.5M18 13.5L20 15.5C21 16.5 21 18 20 19C19 20 17.5 20 16.5 19L14 17.5" /></svg><span className="rd-tooltip">{"BRUSH"}</span></button><button type="button" className="rd-item rd-item-2" aria-label="Color palette tool" tabIndex={open ? 0 : -1} onClick={() => setTool("PALETTE")}><svg className="rd-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" aria-hidden="true"><circle cx="12" cy="12" r="10" /><circle cx="8" cy="10" r="1.5" fill="currentColor" /><circle cx="12" cy="7" r="1.5" fill="currentColor" /><circle cx="16" cy="10" r="1.5" fill="currentColor" /></svg><span className="rd-tooltip">{"PALETTE"}</span></button><button type="button" className="rd-item rd-item-3" aria-label="Vector curve tool" tabIndex={open ? 0 : -1} onClick={() => setTool("CURVES")}><svg className="rd-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" aria-hidden="true"><path d="M3 19C7 19 8 5 12 5C16 5 17 19 21 19" /><circle cx="12" cy="5" r="2" /><circle cx="3" cy="19" r="2" /><circle cx="21" cy="19" r="2" /></svg><span className="rd-tooltip">{"CURVES"}</span></button><button type="button" className="rd-item rd-item-4" aria-label="Export layer tool" tabIndex={open ? 0 : -1} onClick={() => setTool("EXPORT")}><svg className="rd-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" aria-hidden="true"><path d="M21 15V19C21 20.1 20.1 21 19 21H5C3.9 21 3 20.1 3 19V15" /><polyline points="7 10 12 15 17 10" /><line x1="12" y1="15" x2="12" y2="3" /></svg><span className="rd-tooltip">{"EXPORT"}</span></button></div></nav><div className="rd-status" aria-live="polite"><span className="rd-badge">{tool ? tool + " SELECTED" : open ? "DOCK ACTIVE" : "STANDBY"}</span><p className="rd-hint">{"Activate dock or press Esc to collapse"}</p></div></div></div>;
}
export {
RadialToolDock as default
};
.rd-root{--rd-bg:#080c14;--rd-accent:#00f2fe;--rd-glow:#4facfe;--rd-surface:#101726;display:flex;align-items:center;justify-content:center;min-height:380px;padding:32px 16px;background:radial-gradient(ellipse at center,#152036 0%,var(--rd-bg) 85%);color:#f1f5f9;font-family:system-ui,-apple-system,sans-serif;box-sizing:border-box;width:100%}.rd-root *,.rd-root *::before,.rd-root *::after{box-sizing:border-box}.rd-root .rd-container{position:relative;display:flex;flex-direction:column;align-items:center;gap:36px;width:100%;max-width:420px}.rd-root .rd-nav{position:relative;width:80px;height:80px;display:flex;align-items:center;justify-content:center}.rd-root .rd-trigger{width:68px;height:68px;border-radius:50%;background:linear-gradient(135deg,var(--rd-accent),var(--rd-glow));border:2px solid rgba(255,255,255,0.4);color:#080c14;cursor:pointer;display:flex;align-items:center;justify-content:center;box-shadow:0 12px 28px rgba(0,242,254,0.35);z-index:10;transition:transform 0.25s cubic-bezier(0.34,1.56,0.64,1),box-shadow 0.25s}.rd-root .rd-trigger:hover{transform:scale(1.08);box-shadow:0 16px 36px rgba(0,242,254,0.5)}.rd-root .rd-trigger:focus-visible{outline:3px solid #ffffff;outline-offset:3px}.rd-root .rd-icon-plus{width:28px;height:28px;transition:transform 0.3s cubic-bezier(0.34,1.56,0.64,1)}.rd-root .rd-nav.rd-open .rd-icon-plus{transform:rotate(135deg)}.rd-root .rd-items-wrap{position:absolute;inset:0;pointer-events:none;z-index:5}.rd-root .rd-nav.rd-open .rd-items-wrap{pointer-events:auto}.rd-root .rd-item{position:absolute;top:50%;left:50%;width:48px;height:48px;margin:-24px 0 0 -24px;border-radius:50%;background:var(--rd-surface);border:1px solid rgba(255,255,255,0.18);color:#e2e8f0;display:flex;align-items:center;justify-content:center;cursor:pointer;opacity:0;transform:scale(0.3) translate(0,0);transition:transform 0.35s cubic-bezier(0.34,1.56,0.64,1),opacity 0.25s,background 0.2s,box-shadow 0.2s}.rd-root .rd-item:hover{background:#1e293b;color:var(--rd-accent);box-shadow:0 0 16px rgba(0,242,254,0.4)}.rd-root .rd-item:focus-visible{outline:2px solid var(--rd-accent);opacity:1}.rd-root .rd-icon{width:20px;height:20px}.rd-root .rd-tooltip{position:absolute;bottom:-22px;font-size:9px;font-weight:800;letter-spacing:1px;background:rgba(8,12,20,0.9);padding:2px 6px;border-radius:4px;border:1px solid rgba(255,255,255,0.1);opacity:0;pointer-events:none;transition:opacity 0.15s;white-space:nowrap}.rd-root .rd-item:hover .rd-tooltip{opacity:1}.rd-root .rd-nav.rd-open .rd-item{opacity:1}.rd-root .rd-nav.rd-open .rd-item-1{transform:translate(-84px,-48px) scale(1)}.rd-root .rd-nav.rd-open .rd-item-2{transform:translate(-32px,-92px) scale(1)}.rd-root .rd-nav.rd-open .rd-item-3{transform:translate(32px,-92px) scale(1)}.rd-root .rd-nav.rd-open .rd-item-4{transform:translate(84px,-48px) scale(1)}.rd-root .rd-status{text-align:center}.rd-root .rd-badge{font-size:10px;font-weight:800;letter-spacing:2px;color:var(--rd-accent);background:rgba(0,242,254,0.1);padding:4px 10px;border-radius:999px;border:1px solid rgba(0,242,254,0.3)}.rd-root .rd-hint{font-size:12px;color:#94a3b8;margin:8px 0 0 0}@media (prefers-reduced-motion: no-preference){.rd-root .rd-trigger{animation:rd-pulse 3s infinite alternate}}@keyframes rd-pulse{0%{box-shadow:0 8px 20px rgba(0,242,254,0.25)}100%{box-shadow:0 14px 34px rgba(0,242,254,0.45)}}
@media not all and (prefers-reduced-motion: no-preference){.rd-root *{transition:none!important;animation:none!important}}
.rd-root .rd-container{padding-top:78px}.rd-root .rd-item:focus-visible .rd-tooltip{opacity:1}
仕組みとカスタマイズHow It Works & Customization
仕組みHow it works
Scoped CSS creates the visual design; small vanilla JavaScript handlers update the local demo state.
スコープ化したCSSで描画し、Vanilla JavaScriptでデモ内の状態だけを更新します。
カスタマイズ方法Customization
Change this CSS custom property to match your brand color.
CSSカスタムプロパティでブランドカラーを変更できます。
注意点Caveats
The sample content is fictional. No external assets or libraries are required. Motion follows the operating system preference. Demo labels are English; surrounding documentation supports Japanese and English.
デモ内容は架空です。外部素材・ライブラリは不要。OSの動きを減らす設定に対応します。デモ内のラベルは英語、解説は日英対応です。
よくある質問 Frequently Asked Questions
Does this demo need external libraries? 外部ライブラリは必要ですか?
The sample content is fictional. No external assets or libraries are required. Motion follows the operating system preference. Demo labels are English; surrounding documentation supports Japanese and English.
デモ内容は架空です。外部素材・ライブラリは不要。OSの動きを減らす設定に対応します。デモ内のラベルは英語、解説は日英対応です。
AIへの指示テンプレート AI Prompt Template
以下をAIに貼り付けると実装を依頼できます。 Paste the following into your AI assistant to request implementation.