logo hsb.horse
← スニペット一覧に戻る

Snippets

ネイティブUI要素をクローンしてスタイルを継承する

ホストアプリの既存ボタンをcloneNodeで複製し、アイコンとテキストだけ置き換えることで視覚的一貫性を維持する軽量パターン。難読化CSSと戦わない。

公開日: 更新日:

ブラウザ拡張や企業ポータルの埋め込みUIでは、ホストアプリのデザインシステムを再現するのが難しい。難読化されたクラス名やフレームワーク固有のスタイルを追いかけるより、既存のネイティブ要素をクローンして必要な部分だけ置き換える方が、視覚的一貫性を「無料で」得られる。

コード

interface CloneButtonOptions {
/** クローン元のセレクター */
templateSelector: string
/** 置き換える新しいテキスト */
text: string
/** 置き換える新しいアイコン(省略可) */
icon?: string | HTMLElement
/** クリック時のコールバック */
onClick: (event: MouseEvent) => void
}
/**
* ホストアプリの既存ボタンをクローンして新しいボタンを生成する
*/
export const cloneNativeButton = (
options: CloneButtonOptions
): HTMLElement | null => {
const { templateSelector, text, icon, onClick } = options
// クローン元を取得
const template = document.querySelector<HTMLElement>(templateSelector)
if (!template) {
console.warn(`Template not found: ${templateSelector}`)
return null
}
// 深いクローンを作成(子要素・属性・イベントハンドラーは除く)
const cloned = template.cloneNode(true) as HTMLElement
// テキストノードを置き換え
const textNode = findTextNode(cloned)
if (textNode) {
textNode.textContent = text
}
// アイコンを置き換え(指定された場合)
if (icon) {
const iconElement = findIconElement(cloned)
if (iconElement) {
const newIcon = typeof icon === 'string'
? createIconFromString(icon)
: icon
iconElement.replaceWith(newIcon)
}
}
// 元のイベントリスナーは複製されないため、新しいものを設定
cloned.addEventListener('click', onClick)
// IDがあれば削除(重複回避)
if (cloned.id) {
cloned.removeAttribute('id')
}
return cloned
}
/**
* 要素内の最初のテキストノードを見つける
*/
const findTextNode = (element: HTMLElement): Text | null => {
const walker = document.createTreeWalker(
element,
NodeFilter.SHOW_TEXT,
{
acceptNode: (node) => {
// 空白のみのノードを除外
return node.textContent?.trim()
? NodeFilter.FILTER_ACCEPT
: NodeFilter.FILTER_REJECT
}
}
)
return walker.nextNode() as Text | null
}
/**
* 要素内の最初のアイコン要素を見つける(svg, img, i など)
*/
const findIconElement = (element: HTMLElement): Element | null => {
return (
element.querySelector('svg') ||
element.querySelector('img') ||
element.querySelector('i[class*="icon"]') ||
element.querySelector('[role="img"]')
)
}
/**
* HTML文字列からアイコン要素を生成
*/
const createIconFromString = (html: string): HTMLElement => {
const template = document.createElement('template')
template.innerHTML = html.trim()
return template.content.firstElementChild as HTMLElement
}

使用例

// GitHub の既存ボタンスタイルを流用する
const myButton = cloneNativeButton({
templateSelector: '.Button--primary',
text: 'Deploy',
icon: '<svg>...</svg>',
onClick: () => {
console.log('Deploying...')
}
})
// DOM に追加
document.querySelector('.toolbar')?.appendChild(myButton)

仕組み

  1. querySelector でクローン元のテンプレート要素を取得する
  2. cloneNode(true) で深い複製を作成(子要素を含む)
  3. TreeWalker でテキストノードを探索して置き換える
  4. querySelector でアイコン要素を探索して置き換える
  5. 元のイベントリスナーは複製されないため addEventListener で新しく設定する
  6. ID 属性があれば削除して重複を避ける

cloneNode はスタイル・クラス・data属性を保持するが、イベントリスナーは複製されない点に注意すること。

メリット

  • 視覚的一貫性:ホストアプリのデザインシステムを自然に継承できる
  • CSS不要:難読化されたクラス名やフレームワークのスタイルを追いかける必要がない
  • 保守性が高い:ホスト側のデザイン更新に自動追従する
  • 軽量:DOM API のみで実装でき、依存ライブラリ不要

注意点

cloneNode は現在の DOM の状態をコピーするため、動的にクラスやスタイルが変わる要素(ホバー状態など)は複製されない。また、テンプレート要素が削除されると機能しなくなるため、存在確認を入れておくこと。

応用

  • ブラウザ拡張で既存UIと統一感のあるボタンを追加する
  • 企業ポータルの複雑なデザインシステムへ自然に溶け込む機能を埋め込む
  • サードパーティのダッシュボードへカスタム操作パネルを挿入する

実務メモ

このスニペットは、TypeScript、JavaScript、DOM、Browser Extension、UI Pattern の周辺で同じ操作や判定を毎回書きたくない時に向く。小さな補助として切り出しておくと、呼び出し側では意図だけを追いやすい。

逆に、分岐や前提条件が増えて責務が膨らむなら、1本のスニペットに詰め込まない方がよい。手順と helper を分けるか、役割ごとに切り出す方が保守しやすい。