Pallas Athena

More on SVG Viewports: Nesting SVG's

Introduction

Having been engaged recently in the development of a Scalable Vector Graphics (SVG) framework for artists (the SVG Artists Collaborative ™) I've come to appreciate more and more the creative benefits afforded to vector art by the SVG standard. Elsewhere I've discussed many aspects of the standard, especially those that relate to SVG transforms. In this post I want to focus mainly on one important aspect -- namely the capability and implications of nesting svg elements.

Nesting SVG's

The <svg> element is the top-level, or root element in an SVG document. The concept of a root element is particularly important in xml markup languages like SVG. It's also important in HTML. But what's interesting to me is that unlike HTML (where you cannot nest the <html> element) nesting of <svg> elements is perfectly permissible. So what exactly does that mean?

Image: an animated Russian doll SVG graphic illustrating nesting by way of analogy.

Nesting <svg>'s simply means you can put one or more <svg> elements inside another.

<svg>
    <svg>
        ...
    </svg>
    <svg>
        <svg>
            ...
        </svg>
        ...
    </svg>
</svg>

That's it. Pure and simple. Think of it like a Russian doll. Remember those? You have this beautiful craft piece -- an ornately decorated figure -- and you open it up and there's another one inside. And you open that up and there's another, and so on and so on.

Same thing with <svg> elements. You can nest these guys as deeply as you need. In principle the recursion can be infinite.

What does Nesting SVG's Imply?

That sounds easy enough. But what are the implications of nesting SVG's? And why am I so excited about it? Well, the capability to nest <svg> elements is very cool and carries with it several powerful benefits. Let's take a moment to examine them.

Each SVG Has its Own ViewPort

First and foremost, each <svg> gets its own viewport. Remember, the SVG viewport is like a window into your creative SVG space. You can control aspects of the viewport using the viewBox attribute in concert with width height and preserveAspectRatio. Each svg node will essentially have it's own independent user coordinate system opening up whole new avenues for creativity 1 .

Standalone SVG Rendering Contexts

Each SVG is inherently a standalone rendering context -- it's like its own canvas. This enables creators to design modular re-usable components (e.g., icons, charts, game backgrounds, characters, etc.) that can be shown in isolation and composited into larger figures and scenes. Importantly, the capability to nest SVG documents facilitates collaboration and the use of third-party components or illustrations.

Built-in Aspect Ratio Control

Nested <svg> elements respect preserveAspectRatio settings, allowing better control of layout and scaling behavior. This is incredibly useful in responsive web-design with flexible layouts.

Isolation of CSS and styles

A nested SVG can be isolated in terms of style scope. This makes it possible to create complex UIs where encapsulation is needed to prevent unintended inheritance or style bleed from parent SVG's or HTML.

Demonstration: An SVG Carousel

To demonstrate these powerful benefits I've created an SVG carousel for use within the SVG Artists Collaborative™. Popular in web-deign and advertising, carousels are dynamic user-interface designs that can be used to showcase multiple pieces of content -- in this case nested SVG's.

FPS:

The carousel comprises a root <svg> element, which defines its viewport and several nested standalone SVG images and animations. If you click on the carousel viewport, you should see it start revolving. Noticed how the positioning and animation of all the nested SVG's are preserved!

Discussion

The carousel demonstrates the power and artistry that can be achieved using the SVG nesting approach. Generally speaking a lot can be achieved simply by grouping SVG elements using <g> wrappers and applying transformations, but that can quickly become unwieldy and difficult to maintain for larger and more complex artworks. The capability to composite larger works through nesting offers so much greater flexibility and creative options over and above grouping that I hardly even know where to begin.

However, over the course of designing the component I did encounter a few gotchas which I feel obliged to address here. The biggest problem I had to work out concerns stacking order and overlapping displays. To explain, I'll examine some of the design and implementation details around the carousel

Architecture

The carousel architecture takes advantage of <svg> element nesting to dynamically display multiple embedded SVG documents. The documents are loaded dynamically and appended to the DOM composing the structure shown in the following code fragment.

Listing 1

<svg id="carousel" 
     width="550" 
     height="412.5" 
     viewBox="-300 -100 800 400">

    <g class="svg-slide-wrapper" 
       transform="..." 
       data-current-rotation="102" 
       data-depth="97.49279121818236" >

       <svg id="svg_slide_1"
         width="300" height="170" viewBox="0 0 350 200" ... >
           ...
       </svg>

     </g>

</svg>

The fragment (abridged for the sake of brevity) shows the nested structure of the SVG's. Note also the the use of data- attributes and wrapper g which I'll discuss below.

I achieved a faux 3D effect by revolving the carousel items (I called them "slides") on an elliptical path with major and minor axes on $x$ and $y$ respectively.

function revolveSvg ( svgGWrapper, theta ) {
    const radians = theta * (Math.PI / 180);
    let xTrans = carousel.SEMI_MAJOR_AXIS * Math.cos( radians ); 
    let yTrans = carousel.SEMI_MINOR_AXIS * Math.sin( radians ); 
    let scaleFactor = 0.25 + 0.15 * ( 1 + Math.sin( radians ) );
    svgGWrapper.setAttribute( 
        "transform", 
        `translate( ${xTrans} ${yTrans} ) scale( ${ scaleFactor } )` 
    );
    svgGWrapper.setAttribute( "data-current-rotation", theta );
    svgGWrapper.setAttribute( "data-depth", yTrans );
}

