Here's an input. You can tell because it's got a border.
This border is just a solid grey colour: hsl(0 0 75)
. I think it looks pretty nice as is, but there are some situations where it will struggle.
For example on a gradient background:
Now the grey border looks muddy and hard to see. That light grey doesn't stand out from the background at all. We'd need to make the border darker to be properly visible here. Let's make it 25% darker with hsl(0 0 50%)
and see how that looks:
This is better, but it's going to be annoying to manually set the border colour of our input every time we use it on a coloured background. It would be nice if we could make one version of the border that worked on any background.
Luckily we can, by using a semi-transparent border. This will allow some of the background colour to show through, which will make the border naturally appear darker on darker backgrounds. It will also slightly tint the border colour so it matches better.
Let's try setting the border to a transparent grey like hsl(0 0 0 / 0.25)
. This is pure black but at 25% opacity, so it should be equivalent to our 75% lightness original grey colour.
It looks pretty much identical on a white background. Let's put the original back in so we can compare:
Now let's see if it looks better on our gradient background:
That's weird, it hasn't helped at all! It turns out that by default the background of an element extends up to the edge of its "border box".
That means the white background of the input is "underneath" the transparent border. This isn't what we want—for this trick to work we need the gradient background to show through the border.
Luckily there's a CSS property to control where an element's background extends to: background-clip
. By default this is set to border-box
, but we can change it to padding-box
to tell the browser to stop drawing the input's background at the edge of the padding, before the border starts. Let's see how that looks:
How much better does that look? Our input will stay distinct from the background no matter what.
There is another edge-case that we need to account for: shadows. Let's see how our inputs look with a nice shadow under them.
That's annoying. Our borders blend into the shadow and disappear at the bottom. This makes the inputs look kind of weird and bad again.
I was initially confused about why the input with the transparent border looks the same here. Surely it should work the same as with the gradient background? Unfortunately it turns out that CSS box-shadows always start being drawn after the border-box of an element. As far as I can tell there's no way to make the shadow start after the padding-box (underneath the border).
Here's a visualisation of the problem:
The border colour is transparent black, so the gradient background shows through and tints it green. However the red box-shadow doesn't show through—it starts after the border-box, and as far as I can tell there's no way to change this.
In order to work around this we have to stop using borders for our border. We need something that will render outside the element, so it sits on top of the box-shadow. Our two options are either outline
, or more box-shadow
.
Outline is by far the easiest to work with, since the syntax is the same as for borders. We can set outline-color: hsla(0, 0%, 0%, 0.25)
and get the same result as before:
This looks so much better with the shadow. The edge of the input is crisp, even right at the bottom where it meets the shadow. And it still looks great on the gradient background too:
The downside to using an outline as a border is you can't also have an outline. For example if you wanted a more distinct separate focus style you'd be a bit stuck here.
It's more complicated, but you can also simulate a border using box-shadow. You set zero offset and blur, and use the spread to control the thickness. For example: box-shadow: 0 0 0 1px hsl(0 0 0 / 0.25)
.
This looks good on the gradient background too:
The only downside to this approach is that it's more fiddly to add another shadow, since we need to write multiple box-shadows. For example:
input {
box-shadow:
/* The border */
0 0 0 1px hsl(0 0 0 / 0.25),
/* The actual shadow */
0 10px 15px -3px hsl(0 0 0 / 0.2),
0 4px 6px -4px hsl(0 0 0 / 0.2)
}
And finally, putting everything together, let's see how it looks on the gradient background with a shadow:
I love it! Some might argue that this is a lot of effort to go to for a pretty subtle enhancement, but I think that sweating the small UI details is what makes really great products stand out. You only have to write these styles once, but every time a user sees them they pay off.
One final bonus tip: if you're writing Tailwind this is as simple as switching from the border
class to ring
.