In browser extensions and embedded UIs for enterprise portals, replicating the host app’s design system is challenging. Rather than chasing obfuscated class names or framework-specific styles, cloning existing native elements and replacing only what’s necessary gives you visual consistency “for free.”
Code
interface CloneButtonOptions { /** Selector for the template element to clone */ templateSelector: string /** New text to replace */ text: string /** New icon to replace (optional) */ icon?: string | HTMLElement /** Click handler */ onClick: (event: MouseEvent) => void}
/** * Clone an existing button from the host app and generate a new button */export const cloneNativeButton = ( options: CloneButtonOptions): HTMLElement | null => { const { templateSelector, text, icon, onClick } = options
// Get the template element const template = document.querySelector<HTMLElement>(templateSelector) if (!template) { console.warn(`Template not found: ${templateSelector}`) return null }
// Create a deep clone (excluding event handlers) const cloned = template.cloneNode(true) as HTMLElement
// Replace text node const textNode = findTextNode(cloned) if (textNode) { textNode.textContent = text }
// Replace icon (if specified) if (icon) { const iconElement = findIconElement(cloned) if (iconElement) { const newIcon = typeof icon === 'string' ? createIconFromString(icon) : icon iconElement.replaceWith(newIcon) } }
// Event listeners are not cloned, so attach new ones cloned.addEventListener('click', onClick)
// Remove ID if present (avoid duplicates) if (cloned.id) { cloned.removeAttribute('id') }
return cloned}
/** * Find the first text node within the element */const findTextNode = (element: HTMLElement): Text | null => { const walker = document.createTreeWalker( element, NodeFilter.SHOW_TEXT, { acceptNode: (node) => { // Exclude whitespace-only nodes return node.textContent?.trim() ? NodeFilter.FILTER_ACCEPT : NodeFilter.FILTER_REJECT } } ) return walker.nextNode() as Text | null}
/** * Find the first icon element (svg, img, i, etc.) */const findIconElement = (element: HTMLElement): Element | null => { return ( element.querySelector('svg') || element.querySelector('img') || element.querySelector('i[class*="icon"]') || element.querySelector('[role="img"]') )}
/** * Create an icon element from an HTML string */const createIconFromString = (html: string): HTMLElement => { const template = document.createElement('template') template.innerHTML = html.trim() return template.content.firstElementChild as HTMLElement}Usage
// Reuse GitHub's existing button styleconst myButton = cloneNativeButton({ templateSelector: '.Button--primary', text: 'Deploy', icon: '<svg>...</svg>', onClick: () => { console.log('Deploying...') }})
// Append to DOMdocument.querySelector('.toolbar')?.appendChild(myButton)How It Works
- Use
querySelectorto retrieve the template element to clone - Create a deep copy with
cloneNode(true)(includes child elements) - Traverse text nodes with
TreeWalkerand replace them - Find icon elements with
querySelectorand replace them - Event listeners are not cloned, so attach new ones with
addEventListener - Remove the
idattribute if present to avoid duplicates
Note that cloneNode preserves styles, classes, and data attributes, but event listeners are not copied.
Benefits
- Visual Consistency: Naturally inherits the host app’s design system
- No CSS Required: No need to chase obfuscated class names or framework styles
- Maintainable: Automatically follows host design updates
- Lightweight: Implemented with DOM APIs only, no dependencies
Caveats
cloneNode copies the current state of the DOM, so dynamically changing classes or styles (like hover states) are not cloned. Also, if the template element is removed, this approach stops working, so include existence checks.
Applications
- Add buttons with visual consistency in browser extensions
- Embed features that naturally blend into complex enterprise portal design systems
- Insert custom operation panels into third-party dashboards
hsb.horse