Cat Background Slider

Published on March 1st, 2026

If you have been on this site before, you may have noticed the little cat icon in the footer. All my life this cat has been my signature character that I have drawn consistently. Clicking it toggles a tiled cat pattern across the entire page background, and a slider appears to control the spacing between the cats. Here is how I built it.

Loading the SVG

The cat SVG is imported as a raw string using the ?raw suffix. This gives us access to the full SVG markup at build time so we can manipulate it with JavaScript at runtime. The raw string is also stored in a data-svg attribute so the client-side script can access it.

---
import CatSVGFile from '../svgs/cat.svg?raw'
---

<div id="cat-svg" data-svg={CatSVGFile}>
  <Fragment set:html={CatSVGFile} />
</div>

I use Fragment with set:html to render the SVG inline for the clickable cat icon. The data-svg attribute holds the same raw string that the JavaScript reads later to build the background.

Encoding for CSS

To use an SVG as a CSS background-image, it needs to be encoded as a data URI. Rather than base64, I use encodeURIComponent which keeps the string readable and is perfectly valid for inline SVGs.

function encodeSVG(svg: string): string {
  return `data:image/svg+xml,${encodeURIComponent(svg)}`
}

The ViewBox Trick

This is the core of how the spacing works. Instead of changing the background tile size or moving elements around, I expand the SVG's viewBox. A larger viewBox means more transparent space around the cat, and since the browser tiles the background, that transparent space becomes the gap between cats.

const BASE_WIDTH = 500
const BASE_HEIGHT = 300
const BASE_X = 75
const BASE_Y = 75

function updateSVGViewBox(spacing: number): void {
  // Scale factor: slider 100 = 1x, slider 600 = 6x
  const scaleFactor = 1 + (spacing - 100) / 100

  // Expand viewBox to add transparent padding around cat
  const newWidth = BASE_WIDTH * scaleFactor
  const newHeight = BASE_HEIGHT * scaleFactor

  // Center the cat in the expanded viewBox
  const newX = BASE_X - (newWidth - BASE_WIDTH) / 2
  const newY = BASE_Y - (newHeight - BASE_HEIGHT) / 2

  const updatedViewBox = `${newX} ${newY} ${newWidth} ${newHeight}`

  const updatedSVG = rawSVG.replace(
    /<svg[^>]+>/,
    `<svg xmlns="http://www.w3.org/2000/svg"
      viewBox="${updatedViewBox}" opacity="0.2">`
  )

  body.style.backgroundImage = `url('${encodeSVG(updatedSVG)}')`
  body.style.backgroundSize = `${newWidth / 5}px ${newHeight / 5}px`
}

The key insight is that background-size scales proportionally with the viewBox. The formula newWidth / 5 keeps each cat at a constant visual size. As the slider increases, the tile grows but the cat inside it stays the same size, so you only see more space between them.

The Math

The slider value (100–600) maps to a scale factor. At 100, the viewBox is the original size. At 600, it is 6× larger.

Scale Factor

s=1+slider−100100s = 1 + \frac{\text{slider} - 100}{100}

With the slider at its default of 250:

s=1+250−100100=2.5s = 1 + \frac{250 - 100}{100} = 2.5

Expanding the ViewBox

The base viewBox is 75 75 500 300. Multiply width and height by the scale factor:

newWidth=500×2.5=1250\text{newWidth} = 500 \times 2.5 = 1250

newHeight=300×2.5=750\text{newHeight} = 300 \times 2.5 = 750

Centering the Cat

Shift the origin back by half the growth so the cat stays centered:

x=75−1250−5002=−300x = 75 - \frac{1250 - 500}{2} = -300

y=75−750−3002=−150y = 75 - \frac{750 - 300}{2} = -150

So the new viewBox becomes -300 -150 1250 750.

Background Size

Divide by 5 to get the CSS pixel size of each tile:

bgSize=12505×7505=250×150 px\text{bgSize} = \frac{1250}{5} \times \frac{750}{5} = 250 \times 150 \text{ px}

The cat stays the same visual size inside a bigger tile, which is why you see more space between the cats.

Try It

Here is a live demo. The dashed grid shows the tile boundaries. Drag the slider to see how the tile grows while the cat stays the same size.

250

The Slider

The range input drives the spacing value from 100 to 600. On each input event, it recalculates the viewBox and updates the background. The slider value is also persisted to localStorage so it sticks across page loads.

<input id="spacing-slider" type="range"
  min="100" max="600" value="250" />

<script>
  slider.addEventListener('input', (event) => {
    const target = event.target as HTMLInputElement
    spacing = parseInt(target.value, 10)
    updateSVGViewBox(spacing)
    localStorage.setItem('backgroundSpacing',
      JSON.stringify(spacing))
  })
</script>

Persisting State

Both the toggle state and the spacing value are saved to localStorage. When the page loads, the script checks for saved values and applies them immediately. This way, if you set your preferred cat density, it is still there when you come back.

// Restore state on page load
let isBackgroundSet =
  JSON.parse(localStorage.getItem('isBackgroundSet')) || false
let spacing =
  JSON.parse(localStorage.getItem('backgroundSpacing')) || 250

// Save state on change
localStorage.setItem('isBackgroundSet',
  JSON.stringify(isBackgroundSet))
localStorage.setItem('backgroundSpacing',
  JSON.stringify(spacing))

Final Thoughts

This was a fun little feature to build. The viewBox approach turned out to be a clean way to handle tiled spacing without needing to regenerate the SVG on every slider tick. The cat stays the same size, the gaps grow, and the whole thing is just a few lines of vanilla JavaScript. Give it a try by clicking the cat in the footer.