CSS IS AWESOME

Scroll

A dive into CSS awesome features

With this website, I wanted to highlight my favorite things we can do with CSS today. The list is not finished - there is a lot of amazing things we can do with CSS nowadays and in the future - but I think it's a good start. Happy reading !

Some of the functionalities that I'm about to show are newish, so if a section of the website doesn't work as intended, it's can be fixed by simply upgrading your browser.

Skip to :

Relative units

Relative units are one of the main powerful features of CSS : thanks to them, we can easily display responsive and accessible layouts. Let's review the most used and interesting relative units.

EM

An em is equal to the font size of an element or the font size of that element’s nearest ancestor.

For example, we know that 1em refers to the font size. So if the parent's font-size value of an element is 16px, 1em equals 16px for that element. So far, so good. But if the element itself declare a font-size, like 12px, then 1em equals 12px.

/* big purple box */
.parent {
    font-size: 20px;
}

/* medium pink box */
.child-one {
    /* the child hasn't declared a font size, 
        so 1em = parent's font size */
    width: 2em; 
    /* ↑ 2 x 20px = 40px */
    height: 2em; 
    /* ↑ 2 x 20px = 40px */
}

/* small green box */
.child-two {
    font-size: 0.5em; 
    /* ↑ 0.5 x 20px = 10px */
    /* the child has declared a font size, 
        so 1em = element's font size */
    width: 2em; 
    /* ↑ 2 x 10px = 20px */
    height: 2em; 
    /* ↑ 2 x 10px = 20px */
}
                

When should we use em ?

As you can see with the example above, the em unit can be tricky. It's best to use em for typography and spacing on textual components, like a button or input fields.

REM

The unit rem stands for root em.

By "root", it means that 1rem equals the font size of the HTML element (which for most browsers has a default value of 16px).

/* big purple box */
.parent {
    width: 12.5rem;
    height: 12.5rem;
}

/* medium pink box */
.child-one {
    width: 2.5rem;
    height: 2.5rem;
    margin-right: 0.5rem;
}

/* small green box */
.child-two {
    width: 1.125rem;
    height: 1.125rem;
}
                

When should we use rem ?

Unlike the em unit, the rem unit is global and constant. It means that its value will not change, no matter how deeply the element is nested.

For that reason, it's best to use rem for properties that will allow elements to keep the same proportion on every viewports : font size, spacing, box size, etc.

VH, VW, VMIN & VMAX

The units vh, vw vmin and vmax are viewport units. It means that they are relative to the browser window or the device screen size.

In short, 1vh = 1% of the viewport height, whereas 1vw = 1% of the viewport width.

With the definition of these two units, we can easily guess the role of the other two : the viewport minimum vmin will return the smaller value of vh or vw, and the viewport maximum vmax will return the larger value of vh or vw.

In summary, 1vmin = 1% of the smaller value between 1vw or 1vh, whereas 1vmax = 1% of the larger value between 1vw or 1vh.

/* 💡 the boxes will update their sizes 
        as you resize your browser window  */

/* big purple box */
.parent {
    width: 19vw;
    height: 19vw;
    /* ↑ we use vw (and not vh), because we want a square */
    /* and the height and the width of a viewport are always different */
}

/* medium pink box */
.child-one {
    width: 4vw;
    height: 4vw;
}

/* small green box */
.child-two {
    width: 2vmax;
    /* ↑ In short, it will chose 2vw on desktop and 2vh on mobile */
    height: 2vmax;
}
                

When should we use viewport units ?

Viewport units are useful for creating elements that will occupy a constant portion of the screen. For example, it can be useful for displaying a full-height homepage (with 100vh), or to ensure that a certain element stays visible on the screen.

Viewport units can also be combined with the calc() function, to allow, for example, font size to scale in response to a change in the window size.

Overall, viewport units are ideal for creating proportionate and responsive elements.

CH

The ch unit is less used and known than the aforementioned units, but could be just as much useful. It represents the width of the "0" character of the selected font.

So 1ch equals roughly one character. How can we use this information, then ? Well, we can use the ch unit to set a max-width or a min-width, for readability and consistency.

This is a very long text that uses the ch unit to set a max-width to its box container. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vestibulum at tempus lacus. Pellentesque eget sollicitudin urna. Quisque ultrices vitae quam vel imperdiet.
/* purple box */
.element {
    max-width: 50ch;
}

