# XML & SVG Document Support

Source: https://inspectorlab.dev/xml-svg-support

> For the complete documentation index, see [llms.txt](https://inspectorlab.dev/llms.txt).

## XML & SVG Document Support

Inspector Lab extends its inspection capabilities beyond traditional HTML documents to support **XML documents** and **standalone SVG files** opened directly in the browser. This page explains how the inspector adapts itself to render and function correctly in these non-HTML environments.

### The Challenge: Non-HTML Namespaces

When you open a standalone SVG file or XML document directly in a browser tab, the document operates in a different namespace than standard HTML. This creates several technical obstacles:

- `document.createElement()` creates elements in the **null namespace** instead of the HTML namespace
- Elements lack the `.style` property, `attachShadow()` capability, and CSS styling
- HTML-based UI frameworks like React and styled-components break because they depend on `document.createElement()` producing proper HTML elements
- The inspector's chrome (overlays, panels, highlights) cannot render at all

### The Solution: Namespace Redirection

Inspector Lab patches `document.createElement()` at the injection level—before any other code runs—to transparently redirect element creation to the XHTML namespace:

```ts
document.createElement = ((tagName: string, options?: ElementCreationOptions) =>
  document.createElementNS(
    "http://www.w3.org/1999/xhtml",
    tagName.toLowerCase(),
    options,
  )
) as typeof document.createElement;
```

This patch is applied only in the isolated world where the injected inspector lives, so **page scripts never see it**. Every element the inspector creates—React components, styled-component wrappers, overlays—now renders correctly as proper HTML elements.

#### Detection

The inspector detects whether it's running on an HTML or XML document by testing whether `document.createElement()` natively produces HTML-namespace elements:

```ts
export const isHtmlDom: boolean = (() => {
  try {
    return document.createElement("div").namespaceURI === "http://www.w3.org/1999/xhtml";
  } catch {
    return false;
  }
})();
```

### Synthetic Document Head

XML documents have no `<head>` element, yet many libraries—including styled-components and the inspector itself—assume `document.head` exists. They use it to:

- Probe for CSP meta tags
- Append stylesheets
- Sync theme metadata

The compatibility layer creates a synthetic head that satisfies these requirements:

```ts
const detachedHead = document.createElementNS("http://www.w3.org/1999/xhtml", "head");
Object.defineProperty(document, "head", {
  configurable: true,
  get: () => document.getElementById("inspector-lab-extension-layer") ?? detachedHead,
});
```

Before bootstrap completes, `document.head` returns a detached stand-in. Once the inspector's overlay layer is created, the getter switches to return that layer, allowing appended scripts (like the page bridge) and styles to take effect normally.

### SVG Overlay: The ForeignObject Container

On SVG documents, HTML elements appended directly to the root `<svg>` never render. The inspector solves this by wrapping its entire UI in an SVG `<foreignObject>` element:

```ts
export const FOREIGN_LAYER_ID = "inspector-lab-extension-layer";
```

The `<foreignObject>` acts as a viewport-sized container with the full dimensions of the document. Inside it, the inspector renders its React tree, styled-components, and all overlay UI—all of which now render correctly because they're HTML elements inside a `<foreignObject>`.

The overlay root is dynamically set after bootstrap:

```ts
export function setOverlayRoot(element: Element): void {
  overlayParent = element;
}
```

Once the layer is created, all subsequent overlays (element highlights, the picker box, etc.) are positioned absolutely inside this container instead of fixed to the viewport.

### Positioning Mode: Absolute vs. Fixed

Because overlays live inside the `<foreignObject>` layer on SVG documents, their positioning changes from fixed to absolute:

```ts
export function overlayPosition(): "fixed" | "absolute" {
  return overlayParent ? "absolute" : "fixed";
}
```

On HTML documents, overlays are `position: fixed` and stick to the viewport. On SVG documents, they're `position: absolute` and positioned relative to the `<foreignObject>` container—which itself is viewport-sized, producing identical visual geometry while avoiding WebKit's buggy fixed-inside-foreignObject rendering.

### Stylable Elements

Both HTML and SVG elements expose the `CSSStyleDeclaration` interface via their `.style` property, allowing the inspector to read and edit inline styles uniformly:

```ts
export type StylableElement = HTMLElement | SVGElement;
```

The DOM provides no shared base type between these two, so the inspector defines this union explicitly and uses it wherever inline styles are inspected or modified.

### Stylesheet Rule Matching

The `matchedCssRules()` function collects all stylesheet rules that apply to a given element. It works identically on HTML and SVG documents because both support:

- `document.styleSheets`
- `CSSStyleSheet` and `CSSRuleList` APIs
- `element.matches()` for selector matching

However, the function is careful to handle both `HTMLStyleElement` and `SVGStyleElement`:

```ts
const isStyleTag =
  owner instanceof HTMLStyleElement || owner instanceof SVGStyleElement;
```

### Safe Element Serialization

When copying an element's markup, the inspector must strip its own injected nodes. The `serializeElement()` function does this by cloning the element and removing any nodes with inspector-specific IDs:

```ts
export function serializeElement(element: Element): string {
  const clone = element.cloneNode(true) as Element;
  for (const id of INSPECTOR_NODE_IDS) {
    clone.querySelector(`#${id}`)?.remove();
  }
  return clone.outerHTML;
}
```

This works on both HTML and SVG documents because `cloneNode()` and `outerHTML` are supported across both namespaces.

### Implementation Order: Import-Nothing Module

The compatibility layer must be imported **first**, before any other code that might create elements:

```ts
// File: xml-compat.ts
/**
 * This module must stay the first import of the injected entry, ahead of
 * anything that might create elements.
 */
```

Because the patch is applied at module evaluation time, importing it before anything else ensures the redirected `document.createElement` is in place before React, styled-components, or the inspector's own DOM utilities run.

<Callout type="info">
The `FOREIGN_LAYER_ID` constant lives in the compatibility module—which imports nothing—so the head shim can find it and switch `document.head` without pulling in the entire inspector.
</Callout>

### Testing on SVG Documents

To verify inspector functionality on SVG documents locally:

1. Create or locate a standalone `.svg` file
2. Open it directly in your browser tab (not embedded in HTML)
3. Open the Inspector Lab DevTools
4. The inspector should display normally, with full element inspection, highlighting, and panel support

The namespace redirection and `<foreignObject>` overlay are transparent—the inspector works identically on both HTML and SVG documents, despite the very different underlying DOM architecture.
