Skip to main content

Chromium SVG <use> and <foreignObject>

Two mechanisms that create cross-tree references in SVG:

  • <use> — instantiate another element (or subtree) as a reference.
  • <foreignObject> — embed HTML (or any other XML) inside SVG.

Both introduce a tree-shape wrinkle that renderers need to understand.

<use>​

<use href="#target"> renders a copy of #target at the position specified by <use>'s x and y attributes, inheriting styles from the <use>'s ancestor chain (not the target's original ancestors).

Shadow-DOM instantiation​

Blink implements <use> by cloning the target into a user-agent shadow tree attached to the <use> element:

// third_party/blink/renderer/core/svg/svg_use_element.h
class SVGUseElement final : public SVGGraphicsElement,
public SVGURIReference,
public SVGResourceDocumentObserver {
public:
explicit SVGUseElement(Document&);
void InvalidateShadowTree();
void InvalidateTargetReference();
SVGGraphicsElement* VisibleTargetGraphicsElementForClipping() const;
SVGAnimatedLength* x() const { return x_.Get(); }
SVGAnimatedLength* y() const { return y_.Get(); }
Path ToClipPath() const;
};

When <use>'s href resolves, SVGUseElement::BuildPendingResource():

  1. Resolves the reference (same document or external .svg resource).
  2. Deep-clones the target subtree.
  3. Attaches the clone as the <use> element's user-agent shadow root.
  4. Applies <use>'s x/y/width/height to the cloned root (if it's a <symbol> or <svg>; otherwise width/height are ignored).
  5. Style cascade runs on the cloned subtree as if it were a child of <use> — this is how inheritance "flips" from the original's ancestors to the use site's ancestors.

Layout and paint​

The shadow tree participates in layout and paint just like any ordinary subtree. LayoutSVGTransformableContainer (or equivalent) handles the x/y translation. Nothing special happens at paint time — the cloned subtree is painted like any other.

External references​

If href is foo.svg#id, Blink loads foo.svg as a resource document (SVGResourceDocument), parses it, and then resolves #id inside that document. The referenced element is then cloned into the local shadow tree, with its own resources (gradients, clip paths, etc.) carried along.

Invalidation​

Changes to the target element (or its descendants) trigger InvalidateShadowTree() on every <use> that references it. The shadow tree is torn down and rebuilt lazily. This is expensive for deeply instantiated elements — a common optimization target.

<symbol> vs <g>​

<symbol> elements are invisible by default (like <defs>), but when referenced by <use>, they become visible as <svg>-like viewport containers with their own viewBox / preserveAspectRatio. Semantically:

<use href="#sym" x="10" y="10" width="50" height="50"/>

becomes approximately:

<svg x="10" y="10" width="50" height="50" viewBox="...">
(children of #sym)
</svg>

The <use>-as-<svg> expansion is handled by the cloned shadow root being the <symbol>'s children wrapped in an implicit viewport container.

<use> as clip geometry​

LayoutSVGResourceClipper can include <use> as a clip path child. The SVGUseElement::ToClipPath() method returns the geometry of the resolved target, transformed by the <use>'s x/y — so clip paths that reference symbols via <use> can still collapse to a single Path on the fast path.

<foreignObject>​

<foreignObject> embeds a non-SVG document fragment (typically HTML) inside SVG, sized by its x, y, width, height attributes.

Layout bridging​

// third_party/blink/renderer/core/layout/svg/layout_svg_foreign_object.h
class LayoutSVGForeignObject final : public LayoutSVGBlock {
public:
explicit LayoutSVGForeignObject(Element* element);
bool IsObjectBoundingBoxValid() const;
bool NodeAtPointFromSVG(HitTestResult&,
const HitTestLocation&,
const PhysicalOffset&,
HitTestPhase);
};

LayoutSVGForeignObject extends LayoutSVGBlock, which in turn reuses LayoutNG block layout. Inside the foreign object, layout proceeds as ordinary HTML block layout — the SVG coordinate system stops at the <foreignObject> boundary.

The transform from SVG to HTML:

  • SVG x/y attributes become a CSS-pixel translation.
  • SVG transforms on ancestors are folded into the PaintLayer's compositor transform (foreign objects always create their own paint layer).
  • CSS zoom is applied carefully to avoid double-scaling (the SVG ancestor transforms already include zoom).

Paint bridging​

SVGForeignObjectPainter invokes PaintLayer::PaintLayerContents() on the foreign object's layer, which runs the HTML paint path. From outside the foreign object, its rendering is opaque — an SVG painter downstream of it sees a single paint layer, not the HTML subtree.

Hit-test trampoline​

Because <foreignObject> has its own PaintLayer, hit tests must bounce between SVG hit testing (path-based) and HTML hit testing (box-based):

bool LayoutSVGForeignObject::NodeAtPointFromSVG(
HitTestResult& result,
const HitTestLocation& hit_test_location,
const PhysicalOffset& accumulated_offset,
HitTestPhase phase);

The caller (an SVG ancestor) calls NodeAtPointFromSVG with SVG-local coordinates; the method transforms them to HTML box coordinates and delegates to PaintLayer::HitTest().

Nested SVG in foreign object​

An <svg> inside a <foreignObject> creates a new LayoutSVGRoot inside the HTML subtree, which behaves as a replaced element. The SVG coordinate space is established anew.

Why it exists​

The common use case is HTML-styled rich text inside an SVG illustration — e.g., an infographic with <foreignObject>-embedded captions. The foreignObject element preserves HTML semantics (text selection, line breaking, CSS layout) inside an SVG context where those are otherwise unavailable.

Source files​

FileRole
core/svg/svg_use_element.h<use> element; shadow instantiation, external ref loading
core/svg/svg_symbol_element.h<symbol> — viewport semantics when referenced
core/layout/svg/layout_svg_foreign_object.h<foreignObject> bridge
core/paint/svg_foreign_object_painter.ccPaint bridge
core/paint/svg_root_painter.ccLayoutSVGRoot paint entry (also used when SVG nests in HTML)