/* green buttons */
.button {
    min-width: 20ch;
}
                

When should we use the ch unit ?

The ch unit comes in handy for setting a max-width or a min-width. Used with paragraphs and articles, it improves readability : the optimal line length for a body text is believed to be between 50 and 75 characters. Like the above examples, the ch unit can also be used to set a min-width to buttons or navigation items.

Relative units and media queries

With what we learn so far, we can use the relative units to minimize the use of media queries :

:root {
  /* ensures the font-size never drops below the default 
      (system/browser/user defined) value */
  font-size: calc(1rem + 0.5vw);
}
  
/* media queries in pixels */
@media (min-width 960px) {
    :root {
        /* ↓ Upscale by 25% at 960px */
        font-size: 125%;
    }
}

h2 {
    /* ↓ 2.5 × the root font-size */
    font-size: 2.5rem;
}

h2 strong {
    /* ↓ now 1.125 × 2.5rem */
    font-size: 1.125em;
}

.big-responsive-button {
    /* ↓ clamp(min-value, preferred value, max-value) */
    font-size: clamp(1.5rem, 3vw, 2.5rem);
    /* ↓ responsive properties (will adapt to button's font size) */
    padding: 1.5em;
    border-radius: 1.5em;
}

article, p {
    /* ↓ better readability */
    max-width: 60ch;
}

Relative units : further resources

Variables

Variables are powerful sidekicks. It gives us the power of sharing and customize reusable code and the ability the ability to adapt the user interface to user interactions.

How does variables works ?

CSS variables are custom properties. We declare a variable with the -- prefix, for example --my-variable, and we get its value by calling the var() function, like this : var(--my-variable, fallback value). We use variables for creating color palettes, for calculations, for displaying multiple themes and so much more.

Examples

Color palettes with HSL

Main color
Constrast color
Dark color

Below is the code I use for the colors of this website.

:root {
    /* Here are the global variables, 
        shared with all elements of the document /*
    --hue: 336;
    /* we declare a main color, a number between 0 and 360 /*
    ...
}

:root[theme="dark"] {
    /* we then use the variable in the HSL representation */
    --main-color: hsl(var(--hue), 100%, 50%);
    /* HSL stands for --hue, --saturation and --lightness */
    --constrast-color: hsl(var(--hue), 60%, 40%);
    /* we can change the saturation or the lightness
        to create different shades */
    --dark-color: hsl(var(--hue), 40%, 10%);
    ...
}

.section button {
    background-color: var(--constrast-color, black);
    /* ↑ 💡 we use a fallback value, in this example "black" */
    ...
}
            

Light theme / Dark theme

Below is the code I use for the example and for this website.

:root {
    --hue: 336; /* main color, 336 by default */
    /* 💡 the value changes when the user chooses another color */
    ...
}

:root[theme="light"] {
    --background-color: hsl(var(--hue), 35%, 82%);
    /* ↑ 💡 backgroud is light */
    --text-color: hsl(var(--hue), 90%, 10%);
    /* ↑ 💡 text is dark */
    ...
}

:root[theme="dark"] {
    --background-color: hsl(var(--hue), 20%, 20%);
    /* ↑ 💡 background is dark */
    --text-color: hsl(var(--hue), 20%, 80%);
    /* ↑ 💡 text is light */
    ...
}

p {
    color: var(--text-color);
    /* ↑ 💡 will change on user interaction */
    ...
}

section {
    background-color: var(--background-color, lightgrey);
    /* ↑ 💡 we use a fallback value, in this example "lightgrey" */
}
            

Inheritance

By default, variables are inherited by the children of the element that declared them.

.parent {
    --background-color: red;
    /* local variable (not in :root), inherited by children */
}

.child-one {
    background-color: var(--background-color, grey);
    /* by default, background-color: red */
}

.child-two {
    --background-color: green;
    /* local variable, override the parent variable */
    background-color: var(--background-color, grey);
    /* background-color: green */
}
            

Inline style and variables

We can also directly declare variables in HTML, with the inline style. Awesome, right ?

/* I want to add more margin-top to this subtitle */
/* I pass the new margin-top with the inline style */
<h2 style="--margin-top: 5rem;"
    class="subtitle">
    Awesome Subtitle
</h2>

.subtitle {
    --margin-top: 2.5rem;
    /* the margin-top by default */
    /* is overrided by the new value declared in the inline style */
    margin: var(--margin-top, 2.5rem) 0 1rem 0;
    /* so, for the title named "Awesome Subtitle",
        margin: 5rem 0 1rem 0; */
}
            

Variables : further resources

Navigation

Scroll, interactions, accessibility : modern CSS can also enhance user navigation.

Scroll snap

CSS Scroll snap gives us the power to control how elements of a container will respond to scroll. It's particularly useful with touch devices, to create a seamless scrolling experience with items that can be swiped.

Vertical scroll-snap

Scroll in the container box to see how the scrolling stops perfectly with one item fixed at the top of the container !

  • Item 1
  • Item 2
  • Item 3
  • Item 4
  • Item 5
  • Item 6
.container {
    scroll-snap-type: y proximity;
    /* y = horizontal scroll */
    /* proximity = if the user is in a range of proximity
    (a few hundred pixels of a snap point),
    the visual viewport will rest on the next snap point */
    height: 20rem;
    overflow: auto;
}

.items {
    scroll-snap-align: start;
    /* will be at the top (start) of the container 
        when the scrolling stops */
    height: 10rem;
    display: flex;
    justify-content: center;
    align-items: center;
}
                

Horizontal scroll-snap

Swipe the items in the container box to see how the scrolling stops perfectly with one item fixed at the center of the container !

  • Item 1
  • Item 2
  • Item 3
  • Item 4
  • Item 5
  • Item 6
.container {
    scroll-snap-type: x mandatory;
    /* x = vertical scroll */
    /* mandatory = one scroll and the visual viewport will rest
        on the next snap point */
    display: flex;
    gap: 1rem;
    align-items: center;
    height: 12rem;
    max-width: 100%;
    overflow-x: auto;
}

.items {
    scroll-snap-align: center;
    /* will be at the center of the container 
        when the scrolling stops */
    height: 10rem;
    width: 50ch;
    display: flex;
    justify-content: center;
    align-items: center;
    flex: 0 0 35ch;
}
                

Scroll-snap : further resources

Layout

Modern CSS gives us tools to easily display responsive and customized layouts, without the help of CSS libraries. Flexbox and Grid are the most powerful ones.

GRID

CSS Grid Layout is a tool released in 2017. It's a two-dimensional grid-based layout system and was created to solve a common problem : how to control every region of a page, without the need for "hacking" solutions.

I won't dive into every feature of the CSS Grid Layout, but I will show you some examples, along with comments.

If you want to learn the basics of CSS Grid Layout, I highly recommend this tutorial : Learn CSS Grid - A Guide to Learning CSS Grid

CSS Grid Basics

1 justify-self: start;
align-self: start;
2 justify-self: center;
align-self: center;
3 justify-self: end;
align-self: end;
4 justify-self: stretch;
grid-column-start: 2;
5 new item, without properties
6 The items of this row can't be wider than the half of the container height. The items of this row can't be wider than the half of the container height.
/* white container */
#grid-container {
    display: grid;
    grid-template-columns: repeat(3, 1fr);
    /* 💡 ↑ defines width of columns */
    /* 1fr(action) 1fr 1fr => 3 columns of 33% */
    grid-template-rows: 8rem auto minmax(auto, 50%);
    /* 💡 ↑ defines height of rows */
    /* 3 different-sized rows */
    gap: 0.5rem;
    /* 💡 ↑ gutters */
}

.grid-items-one:nth-child(1) {
    justify-self: start;
    align-self: start;
}

.grid-items-one:nth-child(2) {
    justify-self: center;
    align-self: center;
}

.grid-items-one:nth-child(3) {
    justify-self: end;
    align-self: end;
}

.grid-items-one:nth-child(4) {
    justify-self: stretch;
    grid-column-start 2;
}
            

CSS Grid Responsive

Resize the grid to see the changes in action !

