Skip to content

Instantly share code, notes, and snippets.

@jcayzac
Last active February 3, 2026 11:58
Show Gist options
  • Select an option

  • Save jcayzac/eb18b65bd34856948b077729b2f21d20 to your computer and use it in GitHub Desktop.

Select an option

Save jcayzac/eb18b65bd34856948b077729b2f21d20 to your computer and use it in GitHub Desktop.
Swap inline SVG contents in/out automatically when they enter/leave the viewport
---
/**
* @file Custom element that wraps an inline SVG and swaps its content in or
* out as the element enters or leaves the viewport, thus keeping the
* DOM lean even when a lot of big-ass SVGs are used on a single page
* (article lists with SVG heroes, for instance).
*/
import type { ImageMetadata } from 'astro'
import { getImage } from 'astro:assets'
import { experimental_AstroContainer as Container } from 'astro/container'
import { AstroError } from 'astro/errors'
import { HTML } from '/server/HTML'
interface Props {
image: ImageMetadata
alt: string
fit: 'contain' | 'cover'
}
const { image, alt, fit } = Astro.props
type Component = Parameters<typeof Container.prototype.renderToString>[0]
function isComponent(image: unknown): image is Component {
return typeof image === 'function' && 'isAstroComponentFactory' in image && image.isAstroComponentFactory === true
}
if (!isComponent(image) || image.format !== 'svg') {
const error = new AstroError(`<big-ass-svg> only works with imported SVG images.`)
error.hint = `Either use getImage(), an import directive, or the image() schema in your content layer loader.`
throw error
}
// Grab the attributes of the <svg> element
const container = await Container.create()
const result = await container.renderToString(image)
const { attributes } = HTML.fromString(result, true)
const preserveAspectRatio = `xMidYMid ${{ cover: 'slice', contain: 'meet' }[fit]}`
const { src } = image
---
<big-ass-svg {src}>
<svg {...attributes} {preserveAspectRatio} aria-label={alt} role="img" />
</big-ass-svg>
<style>
@layer components {
big-ass-svg {
display: contents;
@media (update: fast) and (not (prefers-reduced-motion)) {
svg > * {
opacity: 0;
transition: opacity .450s ease-in;
&:where(.mounted *) {
opacity: 1;
}
}
}
}
}
</style>
<script>
const parser = new DOMParser()
const io = new IntersectionObserver((entries) => {
for (const entry of entries) {
const el = entry.target.parentElement as BigAssSVG
el.viewportVisibilityChanged(entry.isIntersecting)
}
})
class BigAssSVG extends HTMLElement {
#loading: AbortController | null = null
#svg: SVGSVGElement | null = null
#src: string | null = null
get src() { return this.#src }
viewportVisibilityChanged(isVisible: boolean) {
if (isVisible) {
this.#enterViewport()
}
else {
this.#leaveViewport()
}
}
async #enterViewport() {
const { src } = this
if (!this.#loading && typeof src === 'string') {
this.#loading = new AbortController()
while (!this.#loading.signal.aborted) {
try {
const response = await fetch(src, {
priority: 'low',
signal: AbortSignal.any([
this.#loading.signal,
AbortSignal.timeout(10_000),
]),
})
if (response.ok) {
const { children } = parser.parseFromString(await response.text(), 'image/svg+xml').documentElement
const adopted = [...children].map(el => this.ownerDocument.adoptNode(el))
this.#svg?.replaceChildren(...adopted)
requestAnimationFrame(() => this.#svg?.classList.add('mounted'))
break
}
}
catch (error) {
if (error instanceof Error && error.name === 'AbortError') {
break
}
}
await new Promise(resolve => setTimeout(resolve, 1_000))
}
this.#loading = null
}
}
async #leaveViewport() {
// Abort loading if still in progress
this.#loading?.abort()
// Clear the SVG content
this.#svg?.replaceChildren()
requestAnimationFrame(() => this.#svg?.classList.remove('mounted'))
}
connectedCallback() {
this.#src = this.getAttribute('src') ?? null
this.#svg = this.querySelector('svg')
if (this.#svg) {
io.observe(this.#svg)
}
}
disconnectedCallback() {
if (this.#svg) {
io.unobserve(this.#svg)
}
}
}
customElements.define('big-ass-svg', BigAssSVG)
</script>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment