Usefull links
- Test latest CSS features browser adoption and support: https://www.lambdatest.com/web-technologies/
WARNING: Safari is the new IE. If there is a cool feature in CSS, keep your excitement down as Safari probably don't support it yet. Unfortunatelly, this useless piece of shit browser is used a lot thanks to our little friend the iPhone. Safari is a big piece of junk, so please refer to the The million things Safari does not want you to do in CSS section, so that you can find work arounds.
- CSS
- CSS syntax
- CSS selectors
- Fonts
- Specificity or how to stop using
!important
z-index
and stacking order
- 1st rule - Elements in the same stacking context will display in order of appearance
- 2nd rule - Positioned elements are always higher than unpositioned elements
- 3rd rule -
transform
puts the element in a new stacking context- 4th rule - The stacking order of a child is bounded to it's parent's stacking order
- Scrolling
- Animation
- Text
- Images
- Shadow
- Layout
- Media queries
- Masking
- CSS-in-JS
- Browsers compatibility
- Mobile challenges
:hover
on mobile- Performances
- The million things Safari does not want you to do in CSS
- Border radius broken when the container uses
overflow: hidden
- Gradient effect on text does not work
height:100%
does not work when the parent does not explicitely define its height- Mobile Safari freezes when scrolling at the top or bottom or while touching the screen as it scrolls
- Flex
gap
not supported- SCSS
- Troubleshooting
- FAQ
- How to configure the
src
attribute of theimg
tag?- How to delay effects?
- How to prefix certain CSS properties for cross-browser compatibility?
- How to make an image fit a container without being skewed?
- How to load CSS stylesheets asynchronously?
- How to find a native font similar to your custom font?
- How to have padding and border width contained inside the element's dimensions?
- Annexes
- References
Allows to import other CSS files.
@import "navigation.css";
IMPORTANT: It must be located at the top of the file
Nowadays, HTML supports adding inline CSS inside DOMs:
<div>
<style type="text/css">
</style>
</div>
/* Apply style on class 'MuiSwitch-thumb' only when one of its parent span does not contain the 'Mui-checked' class */
span:not(.Mui-checked) > .MuiSwitch-thumb {
border: 1px solid #d7d7d7;
}
Web safe fonts are the fonts that all browsers support out-of-the-box. They also work as fallback with custom fonts when they fail to load properly (more about this below).
- "American Typewriter", serif
- "Andalé Mono", monospace
- "Arial", sans-serif
- "Arial Black", sans-serif
- "Bodoni Poster", serif
- "Bradley Hand", cursive
- "Brush Script", cursive
- "Calibri", sans-serif
- "Candara", sans-serif
- "Century Gothic", sans-serif
- "Comic Sans MS", cursive
- "Copperplate", fantasy
- "Courier New", monospace
- "Courier", monospace
- "DejaVu Sans", sans-serif
- "EB Garamond", serif
- "Georgia", serif
- "Helvetica", sans-serif
- "Impact", sans-serif
- "Linotype Didot", serif
- "Lucida Console", monospace
- "Lucida Sans Unicode", sans-serif
- "Luminari", fantasy
- "Monaco", monospace
- "Palatino", serif
- "Papyrus", fantasy
- "Playbill", fantasy
- "Roboto", sans-serif
- "Rockwell", serif
- "Segoe Ui", sans-serif
- "Tahoma", sans-serif
- "Times New Roman", serif
- "Trattatello", fantasy
- "Trebuchet MS", sans-serif
- "Verdana", sans-serif
They are grouped in 5 generic fonts:
serif
sans-serif
monospace
fantasy
cursive
The way CSS fonts are defined is as follow:
.some-class {
font-family: "Montserrat", "Arial", sans-serif;
}
The font-family
needs at least two fonts: the family name (e.g., Montserrat
or Arial
) and the generic family name (e.g., sans-serif
). In the example above, the first two are the font that the browser must try to access, starting with the first one. That's why using the a web safe font as a last resort will always work.
Tip: Use a web safe font from the same generic family than the custom font. This helps keeping your brand somewhat consistent, even in case of custom font failure.
Original article: System font deep dive
Sometimes referred as to System Font Stack
, the system fonts are the default font used by the device's OS. The advantages of using those fonts are:
- No loading time, which means best performance and no SEO negative side-effects (FOUT, CLS).
- Website looking like native apps.
- Font that follows the latest trend.
The obvious drawback of system fonts is that your website will look different on every operating system. The font-size, letter-spacing, width and weight you carefully select on one platform may not look ideal when another font is used on a different platform. This is why the system font is usually a good choice for body text and maybe less of a good choice for headers.
You can define the system font in your CSS as follow:
font-family: system-ui;
NOTE: The legacy version of this code above is:
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";
OS | system-ui font |
---|---|
macOS, iOS, iPadOS | SF Pro |
Windows | Segoe UI |
Android | Roboto |
Ubuntu | Ubuntu |
Safari on Apple devices supports extra system ui on top of the universal system-ui
:
ui-monospace
ui-rounded
ui-sans-serif
ui-serif
: New Yorker.
There are 1.5 ways to load a font in your website:
@font-face
- HTML
link
tag, which in turns loads a CSS file that defines@font-face
, which is why I mention "1.5 ways to load a font".
The usual way to include font file is to include in your CSS a font face as follow:
@font-face {
font-family: 'Montserrat';
font-style: italic;
font-weight: 200;
font-display: optional;
src: url(https://fonts.gstatic.com/s/montserrat/v15/JTUPjIg1_i6t8kCHKm459WxZBg_z-PZwjimrq1Q_.woff2) format('woff2');
unicode-range: U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
}
With a link
, use that tag under the head
tag as follow:
<!-- https://fonts.gstatic.com is the font file origin -->
<!-- It may not have the same origin as the CSS file (https://fonts.googleapis.com) -->
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<!-- Asynchronously load that resource -->
<link rel="preload" as="style" href="https://fonts.googleapis.com/css2?family=Merriweather&display=swap"/>
<!-- Asynchronously load that resource CSS -->
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Merriweather&display=swap" media="print" onload="this.media='all'" />
<!-- Fallback if JS is disabled -->
<noscript>
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Merriweather&display=swap" />
</noscript>
For more details about font loading performance, please refer to the Fonts & performances section.
Google fonts (https://fonts.google.com/) exposes custom fonts via CSS rather than font files (e.g., woff2
). This means you do not use @font-face
directly (though this is what kind of happens at the end (1)). Instead, the font link is included in your page's head
:
<head>
<link rel="preconnect" href="https://fonts.gstatic.com">
<link href="https://fonts.googleapis.com/css2?family=Montserrat:ital,wght@0,200;0,300;0,400;0,500;0,600;0,700;0,800;0,900;1,200;1,300;1,400;1,500;1,600;1,700;1,800;1,900&display=optional" rel="stylesheet">
</head>
IMPORTANT: Notice the
display=optional
in the URL. Use it instead of the defaultdisplay=swap
. Otherwise, you will experience the FOIT (Flash Of Invisible Text) or FOUT (Flash Of Unstyled Text) issue.
(1) If you load that
https://fonts.googleapis.com/css2?...
link directly in your browser, you'll see that it returns a CSS file that uses@font-face
to load.woff2
files. This CSS file may vary based on your system. This design allows Google to serve CSS files optimized for the device requesting it.
Go to https://fonts.google.com/ to create the URL optimal to your needs. That URL is structured as follow:
https://fonts.googleapis.com/css2?<FAMILY CONFIG 01>&<FAMILY CONFIG 02>
Where you can add as many <FAMILY CONFIG XX>
as you want in the query parameters. <FAMILY CONFIG XX>
is configured as follow:
family=<FAMILY NAME>:<CONFIG NAME 01>,<CONFIG NAME 02>@<CONFIG VALUE 01/01>,<CONFIG VALUE 02/01>;<CONFIG VALUE 01/01>,<CONFIG VALUE 02/02>
As you can see, there can be many config names and their values can create many combinations. For example, let's imagine we need this font: Raleway
(family) with regular and font-weigth 300, italic and font-weigth 200 and italic and font-weigth 400. For that set of fonts, the query parameters will be:
family=Raleway:ital,wght@0,300;1,200;1,400
For multiple font families, the entire URL could be:
https://fonts.googleapis.com/css2?family=Raleway:ital,wght@0,300;1,200;1,400&family=Roboto:wght@100;400&display=swap
Google fonts also supports icons. Go to https://fonts.google.com/icons to browse them. Those icons are fonts that are available in both variable and static fonts.
The variable fonts can be installed and used as follow:
<!-- Add this in your page's <head> -->
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:opsz,wght,FILL,[email protected],100..700,0..1,-50..200" />
<style>
.material-symbols-outlined {
font-variation-settings:
'FILL' 0,
'wght' 400,
'GRAD' 0,
'opsz' 24
}
</style>
Where:
FILL
is a binary to indicate whether the icon is filled or not.wght
is the font weight.GRAD
is the font's grade. Grade is an additional layer to the font-weight to control finer details.opsz
is the optical size. This setting helps to automatically adjust the font-weight as the icon changes size.
To use the settings
icon, simply use this:
<span class="material-symbols-outlined">settings</span>
However, as discussed in the next section, variable fonts incur a heavy toll on performance. You might prefer to download a narrowed down version of the icon font. In the example above, the settings
icon uses the config 'FILL' 0, 'wght' 400, 'GRAD' 0, 'opsz' 24
. If that's all we need, then the font file can be updated as follow:
<!-- Add this in your page's <head> -->
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:opsz,wght,FILL,GRAD@40,200,0,200" />
The CSS config can be omitted since we only have access to a unique font-variation-settings
. This setup reduces the file's size from 2.9MB to 238KB.
The latest version of Google fonts support Variable fonts. On paper, one might think this should be the one to use, but in practice the static version is still the correct choice for one simple reason: Variable font files are too big! Indeed, a typical variable font file is around 3MB, while a specific font file might be around 200KB.
This doesn't mean variable fonts should not be used. The scenarios where they are usefull are:
- Experimentation and design phase.
- Many different variation of the same font results in a too many requests to the server or represent a total file size greated than the variable font's size.
Please refer to the Fonts & performances section under Performances.
https://meowni.ca/font-style-matcher/
Variable fonts are modern font that can be fully customized via the font-variation-settings
CSS property. This removes the need to download precompile fonts (one for thin, another for bold, ...). For example, the new Google Font Roboto Flex
is a variable font.
Specificity is the means by which browsers decide which CSS property values are the most relevant to an element and, therefore, will be applied. It is important to understant how it works, otherwise, you'll be tempted to overuse the !important
rule to force a CSS rule to overide another. Thought this can work, it has the few following negative side-effects:
!important
is not supported in AMP websites.- Many
!important
makes CSS messy and hard to read.
In a nutshell, remember that the most specific CSS selector wins. The priority rules by order of least importance are:
- Type selectors (e.g., h1) and pseudo-elements (e.g., ::before).
- Class selectors (e.g., .example), attributes selectors (e.g., [type="radio"]) and pseudo-classes (e.g., :hover).
- ID selectors (e.g., #example).
For example, in the following HTML:
<div class="my-title">
<span id="my-text">Hello</span>
<span class="my-subtitle">World</span>
</div>
.my-title {
color: red;
}
.my-subtitle {
color: blue;
}
Hello is red
and World is blue
. But if we add the following CSS rule:
.my-title .my-subtitle {
color: green;
}
Then World becomes green
because .my-title .my-subtitle
is more specific than .my-subtitle
.
Then, if we add:
#my-text {
color: pink;
}
Hello becomes pink
because ID selectors are more specific than class selectors.
Original article: https://www.freecodecamp.org/news/4-reasons-your-z-index-isnt-working-and-how-to-fix-it-coder-coder-6bc05f103e6c/
Correctly positioning elements on top of each other using CSS can be tricky and requires the understanding of what is referred to as stacking order.
A stack is a context in which the z-index
property has an effect.
By default, elements and stacks that are lower in the DOM are higher. In the example below, by default, the 3rd div is higher than the 2nd which is higher than the 1st.
<div id="one"></div>
<div id="two"></div>
<div id="three"></div>
The z-index
can overide this behavior. The following example shows how to position the 1st element on top of all the others:
<div id="one" style="z-index:1;"></div>
<div id="two"></div>
<div id="three"></div>
<div id="one"></div>
<div id="two" style="position:relative;"></div>
<div id="three"></div>
The 2nd element is now higher than both the 1st and 3rd element. The z-index
can overide this behavior. The following example shows how to position the 3rd element back on top of all the others:
<div id="one"></div>
<div id="two" style="position:relative;"></div>
<div id="three" style="z-index:1;"></div>
The transform
property (as well as the opacity
in certain situations) put the element into a new stack. In the example below, the 3rd element is added in a new stack, and because that stack is lower in the DOM tree than the previous two elements, it will appear on top of them, even though it has no position set while the 2nd element has:
<div id="one"></div>
<div id="two" style="position:relative;"></div>
<div id="three" style="transform:scale(1);"></div>
The z-index
can overide this behavior. The following example shows how to position the 2nd element back on top of all the others:
<div id="one"></div>
<div id="two" style="position:relative;z-index:1;"></div>
<div id="three" style="transform:scale(1);"></div>
The example below, you may think that the child
element is above the 3rd element because its z-index
is greater. However, that's not the case, because its parent's z-index is in a lower stack that the 3rd element.
<div id="one"></div>
<div id="two" style="position:relative;z-index:1;">
<div id="child" style="z-index:100;"></div>
</div>
<div id="three" style="z-index:1;"></div>
To fix this issue, you must increase the stack order of the 2nd element as follow:
<div id="one"></div>
<div id="two" style="position:relative;z-index:2;">
<div id="child" style="z-index:100;"></div>
</div>
<div id="three" style="z-index:1;"></div>
https://24ways.org/2019/beautiful-scrolling-experiences-without-libraries/
/* Hide scrollbar for Chrome, Safari and Opera */
.example::-webkit-scrollbar {
display: none;
}
/* Hide scrollbar for IE, Edge and Firefox */
.example {
-ms-overflow-style: none; /* IE and Edge */
scrollbar-width: none; /* Firefox */
}
Original article: https://www.smashingmagazine.com/2018/08/scroll-bouncing-websites/
This issue occurs mainly on mobile devices when the user scrolls down when the page is already at the top or when up when the page is already at the bottom. The default page behavior is to extend the scroll outside of the mobile screen as if it was streching. This effect is called scroll bouncing. There are many way to address this problem, but the simplest is to use CSS as follow:
body {
width: 100%;
position: fixed;
overscroll-behavior: none;
}
For more info about overscroll-behavior
, please refer to https://developer.mozilla.org/en-US/docs/Web/CSS/overscroll-behavior
/* The animation code */
@keyframes example {
from {background-color: red;}
to {background-color: yellow;}
}
/* The element to apply the animation to */
div {
width: 100px;
height: 100px;
background-color: red;
animation-name: example;
animation-duration: 4s;
animation-fill-mode: forwards; /* Use to prevent the animation to come back to start when it is done. */
/* animation-delay: 400ms; */
}
@keyframes refresh-icon-spin {
from {
transform:rotate(0deg);
}
to {
transform:rotate(360deg);
}
}
& .refresh-spinner {
animation-name: refresh-icon-spin;
animation-duration: 1000ms;
animation-iteration-count: infinite;
animation-timing-function: linear;
}
Truncate a single line:
.truncate {
width: 160px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
Allowing multiple lines and truncating the last one if the text overflows involves:
- Add ellipsis to the last line.
- Hiding the rest of the text behind an overflow.
span {
overflow: hidden;
text-overflow: ellipsis;
display: -webkit-box;
-webkit-line-clamp: 2; /* number of lines to show */
-webkit-box-orient: vertical;
}
overflow-wrap: anywhere;
.prevent-select {
-webkit-user-select: none; /* Safari */
-ms-user-select: none; /* IE 10 and IE 11 */
user-select: none; /* Standard syntax */
}
Use this CSS:
float: left;
width: 510px;
height: 220px;
object-fit: cover;
For example:
<div class="image">
<a href="/what-we-do/project-services"><img style="float: left;width: 510px;height: 220px;object-fit: cover;" src="images/unsplash/austin-distel-wD1LRb9OeEo-unsplash.jpg" alt=""></a>
</div>
You can also try to use this:
{
background: url(mountain.jpg);
background-size: cover;
}
img {
background:
linear-gradient( rgba(0, 0, 0, 0.3), rgba(0, 0, 0, 0.6) ),
url(/images/austin-distel-mpN7xjKQ_Ns-unsplash.jpg);
}
.shadow {
box-shadow: 0 4px 8px 0 rgb(0 0 0 / 20%), 0 6px 20px 0 rgb(0 0 0 / 19%);
}
WARNING: For full browser support use all those properties:
box-shadow
,-moz-box-shadow
,-webkit-box-shadow
,-ms-box-shadow
,-o-box-shadow
.
Original post: https://codepen.io/mirandalwashburn/pen/bGdvGwR
For this to work, make sure that the box's surroundings are not white. Use some sort of light grey (e.g., #f0f0f3
):
border-radius: 8px;
box-shadow: inset -6px -6px 6px 0 rgb(255 255 255 / 70%), inset 6px 6px 6px 0 rgb(174 174 192 / 20%);
CSS Flexbox is the best way to place items inside a container. If you're just after aligning items, then this should do. If you wish to place items following strict rows/columns rules, then you should use CSS Grid instead.
.somecell {
flex-grow: 1;
}
CSS grid is the best way to organize content following rows/columns rules.
.wrapper {
display: grid;
grid-template-rows: 60px 1fr 2fr;
grid-template-columns: 1fr 1fr 200px;
}
The above creates a gris with 3 rows (row 1: 60px, row 2: 1 unit of available space, row 3: 1 units of available space) and 3 columns (column 1: 1 unit of available space, column 2: 1 unit of available space and column 3: 200px). This means the row 2 and 3 as well as column 1 and 2 will auto-adjsut to fill all available space.
<div class="wrapper">
<div>Row 1 - Column 1</div>
<div>Row 1 - Column 2</div>
<div>Row 1 - Column 3</div>
<div>Row 2 - Column 1</div>
<div>Row 2 - Column 2</div>
<div>Row 2 - Column 3</div>
<div>Row 3 - Column 1</div>
<div>Row 3 - Column 2</div>
<div>Row 3 - Column 3</div>
</div>
Notice that the content fills the grid top to right. To learn how to overide this placement, please jump to the next section.
TIP: If your to repeat the same dimensions, use the
repeat
CSS function:grid-template-rows: repeat(3, 1fr);
The previous example places content in each cell. However, one of the neat feature of CSS grid is the ability to put content in specific cells as well as put content that overlap a group of cells.
.box1 {
grid-column-start: 1;
grid-column-end: 3;
}
This class places the content in the first 2 columns:
<div class="wrapper">
<div>Row 1 - Column 1</div>
<div>Row 1 - Column 2</div>
<div>Row 1 - Column 3</div>
<div>Row 2 - Column 1</div>
<div>Row 2 - Column 2</div>
<div>Row 2 - Column 3</div>
<div class="box1">Row 3 - Column 1 & 2</div>
<div>Row 3 - Column 3</div>
</div>
CSS properties grid-row-start
and grid-row-end
follow the same reasoning.
Another way to control the content placement is via grid-area
.
.wrapper {
display: grid;
grid-gap: 15px;
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
}
--margin: 30px;
display: grid;
height: 100vh;
width: 100vw;
grid-template-areas:
'nav nav nav'
'work work config'
'work work config';
grid-template-rows: 60px 1fr 1fr;
grid-template-columns: 1fr 1fr 200px;
& .nav-bar {
grid-area: nav;
background-color: #74BDCB;
display: flex;
align-items: center;
padding: 0 var(--margin) 0 var(--margin);
}
& .workspace {
grid-area: work;
background-color: #FFA384;
}
& .config-pane {
grid-area: config;
background-color: #EFE7BC;
}
grid-gap: 15px;
This is a shortcut for:
grid-row-gap: 15px;
grid-column-gap: 15px;
@media (max-width: 768px) {
/* CSS for your small screen here */
}
This feature allows you to use a background image(1) as a mask on your background element:
.mask {
--mask: url(https://fonts.gstatic.com/s/i/short-term/release/materialsymbolsoutlined/alarm/default/24px.svg);
--size: 32px;
/* This trick */
-webkit-mask-image: var(--mask, unset);
mask-image: var(--mask, unset);
-webkit-mask-size: var(--size);
mask-size: var(--size);
-webkit-mask-repeat: no-repeat;
mask-repeat: no-repeat;
/* Coloring your result */
background-color: red; /* You could also use an image or a gradient */
}
(1) Usually an svg or png image. The transparent pixels are equivalent to 0s, which means, they will hide the background.
// Show the CSS:
console.log(document.styleSheets[2].cssRules[i].cssText)
// Create a new CSS rule
document.styleSheets[5].insertRule('.helloDude {color:red;}')
WARNING: Trying to access a stylsheet from another domain will throw an exception. More about dealing with this issue under the Uncaught DOMException: Failed to read the 'cssRules' property from 'CSSStyleSheet': Cannot access rules section.
import { css } from '@emotion/css'
const yourClass = css`
padding: 10px;
background: red;
&.special {
& .hello,
& .world {
font-size: 13px;
}
& .hello {
font-weight: bold;
}
}
supercool & {
background: cyan;
}
`
render(
<div className={`${yourClass} special`}>
<div class="hello">Hello</div>
<div class="world">World</div>
</div>
)
Notice:
&.special
: Because there is no space between&
and.special
, this means this style only applies if thespecial
class is defined on the same DOM as theyourClass
class.& .hello
: The space between&
and.hello
means that any DOM with thehello
class nested anywhere under.special
will be styled accordingly to thehello
definition.& .hello,& .world
: Emotion allows to group styles.supercool &
: Allows to define style outside of the scope of the component. In this case, this means that if any ancestor defines asupercool
class, this style will apply.
Let's changing the properties on small screen:
const yourClass = css`
padding: 10px;
background: red;
&.special {
& .hello,
& .world {
font-size: 13px;
}
& .hello {
font-weight: bold;
}
}
supercool & {
background: cyan;
}
@media (max-width: 768px) {
padding: 5px;
}
`
https://emotion.sh/docs/@emotion/css#global-styles
Linaria is similar to Emotion except it compiles the styles into CSS files. Advantages:
- Better app performances:
- No extra parsing needed for CSS on the client (as opposed to Emotion).
- CSS is downloaded and parsed separately from JS, which means parallel files download.
- No style duplication on SSR
- Catch errors early due to build-time evaluation
- Familiar CSS syntax
- Works without JavaScript
import { css } from '@linaria/core'
export const myGlobals = css`
:global() {
html {
box-sizing: border-box;
}
*,
*:before,
*:after {
box-sizing: inherit;
}
body {
margin: 0;
background: red;
}
}
`
This myGlobals
does not to be added on any DOM. The simple fact this code is executed updates the CSS globally.
animation
:-moz-animation
,-webkit-animation
background-clip
:-moz-background-clip
,-webkit-background-clip
box-shadow
:-moz-box-shadow
,-webkit-box-shadow
,-ms-box-shadow
,-o-box-shadow
box-sizing
:-moz-box-sizing
,-webkit-box-sizing
text-fill-color
:-moz-text-fill-color
,-webkit-text-fill-color
transform
:-webkit-transform
,-moz-transform
transition
:-webkit-transition
,-moz-transition
,-o-transition
This is supported differently based on the browser. Use the following to cover all scenarios:
box-sizing: border-box;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
Add this CSS on the affected DOM:
-webkit-tap-highlight-color: transparent;
Please refer to the The million things Safari does not want you to do in CSS section.
Though the hover effect does not apply on mobile, it can be triggered by other touch interactions depending on the browser. This leads to unwanted behaviors. To avoid those unpredicted effects, the :hover
effect should only be defined when it is supported. CSS media queries helps achieving this setup:
@media (hover: hover) {
button {
background: white;
}
button:hover {
background: yellow;
}
}
To test if the current device is mobile, please refer to the JS code in the annexes.
- Understanding Web Animation Performance
- Investigate Animation Performance with DevTools
- The basics of CSS transforms: Part 1
- Browser Rendering Optimization
https://web.dev/defer-non-critical-css/
Best article that summarizes it: Investigate Animation Performance with DevTools Other great articles:
In a nutshell:
- A web page rendering process follows this sequence:
- HTML parsing.
- Style calculation.
- Layout creation (creating the boxes that contain your content and sizing).
- Painting (creating the pixels for each layout).
- Compositing (Putting all painted layouts together to create your page).
- Animations that suck require reprocessing steps 3 to 5.
- Animations that rock only require reprocessing step 5.
The CSS properties that only need step 5 reprocessing are:
transform
opacity
To make sure animations runs at the optimal 60FPS, please replace the following CSS properties as follow:
height
andwidth
withtransform: scale
top
,left
,bottom
,right
withtransform: translate
If you're interested in knowing how to best manipulate CSS in Javascript, please refer to this document: https://gist.github.com/nicolasdao/312da25b71bf08c36083694c045d8d5b#waapi--worklets
- Stick to CSS properties
transform
andopacity
when designing animated interactions. - DO NOT USE CSS VARIABLES IN
transform
ORopacity
TO ANIMATE THEM. This is a typical mistake. Updating CSS Variables triggers a reflow because they are inheritable. When you wish to animate a DOM, update thetransform
ORopacity
property explicitly by targetting the exact DOM. - Replace modifying CSS style properties individually because each property triggers a reflow for each dynamic style change. Use a single class with all the CSS properties instead. Alternatively, use
cssText
// WARNING: Check that 'cssText' is supported in your browser
element.style.cssText += "left: " + left + "px; top: " + top + "px;"; // triggers reflow once
- Apply animations with position
fixed
orabsolute
so it doesn’t affect the layout of other elements. - Cache any of the following CSS properties as each invocation requires a reflow:
clientHeight
clientWidth
offsetHeight
offsetWidth
getBoundingClientRect()
WARNING: Requesting those properties in a loop can have terrible negative implications on performances.
- When possible, hide or show the element using
visibility: hidden
andvisibility: visible
instead ofdisplay: none
anddisplay: block
. This will prevent reflows. - Use
textContent
instead ofinnerText
.innerText
creates reflow each time it is being requested. - Use
requestAnimationFrame
for expensive ops:
requestAnimationFrame(() => {
/* should be able to get offsetHeight here */
console.log(divContainer.offsetHeight);
}
- Use the
will-change
CSS property to force the browser to put the specific DOM with that property in its own layout:
.my-animation {
transition: transform 200ms linear;
will-change: transform;
}
WARNING: Do not overuse this as creating too many layout may stress the browser excessively and eventually degrade performances rather than improving it.
The most important rule is: DON'T USE CUSTOM FONTS!. Instead, use a Web Safe Font or a System Font.
If you still need a custom font, you must be aware that:
- YOU CANNOT AVOID FOUT (Flash Of Unstyled Text) with custom fonts. Browse to one of your favorite website from an established company that uses a custom fonts and pay attention to the few milliseconds when the page loads. You'll notice FOUT.
- At best, you should aim to avoid the Content Layout Shift (CLS).
This does not mean you cannot create the illusion of NO FOUT with a custom font. To create that illusion:
- Optimize the font loading to speed up its availability.
- Use visual effects to delay when the text is rendered (making sure those effects don't create CLS).
To avoid CLS, please refer to the Avoiding Content Layout Shift due to font swapping section.
Original articles:
- By far the best performance tip: DON'T USE CUSTOM FONTS AT ALL - USE WEB SAFE FONTS INSTEAD. Web Safe Fonts are preloaded by all browsers. Most of them are listed in the Web safe fonts sections. If you still need a custom fonts, then minimize their use to parts that require strong branding (e.g.,headers) and use a web safe font for the body.
- Use
preconnect
(https://web.dev/font-best-practices/#preconnect-to-critical-third-party-origins)
<head>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
</head>
- Use
preload
to asynchronously load the font before it is needed:
<!-- https://fonts.gstatic.com is the font file origin -->
<!-- It may not have the same origin as the CSS file (https://fonts.googleapis.com) -->
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<!-- Asynchronously load that resource -->
<link rel="preload" as="style" href="https://fonts.googleapis.com/css2?family=Merriweather&display=swap"/>
<!-- Asynchronously load that resource CSS -->
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Merriweather&display=swap" media="print" onload="this.media='all'" />
<!-- Fallback if JS is disabled -->
<noscript>
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Merriweather&display=swap" />
</noscript>
The
onload="this.media='all'"
is a hack to prevent the browser to block rendering. More about the hack in the How to load CSS stylesheets asynchronously? section.
- Use fonts that are hosted on a CDN. If you don't self-host on a CDN, self-hosting your font will be worse than using a third-party that uses a CDN.
- Use compressed fonts. Use
WOFF2
. Because it uses Brotli, WOFF2 compresses 30% better than WOFF, leading to less data to download and therefore faster performance. - Subset your font, i.e., don't load all characters of a font if you don't use it. Google font easily let you choose. This will decrease your font size.
- Match your fallback font with your custom font. Use a tool such as Font style matcher to help you.
- Cache your custom font on your CDN using the
Cache-Control
header. - Use the CSS property
font-display
to adjust your strategy to your needs (most common values areoptional
andswap
):- If performance is a top priority: Use
font-display: optional
. This is the most "performant" approach: text render is delayed for no longer than 100ms and there is assurance that there will be no font-swap related layout shifts. However, the downside here is the web font will not be used if it arrives late. - If displaying text quickly is a top priority, but you want to still ensure the web-font is used: Use
font-display: swap
but make sure to deliver the font early enough that it does not cause a layout shift. The downside of this option is the jarring shift when the font arrives late. - If ensuring text is displayed in a web font is a top priority: Use
font-display: block
but make sure to deliver the font early enough that it minimises the delay of the text. The downside of this is the initial text display will be delayed. Note despite this deplay, it can still cause a layout shift as the text is actually drawn invisible, and the fallback font space is therefore user to reserver the space. Once the web font loads, this may require difference space and hence a shift. This may, however, be a less jarring shift than font-display: swap as the text itself will not be seen to shift.
Also keep in mind that these two approaches can be combined: for example, use font-display: swap for branding and other visually distinctive page elements; use font-display: optional for fonts used in body text.
- If performance is a top priority: Use
- Don't use fonts for icons, use SVG instead. Icon fonts are more likely to cause significant layout shifts. Material design fonts and Font awesome now support SVG.
- Use the CSS property
size-adjust
to manually minimize the layout shit during web font swapping. Full details at https://web.dev/css-size-adjust/.
A semi-exhaustive list of the latest (August 2023) Web Safe Fonts is defined under the Web safe fonts section. As for system fons
Caching your CSS and font files can significantly improve your page loading time. This is not something you can do on the client side (besides using a service worker to work offline). Instead, a caching policy must be configure on your file hosting server which must set the Cache-Control
response header. To learn more about this topic, please refer to the Understanding the native Cache-Control HTTP header section of the AWS CLOUDFRONT GUIDE document.
Main article: Performance of CSS Variables
- Border radius broken when the container uses
overflow: hidden
- Gradient effect on text does not work
height:100%
does not work when the parent does not explicitely define its height- Mobile Safari freezes when scrolling at the top or bottom or while touching the screen as it scrolls
Border radius broken when the container uses overflow: hidden
To fix this, use the following CSS:
overflow: hidden;
-webkit-mask-image: -webkit-radial-gradient(white, black);
You must add display: block
as follow:
display: block; /* For safari */
background-image: linear-gradient(0deg, red, blue);
background-clip: text;
-moz-background-clip: text;
-webkit-background-clip: text;
text-fill-color: transparent;
-moz-text-fill-color: transparent;
-webkit-text-fill-color: transparent;
Try adding a display:flex;
on the parent and then replace height:100%;
on the child with height:auto;
.
Mobile Safari freezes when scrolling at the top or bottom or while touching the screen as it scrolls
Known bug that the freaking Safari team never bother to fixed. This is documented here. To fix this stupid behavior, try adding the following CSS on the body
:
body {
overflow: hidden;
}
Supported on Desktop but not on Mobile.
Forget about this simple code:
.simple {
display: flex;
gap: 15px;
}
.parent {
display:none;
}
.my-class {
color: red;
&:hover {
color: green;
}
.bg {
background-color: black;
}
.parent & {
display:flex;
}
}
When transpiled by the SASS loader, this SASS code becomes this CSS:
.my-class {
color: red;
}
.my-class:hover {
color: green;
}
.my-class .bg {
background-color: black;
}
.parent {
display:none;
}
.parent .my-class {
display:flex;
}
$my-constant: 12;
$my-maps: ("small":10px, "regular":30px, "large":60px);
$small: map.get($my-maps, "small"); // 10px
$my-list: 10px, 30px, 60px;
$second-val: list.nth($my-list, 2); // 30px
$last-val: list.nth($my-list, -1); // 60px
NOTES:
- Both
list
andmap
are iterables. This means you can loop through them via@each
.- A
map
is immutable.
Use the #{...}
to insert a variable's value with inside a string:
$my-constant: 12;
.col-#{$my-constant} {
color:red;
}
$columns: 3
@for $i from 1 through $columns {
.col-#{$i}: {
width: ($i*10) + px
}
}
generates this CSS:
.col-1 {
width: 10px;
}
.col-2 {
width: 20px;
}
.col-3 {
width: 30px;
}
$icons:
"eye" "\f112" 12px,
"start" "\f12e" 16px,
"stop" "\f12f" 10px;
@each $name, $glyph, $size in $icons {
.icon-#{$name}:before {
display: inline-block;
font-family: "Icon Font";
content: $glyph;
font-size: $size;
}
}
Becomes this CSS:
icon-eye:before {
font-family: "Icon Font";
content: \f112;
}
icon-start:before {
font-family: "Icon Font";
content: \f12e;
}
This keywords allows to import SCSS mixins, variables, functions into the current SCSS file. The files that are being referenced are called modules
.
// style.scss
@use 'foundation/code';
@use 'foundation/lists';
In this example, there are two module files:
foundation/_code.scss
foundation/_lists.scss
Notice that those files are prefixed with underscore. This is an SCSS convention. More about this convention in the File structure and the underscore convention section.
This keyword allows to invoke a mixin:
nav ul {
@include horizontal-list;
}
@mixin create-classes($prop, $step, $unit, $end) {
@for $i from 1 through $end {
&-#{$i} {
#{$prop}: ($i*$step) + $unit
}
}
}
.my-class {
@include create-classes(width, 5, px, 2)
}
generates this CSS:
.my-class-1 {
width: 5px;
}
.my-class-2 {
width: 10px;
}
To default arguments, use something similar to the following:
@mixin create-classes($prop, $step, $unit, $end:2) { ... }
You'll notice that many SCSS library (e.g., Bootstrap) are structured this way:
_alert.scss
_badge.scss
...
bootstrap.scss
If you open the bootstrap.scss
, you'll see something similar to this:
@import "badge";
@import "alert";
...
With Saas, an _
means a partial scss file. This means that files with an _
prefix are not parsed by the compiler. There will be neither _badge.css
nor _alert.css
. Those files are instead used to breakdown your SCSS files in modules that are then aggregated into a bigger one (in the Bootstrap example, that's the bootstrap.scss
).
NOTICE: In the
bootstrap.scss
file, you import partial scss by omitting the underscore.
overflow:hidden
with border-radius
not working in Safari
That's a known bug. Add the following CSS property:
/* Safari hack for border-radius with overflow: hidden */
-webkit-mask-image: -webkit-radial-gradient(white, black);
Uncaught DOMException: Failed to read the 'cssRules' property from 'CSSStyleSheet': Cannot access rules
This most probably happens because the document.styleSheets[].rules
JS is trying to access a stylesheet from another domain. To check if the CSS is coming from the same domain, use something similar to:
const accessibleStylesheet = !document.styleSheets[0].href || document.styleSheets[0].href.startsWith(WIN.location.origin)
A common reason stylesheet from different domain are loaded on a website are Chrome extensions.
Add the following CSS property:
.your-class {
animation-fill-mode: forwards;
}
This is useful when you want to change the image based on hover state for example. Your normal HTML would normally look like this:
<div>
<img src="https://example.com/myimage_01.jpg" alt="my image description">
</div>
Replace the above with:
<div class="image-style">
<img src="https://example.com/myimage_01.jpg" alt="my image description">
</div>
.image-style {
display: block;
-moz-box-sizing: border-box;
box-sizing: border-box;
background: url(https://example.com/myimage_01.jpg) no-repeat;
width: 38px; /* image width */
height: 38px; /* image height */
padding-left: 38px; /* Equal to image width */
}
.image-style:hover {
cursor: pointer;
background: url(https://example.com/myimage_02.jpg) no-repeat;
}
Note that we have to keep the explicit
src
attribute set to some value.
There are 2 different ways to delay CSS effects:
- Using the an extra parameter in the
transition
property. For example, the following snippet shows how to transition from no background to a black background within 100ms:To add a 200ms delay, before that transition starts, we can rewrite the above as follow:background: black; transition: background 100ms ease;
background: black; transition: background 100ms 200ms ease;
- Use the
transition-delay
property:background: black; transition: background 100ms ease; transition-delay: 200ms;
The reason you would use the first method over the second depends on whether the delay happens to all transitions or not. With the first method, you can target which specific transition should be delayed.
-webkit-transition: ...;
-moz-transition: ...;
-ms-transition: ...;
-o-transition: ...;
transition: ...;
Please refer to the Fitting images section.
The hack is to use an invalid media
attribute on the link
tab. This causes the browser to download the stylesheet, but it won't wait for the content to be available before rendering the page. Once the content is loaded, the media is set to a valid value so that the style can be applied properly:
<link rel="stylesheet" href="style.css" media="none" onload="this.media='all'">
https://meowni.ca/font-style-matcher/
By default, when padding and borders are added to an element, the extra thickness is added on top of the current element's dimensions. For example, if the element's width is 100px, and its padding is 20px and border is 1px, then its total width is 121px. If you wish to preserve the element's width to 100px, add the following CSS property:
{
box-sizing: border-box;
}
/*eslint-disable */
const MOBILE_USER_AGENT_01 = /(android|bb\d+|meego).+mobile|avantgo|bada\/|blackberry|blazer|compal|elaine|fennec|hiptop|iemobile|ip(hone|od)|ipad|iris|kindle|Android|Silk|lge |maemo|midp|mmp|netfront|opera m(ob|in)i|palm( os)?|phone|p(ixi|re)\/|plucker|pocket|psp|series(4|6)0|symbian|treo|up\.(browser|link)|vodafone|wap|windows (ce|phone)|xda|xiino/i
const MOBILE_USER_AGENT_02 = /1207|6310|6590|3gso|4thp|50[1-6]i|770s|802s|a wa|abac|ac(er|oo|s\-)|ai(ko|rn)|al(av|ca|co)|amoi|an(ex|ny|yw)|aptu|ar(ch|go)|as(te|us)|attw|au(di|\-m|r |s )|avan|be(ck|ll|nq)|bi(lb|rd)|bl(ac|az)|br(e|v)w|bumb|bw\-(n|u)|c55\/|capi|ccwa|cdm\-|cell|chtm|cldc|cmd\-|co(mp|nd)|craw|da(it|ll|ng)|dbte|dc\-s|devi|dica|dmob|do(c|p)o|ds(12|\-d)|el(49|ai)|em(l2|ul)|er(ic|k0)|esl8|ez([4-7]0|os|wa|ze)|fetc|fly(\-|_)|g1 u|g560|gene|gf\-5|g\-mo|go(\.w|od)|gr(ad|un)|haie|hcit|hd\-(m|p|t)|hei\-|hi(pt|ta)|hp( i|ip)|hs\-c|ht(c(\-| |_|a|g|p|s|t)|tp)|hu(aw|tc)|i\-(20|go|ma)|i230|iac( |\-|\/)|ibro|idea|ig01|ikom|im1k|inno|ipaq|iris|ja(t|v)a|jbro|jemu|jigs|kddi|keji|kgt( |\/)|klon|kpt |kwc\-|kyo(c|k)|le(no|xi)|lg( g|\/(k|l|u)|50|54|\-[a-w])|libw|lynx|m1\-w|m3ga|m50\/|ma(te|ui|xo)|mc(01|21|ca)|m\-cr|me(rc|ri)|mi(o8|oa|ts)|mmef|mo(01|02|bi|de|do|t(\-| |o|v)|zz)|mt(50|p1|v )|mwbp|mywa|n10[0-2]|n20[2-3]|n30(0|2)|n50(0|2|5)|n7(0(0|1)|10)|ne((c|m)\-|on|tf|wf|wg|wt)|nok(6|i)|nzph|o2im|op(ti|wv)|oran|owg1|p800|pan(a|d|t)|pdxg|pg(13|\-([1-8]|c))|phil|pire|pl(ay|uc)|pn\-2|po(ck|rt|se)|prox|psio|pt\-g|qa\-a|qc(07|12|21|32|60|\-[2-7]|i\-)|qtek|r380|r600|raks|rim9|ro(ve|zo)|s55\/|sa(ge|ma|mm|ms|ny|va)|sc(01|h\-|oo|p\-)|sdk\/|se(c(\-|0|1)|47|mc|nd|ri)|sgh\-|shar|sie(\-|m)|sk\-0|sl(45|id)|sm(al|ar|b3|it|t5)|so(ft|ny)|sp(01|h\-|v\-|v )|sy(01|mb)|t2(18|50)|t6(00|10|18)|ta(gt|lk)|tcl\-|tdg\-|tel(i|m)|tim\-|t\-mo|to(pl|sh)|ts(70|m\-|m3|m5)|tx\-9|up(\.b|g1|si)|utst|v400|v750|veri|vi(rg|te)|vk(40|5[0-3]|\-v)|vm40|voda|vulc|vx(52|53|60|61|70|80|81|83|85|98)|w3c(\-| )|webc|whit|wi(g |nc|nw)|wmlb|wonu|x700|yas\-|your|zeto|zte\-/i
/*eslint-enable */
let _isMobile = null
const isMobile = () => {
if (_isMobile === null) {
if (!WIN || !WIN.navigator || !WIN.navigator.userAgent) {
_isMobile = false
} else {
const userAgent = WIN.navigator.userAgent
_isMobile = MOBILE_USER_AGENT_01.test(userAgent) || MOBILE_USER_AGENT_02.test(userAgent.substr(0,4))
}
}
return _isMobile
}