Lesser known CSS quirks & advanced tips

Below you’ll find some of the weirdest CSS features, along with tips and tricks for advanced CSS users.

Peedu Tuisk
8 min readFeb 28, 2018


• Vertical padding is relative to element’s width not height

So padding-top: 50% does not add 50% of the original height of the element as padding, but 50% of the width of the parent element:

Knowing this we can easily make responsive elements that keep their height/width ratio:

.square {
width: 100%;
height: 0;
padding-bottom: 100%;

• Margins overlap, but only sometimes

So space between the following elements will be 20px, not 40px:

<div style="margin-bottom:20px">foo</div>
<div style="margin-top:20px">foo</div>

However, there are exceptions! The margins don’t collapse in the following circumstances:

  • floated elements
  • absolutely positioned elements
  • inline-block elements
  • elements with overflow set to anything other than visible (they do not collapse margins with their children.)
  • cleared elements (They do not collapse their top margins with their parent block’s bottom margin.)
  • the root element

• Opacity can change the z-index stacking order

Say you have 3 divs, each positioned absolutely, containing another element with increasing z-index number. Each next one will appear on top of the previous one. If you add z-index: 10 to the first one, it will now appear on top of the other two, which remain ordered as before. So far all good. Now add “opacity: 0.99” to the first div and watch it get stacked under the other two. :)

More info about why this happens: https://philipwalton.com/articles/what-no-one-told-you-about-z-index/

• CSS custom properties and variables

Having used SASS or LESS one might figure the CSS custom properties and variables to be the equivalent of features available in those preprocessors, but there are a few key differences worth looking into.

First the basics:

