Pseudo buttons

This week I came across an interesting little detail when using :active pseudo classes on <button> elements. I can't remember if I already knew this or read about this at some point, but it led me down another tiny rabbit hole.

Let's say you want to make a simple button, with an active state, which makes it look like it's pressed:

<button>A simple button</button>

<style>
	button:active {
		translate: 0 1px;
	}
</style>

(Now you know why I needed live code snippets.)

All good so far. There are different techniques to do this of course. CSS-Tricks uses position: relative; top: 1px; as an example in their almanac.

Now let's try it out with a little scaling effect. And some feedback that we actually clicked the button:

<button>A simple button</button> <span></span>

<style>
	button:active {
		scale: .99;
	}
</style>

<script>
	document.querySelector('button').addEventListener('click', function() {
		this.nextElementSibling.textContent = this.nextElementSibling.textContent == 'Clicked!' ? 'Again!' : 'Clicked!';
	});
</script>

Nice!

But here's the thing if you exaggerate the effect. Try clicking it now:

<button>A simple button</button> <span></span>

<style>
	button:active {
		scale: .2;
	}
</style>

<script>
	document.querySelector('button').addEventListener('click', function() {
		this.nextElementSibling.textContent = this.nextElementSibling.textContent == 'Clicked!' ? 'Again!' : 'Clicked!';
	});
</script>

Thank goodness we still have keyboards, right?

Here's a little video of what happened on my blog when I played with this:

Luckily it's not just me. One of my all-time heroes, Marcin Wichary, has the same 1-pixel issue on his wonderful new blog, Unsung:

Fourteen years ago I saw him recover from a literal laptop crash in the smoothest way possible, and I still get watery eyes when I think about that talk. Did I already mention he's one of my heroes? Anyway, I digress.

One of the solutions for this problem could be to use a sort of pseudo button, like this:

Which then works like this:

Or, in code, (ab)using a pseudo element to invert the scaled state and still cover the whole hit area:

<button>A simple button</button> <span></span>

<label><input type="checkbox">Show pseudo button</label>

<style>
	button:active {
		scale: .5;
		&::after {
			content: '';
			position: absolute;
			inset: 0;
			scale: 2;
		}
	}

	/* For debugging */
	html:has([type="checkbox"]:checked) button:active::after {
		background: rgb(0 255 0 / .4);
	}
</style>

<script>
	document.querySelector('button').addEventListener('click', function() {
		this.nextElementSibling.textContent = this.nextElementSibling.textContent == 'Clicked!' ? 'Again!' : 'Clicked!';
	});
</script>

(I'm skipping over the default button padding, border and stacking context details, but you'll get the idea.)

Over here I went through all this trouble just for one pixel. Marcin, amongst others, taught me to pay attention to details, so I blame him:

<button>A simple button</button> <span></span>

<label><input type="checkbox">Show pseudo button</label>

<style>
	button {
		padding: .5em;
		border: 1px solid;
		border-radius: .3em;
		cursor: pointer;
		&:active {
			translate: 0 1px;
			&::after {
				content: '';
				position: absolute;
				inset: -1px;
				border-radius: inherit;
				translate: 0 -1px;
			}
		}
	}

	/* For debugging */
	html:has([type="checkbox"]:checked) button:active::after {
		background: rgb(0 255 0 / .4);
	}
</style>

<script>
	document.querySelector('button').addEventListener('click', function() {
		this.nextElementSibling.textContent = this.nextElementSibling.textContent == 'Clicked!' ? 'Again!' : 'Clicked!';
	});
</script>

While working on this, I remembered another invisible detail I implemented in a side project years ago. Hakim El Hattab (I have many heroes) showed an SVG trick during CSS Day 2019, while building better interfaces, and I just had to make that with CSS back then.

Done using three carefully placed pseudo elements. They're lovely. Go use them more to make your interfaces slightly better!

Gotta run now, because one of my daughters got inspired by my live preview thingy from yesterday. So we need to get an HTML book at the library. Here's another inspiring video, that's maybe related to all this.

Add your comment

Code snippets and live previews

Playing with text-fit