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
With the slider at its default of 250:
Expanding the ViewBox
The base viewBox is 75 75 500 300. Multiply width and height
by the scale factor:
Centering the Cat
Shift the origin back by half the growth so the cat stays centered:
So the new viewBox becomes -300 -150 1250 750.
Background Size
Divide by 5 to get the CSS pixel size of each tile:
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.
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.