1 I'm an only child. Give me all the space I need, I deserve it
2 We are two children. Give us equal space. When it's too narrow, give us all the space, separately
2 We are two children. Give us equal space. When it's too narrow, give us all the space, separately
3 We are three children. Give us equal space, but don't forget that we need a minimum space. When it's too narrow, give us all the space, separately
3 We are three children. Give us equal space, but don't forget that we need a minimum space. When it's too narrow, give us all the space, separately
3 I'm the third child. On some occasions, I'm left alone, and, on top of that, I can't take advantage of all the available space 😥
4 We are four children. Give us equal space, but don't forget that we need a minimum space. When it's too narrow, give us all the space, separately
4 We are four children. Give us equal space, but don't forget that we need a minimum space. When it's too narrow, give us all the space, separately
4 We are four children. Give us equal space, but don't forget that we need a minimum space. When it's too narrow, give us all the space, separately
4 I'm the fourth child. On some occasions, I'm left alone, and, on top of that, I can't take advantage of all the available space 😥
/* white container */
#grid-container {
    --minColWidth: 30ch;
    /* ↑ minimum size of an element of a grid */
    resize: horizontal;
    overflow: scroll;
    width: 100%;
}

/* the four grids */
.grids {
    display: grid;
    grid-template-columns: repeat(auto-fit, 
        minmax(var(--minColWidth), 1fr));
    gap: 0.5rem;
    margin-bottom: 0.5rem;
}
            

CSS Grid Positioning and Spanning

1 order: 0; (by default)
2 order: 1;
grid-column: 2 / span 2;
3 grid-column: 1;
grid-row: 2 / 4;
4 grid-column: 3;
grid-row: 2;
5 grid-area: 1 / 1 / 2 / 3;
6
7 order: 2;
grid-column: span 3;
/* white container */
#grid-container {
    display: grid;
    grid-template-columns: repeat(3, 1fr);
    width: 100%;
}

.grid-items:nth-child(2) {
    order: 1;
    grid-column: 2 / span 2;
}

.grid-items:nth-child(3) {
    grid-column: 1;
    grid-row: 2 / 4;
}

.grid-items:nth-child(4) {
    grid-column: 3;
    grid-row: 2;
}

.grid-items:nth-child(5) {
    grid-area: 1 / 1 / 2 / 3;
}

.grid-items:nth-child(7) {
    order: 2;
    grid-column: span 3;
}
            

CSS Grid Area (with Media Queries)

HEADER grid-area: header;
MAIN grid-area: main;
SIDEBAR grid-area: sidebar;
4 grid-area: 3 / 2;
5 z-index: 1;
grid-column: footer-start / span 2;
grid-row-start: footer-start;
FOOTER
grid-area: footer;
/* for simplicity, we start with the minimum size first */
/* that way, we don't have to, for bigger sizes and some properties,
/* reset the value to "initial" */
/* we set the basics of the grid and then 
   we add extra features on other viewports */
#grid-container {
    display: grid;
    grid-template-areas: 
    "header"
    "main"
    "sidebar"
    "."
    "."
    "footer";
    gap: 0.5rem;
    width: 100%;
}

.grid-items:first-child {
    grid-area: header;
}

.grid-items:nth-child(2) {
    grid-area: main;
}

.grid-items:nth-child(3) {
    grid-area: sidebar;
}

.grid-items:nth-child(4) {
    grid-row-start: sidebar-end;
    grid-column: 1 / 1;
}

.grid-items:nth-child(5) {
    z-index: 1;
    grid-row-end: footer-start;
    grid-column: 1 / 1;
}

.grid-items:last-child {
    grid-area: footer;
}

/* tablet layout */

@media (min-width: 768px) {
    #grid-container {
        grid-template-columns: auto 30%;
        /* 30% of the container for the sidebar */
        grid-template-rows: 1fr;
        grid-template-areas: 
            "header header"
            "main sidebar"
            "main sidebar"
            ".  ."
            "footer footer"
            "footer footer"
            "footer footer";
    }

    .grid-items:nth-child(4) {
        grid-row-start: main-end;
        grid-column: 1 / span 2;
    }

    .grid-items:nth-child(5) {
        z-index: 1;
        grid-column: footer-start / span 1;
        grid-row: 7 / span 1;
    }
}

/* desktop layout */

@media (min-width: 1200px) {
    #grid-container {
        grid-template-columns: repeat(4, 1fr);
        grid-template-rows: repeat(4, 1fr);
        grid-template-areas: 
            "header header header header"
            "main main . sidebar"
            "main main . sidebar"
            "footer footer footer footer";
    }

    .grid-items:nth-child(4) {
        grid-area: 3 / 2;
    }

    .grid-items:nth-child(5) {
        z-index: 1;
        grid-column: footer-start / span 2;
        grid-row-start: footer-start;
    }
}
            