To complete the effect I used a simple heuristic -- scaling down the svg as a function of "depth" (really just $y$) to make them appear smaller and more distant the higher they are in the parent viewport.

And You Thought it'd be that Easy ... gotcha!

And now for the gotchas. Ever the optimist, when I conceived this demo I figured I could hammer it out before lunch. Sadly -- the devil's in the details and I got caught by a couple of "gotchas" that dragged out the timeline a bit.

SVG Stacking

SVG elements are declared hierarchically and displayed in the order they're declared. In cases of overlap that means that elements declared subsequent to earlier elements in document order will hide portions of those earlier elements where they overlap.

For example if I have:

<svg id="carousel" 
     width="550" 
     height="412.5" 
     viewBox="-300 -100 800 400">

    <g ...>
       <svg id="svg_slide_1" ... >
           ...
       </svg>
     </g>

    <g ...>
       <svg id="svg_slide_2" ... >
           ...
       </svg>
    </g>

    ...

</svg>

And svg_slide_2 overlaps svg_slide_1 then svg_slide_2 will hide the portion of svg_slide_1 with which it overlaps. The problem with the carousel is that as it revolves items have to overlap correctly to to give that 2.5D effect. I must confess, the 'gotcha' for me was I didn't initially visualize that in my mind's eye when I conceived the design.

At this point if you're versed in CSS you might be thinking "well just set the z-index". 'Problem is, SVG doesn't have the concept of a z-index. And for this demo I was after a pure SVG solution. So I opted for an alternative approach, namely:

  1. Periodically compute the stacking order based a "distance heuristic", and

  2. Sort and shuffle the nested SVG's as needed.

const sortSlides = ( a, b ) => {
    const aDepth = a.getAttribute( "data-depth" );
    const bDepth = b.getAttribute( "data-depth" );
    return ( aDepth - bDepth ) ;
};

/**
 * Restack the slides to INSURE THAT OVERLAPPING
 * ELEMENTS ARE DISPLAYED CORRECTLY TO PRESERVE THE
 * ILLUSION OF DEPTH...
 */
const reStackSlides = () => {
    let stack = Array.from(
        carouselGroup.querySelectorAll('.svg-slide-wrapper')
    );
    stack.sort( sortSlides );
    carouselGroup.innerHTML = "";
    for( let slide of stack ) {
        carouselGroup.append( slide );
    }
 }

If you take a moment to look at the listing you'll notice I store the depth information as an attribute on the SVG XML. I've been a fan of utilizing data- attributes since they were introduced in HTML5 and I find them particularly useful working with SVG.

Pro Tip

Use data- attributes to store small bits of info if you need to work on SVG DOM elements.

Performance

Now if you're worried about performance at this point (I know I certainly was) your fear wouldn't be misplaced. It seems like a lot of DOM manipulation going on now. But, again -- nested <svg>'s to the rescue. The nested SVG DOM's don't have to be re-parsed and computed on the shuffle and the re-indexing didn't have much impact. Even with non-trivially complex SVG's like the biome animation 2 the carousel has no problem hitting the 60FPS target I usually aim for. And that's on my aging linux box. On my Samsung phone the carousel does 130 FPS or better.

Browser Considerations

The second thing that got me has to do with browser differences. I originally hammered out the carousel iteratively testing in Firefox. Over the course of doing so I applied the carousel transforms directly on the nested <svg>'s and everything worked as I expected. Buuut when I went and tested on Chrome at the end of the day I had a bit of a surprise. Chrome was not applying the transformations as expected to the nested <svg>s. Here's the story ...

The way transforms like translate, rotate etc. work in SVG is they are applied to an element and inherited by it's children such that as you work your way down the SVG DOM you get a cumulative effect 3 . But sadly in SVG 1.1 the transform attribute was not explicitly permitted on <svg>. That changed in SVG 2.

All that being said it looks like there are still browser differences with respect to compliance with the spec (not something I expected in 2025) and Chrome did not apply the transformations as expected.

The good news is there's a straight forward work around: *wrap the nested SVG's in a <g> element if you need to apply transformations. The enclosing <g>'s will be in the containing <svg>'s scope and Chrome respects that and applies them to the nested <svg>'s.

The bottom line is that's why we still need to test in every target browser and device.

Conclusion

So that brings us to the conclusion of this post. I sincerely hope you've found it educational and useful. But most of all, I hope you come away from this inspired to continue exploring SVG and all that it provides toward enabling the creation of beautiful artworks.

Endnotes

  1. I've written in greater depth about viewports -- specifically with regard to here for those interested more details .

  2. Special thanks to Tyhond (an SVG Artist's Collaborative member) for permission to use his biome for this article .

  3. For more details on deconvolving SVG transformations using the matrix visit... .

Resources

  1. Defining an SVG document fragment: the svg element

  2. Understanding SVG Coordinate Systems and Transformations (Part 3) -- Establishing New Viewports

  3. Stack Overflow: Why nest an <svg> element inside another <svg> element?