// you can set and use custom properties like such::root {
--foo: #000;
button {
background-color: var(--foo); //background is black

They’re also inheritable, so if you reassign a local variable it’ll have an effect on all children elements and contrary to preprocessors the browser will actually recalculate all variables and expressions where applicable when that happens.

Fallbacks can be used with commas, you can stack multiple fallbacks after comma, even other variables:

.foo {
color: var(—-my-var, 'blue');

This leads us to the main difference from preprocessors: CSS variables are aware of the DOM’s structure and are dynamic.

::root {
--default-color: #000000;
header {
--primary-color: #ff0000;
a {
color: var(--primary-color, --default-color);
<a href="">this is black</a>
<a href="">this is red.</a>

Contrary to the first example of inheritability, this example relies on fallback being aware if the custom property was set in the parent DOM element or not.

Furthermore, they can also be easily changed using javascript:

// get variable from inline style
// set variable on inline style
element.style.setProperty("--my-var", jsVar + 4);

Supported upward since Edge15.

• Vertical align: top | middle | bottom

“Vertical-align: top | middle | bottom” only works for inline (edit: including inline-block) and table-cell, it is not a legit way to align an element inside its parent the way one might intuitively believe.
Use flexbox, or what has become known as the douchebag vertical align (explained below) for that. :)

• Height: 100% may not do what you think it does

Same goes for “height: 100%”, in many cases this does not do what the developer expects, because parent element’s height is not set. So for example:

<div style=”height:100%;background:red;”></div>

This does not fill the whole page red. In order to do that, you’d need to set the height of both body and html to 100%.

• Id > class

Id styles overrule all class styles. This is due to id being more precise than a class, similarly “.foo.bar” will overrule just “.foo” or “.bar”.

#foo { color: red; } 
.bar { color: green; }
<h1 id="foo" class="bar">this will be red not green</h1>

• Attribute targeting

You can target specific attributes and their contents, like for example src or href attribute content.

// target all zip files, case insensitive
a[href$=".zip" i] { }
// make google.com links red
[href*="google.com"] { color: red; }

This may help when debugging, like displaying all image elements without or an empty “alt” attribute:

img:not([alt]) {
border: 2px dashed red;
img[alt=""] {
border: 2px dashed red;

If you’re using Angular, this technique can also be useful for targeting some elements that contain [ng-click]. Or if you wish, you can target all the links that start with “http” or “https” instead of locals.

• Horizontal / vertical axis values inconsistency

When declaring values for horizontal and vertical axis, usually first number declares vertical values and second the horizontal (ie: in “padding: 10px 20px;” the 10px is top and bottom, 20px is left and right). This is true for padding, margin, borders, etc pretty much everything.

…Except for “border-spacing” in tables, where the exact opposite is the case: first number sets horizontal and the second vertical values … o_O

• Multiple backgrounds

You can add multiple backgrounds to a single element and position them all differently as well, just separate with comma like so:

background: url(example1.png’) no-repeat center 50px, url(‘example2.jpg’) no-repeat bottom top;

This is supported upwards from IE11.

• Stacking animations

Similarly to backgrounds, you can also stack CSS animations:

@keyframes foo { 
0% { opacity: 0; }
100% { opacity: 1; }
@keyframes bar {
0% { transform: translateX(-100px); }
100% { transform: translateX(0px); }
.element {
animation: foo 2s 0s, bar 1s 0s;
Half way through the article! Kind of like GRRM is probably half way through writing the next book?

• Strange behavior of “position: fixed” with “translateZ”

Adding transform: translateZ(0); to container that includes an element with position: fixed; makes the fixed element align to the container instead of the window.

Styling of the url hash (/#foo) targeted elements

You can use :target pseudo-class to target the …ahem… targeted element. So for example clicking on <a href=”#foo”>Go to Foo</a> will scroll you to <div id=”foo”>foo</div> element on the page. Now, if you have included #foo:target { color: red; } in css, the targeted #foo div element text will now be red.
This can be useful if you wish to highlight a targeted element to people who have arrived to the page via an external hash link, like for example: www.example.com/#foo
Not only will browser scroll to the correct element, but you can also use CSS to make this targeted section more apparent visually. This is rarely done these days, but can be a very useful technique to better your user experience.

• Lesser known “content: ‘foo’;” property features

Data attributes
You can use data attributes for dynamic CSS content. For example:

<div data-text="foo"></div>div:before {
content: attr(data-text);

This can be useful, for example if you wish to translate the pseudo class content texts (e.g. when using them for tooltips). Currently attr() is supported for content property and almost no browser supports it for other properties, but perhaps they will in the future. Furthermore, the values from attr() are strings, so it really was only meant to be used for content in the first place and can’t be used for units (e.g. font-size) or urls (e.g. content: url() ). Talking of which:

Content: url()
This can be used for many types of media (image, sound, video).

<div></div>div:before {
content: url(image.jpg);

However, what can be used to pass any content from DOM to CSS, is the previously mentioned custom properties:

<div style="--background-image: url('http://via.placeholder.com/150x150');"></div>div:after {
content: '';
background-image: var(--background-image);

Incremental counter
You can use the property content: counter() feature to incrementally number the pseudo elements:

p {
counter-increment: myIndex;
p:before {

Open and close quote-marks
The content property of pseudo classes like :before and :after can be used to open and close quote-marks:

q:before {
content: open-quote;
q:after {
content: close-quote;

Talking of quotes, combined with the previously mentioned data attribute targeting, you can even use CSS to set a specific localized style of quotes based on the language of the site with just the quotes property, like so:

html[lang=”fr”] q {
Quotes: “«” “»”;

• Font is a shorthand property

“Font” is a CSS shorthand property. Therefore, instead of writing all properties, you can just combine them all under one:

h1 {
font-weight: bold;
font-style: italic;
font-size: 1rem;
// vsh1 {
font: italic lighter 1rem/150% Verdana, Helvetica, sans-serif;
// syntax
// font: font-style font-variant font-weight font-size/line-height font-family;

• @supports for checking browser support

You can use @supports feature query to check for browser support. As an example: if you wish to use “display:flex” only when it’s actually supported you can set it up like so:

@supports (display: flex) {
div {
display: flex;

• Colons in class names

Using colons in class names can be helpful for differentiating purpose. Some CSS UI frameworks (e.g. Tailwind) have taken the following naming convention into use:

<div class="justify-start sm:justify-center md:justify-end lg:justify-between xl:justify-around"><button class=”bg-blue hover:bg-blue-dark text-white hover:text-blue-light”>Button</button>

Declaring specific classes for hover styles is probably not useful in most cases, but the way this naming convention makes hover states clearly recognizable, as such, seems to provide better readability. It’s very descriptive.

In CSS, you simply need to escape the colon, like so:

.sm\:justify-center { }

• Lobotomized owl selector

Everybody reading this article should know about the lobotomized owl selector:

* + * {
margin-top: 2rem;

Useful for situations when you have multiple elements of the same kind that need some spacing, for example list or nav link items.

li + li {
margin-top: 1rem;
// vsli {
margin-bottom: 1rem;
li:last-of-type {
margin-bottom: 0;

• Douchebag vertical align

While we’re at funny sounding selectors, you might be interested in what has been named as “douchebag vertical align”:

.element {
position: relative;
top: 50%;
transform: translateY(-50%);

• Font-feature-settings property for OpenType fonts

OpenType fonts have feature settings you can use to tweak the font to your liking, using font-feature-settings property.
One specific use-case for this feature is when you wish to use a cool looking font for a countdown timer, but it doesn’t happen to be a mono-spaced font. This means the width of numbers will change and keep pushing content around. Solution:

font-feature-settings: “tnum”;
font-variant-numeric: tabular-nums;

• Truncate text with an ellipsis “…” ending

p {
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;

• Extra: wbr element

Not CSS, but here’s an interesting lesser known HTML element: <wbr>, it allows to mark a spot where the word should break.

• But wait, where’s CSS Grid?

This article is long enough, maybe in the next one. It’s not like we’re ever going to run out of quirks and oddities in CSS. :)

Thanks for reading!

Hit clap for each new tip you got. ;)

You can find me at https://twitter.com/peedutuisk