CSS Grid : further resources

Shape Outside

The shape-outside property defines a shape (circle, ellipse, polygon, rectangle) for an element, and therefore how content will wrap around that element.

Lorem ipsum dolor sit amet consectetur, adipisicing elit. Consequatur autem quae sapiente? Nulla libero reprehenderit praesentium aut amet expedita voluptas exercitationem dolore minus ut. Itaque tempora quam vel unde laboriosam.

Lorem ipsum dolor sit amet consectetur, adipisicing elit. Consequatur autem quae sapiente? Nulla libero reprehenderit praesentium aut amet expedita voluptas exercitationem dolore minus ut. Itaque tempora quam vel unde laboriosam.

Lorem ipsum dolor sit amet consectetur, adipisicing elit. Consequatur autem quae sapiente? Nulla libero reprehenderit praesentium aut amet expedita voluptas exercitationem dolore minus ut. Itaque tempora quam vel unde laboriosam.

.shape-outside-element {
    width: 8rem;
    aspect-ratio: 1;
    background-color: var(--illustration-pink, --pre-pink);
    float: left; /* mandatory */
    margin-right: 2rem;
}

/* circle shape */
.shape-outside-element:first-child {
    border-radius: 50%;
    shape-outside: circle(50%);
}

/* diamond shape */
.shape-outside-element:nth-of-type(2) {
    shape-outside: polygon(50% 0, 100% 50%, 50% 100%, 0 50%);
    /* the property only changes the relationship of adjacent elements,
    not the geometry of the element itself */
    /* to change the geometry of the element we use the clip-path property,
    with the same values */
    clip-path: polygon(50% 0, 100% 50%, 50% 100%, 0 50%);
}

/* ellipse shape */
.shape-outside-element:nth-of-type(3) {
    shape-outside: ellipse(35% 50% at 50% 50%);
    clip-path: ellipse(35% 50% at 50% 50%);
}
                

Shape-outside : further resources

Drop Shadow

Like the box-shadow property, the drop-shadow function applies a shadow around an element. But, unlike the former, the shadow is applied to the shape of that element, not to its container box. The drop-shadow function is used with the filter property.

Drop-shadow circle
Box-shadow circle
Drop-shadow triangle (using gradient)
Box-shadow triangle (using gradient)
Drop-shadow cross (using clip-path)
Box-shadow cross (using clip-path)
.drop-shadow-element {
    width: 8rem;
    aspect-ratio: 1;
    background-color: var(--illustration-green, --pre-green);
    /* with drop-shadow, we use the filter property */
    filter: drop-shadow(1rem -0.25rem 0.5rem var(--shadow-color));
}

.box-shadow-element {
    width: 8rem;
    aspect-ratio: 1;
    background-color: var(--illustration-green, --pre-green);
    /* the box-shadow is applied to the box container */
    box-shadow: 1rem -0.25rem 0.5rem var(--shadow-color);
}

/* circle shape */
.drop-shadow-circle,
.box-shadow-circle {
    border-radius: 50%;
}

/* triangle shape (using gradient) */
.drop-shadow-triangle,
.box-shadow-triangle {
    background-color: initial;
    background-image: linear-gradient(45deg,
    var(--illustration-green, --pre-green) 50%,
    rgba(255, 255, 255, 0) 50%);
}

/* cross shape (using clip-path) */
.drop-shadow-cross,
.box-shadow-cross {
    display: flex;
    flex-direction: column;
    width: 8rem;
    aspect-ratio: 1;
    background-color: var(--illustration-green, --pre-green);
    /* With clipped element, we lose the shadow */
    clip-path: polygon(20% 0%, 0% 20%, 30% 50%, 0% 80%, 20%
    100%, 50% 70%, 80% 100%, 100% 80%, 70% 50%, 100% 20%, 80% 0%, 50% 30%);
}

.drop-shadow-container {
    /* Solution : the drop-shadow filter is applied on the parent element */
    filter: drop-shadow(1rem -0.25rem 0.5rem var(--shadow-color));
}
                

Drop-shadow : further resources