Re-Creating The Pop-Out Hover Effect With Modern CSS (Part 1)

In a previous article on CSS-Tricks, I demonstrated how to create a fancy hover effect where an avatar pops out of a circle on hover. The challenge was to make such an effect using only the <img> tag.

I know it might seem like this shape requires advanced trickery. But if we break it down a bit, all we’re really talking about is a series of small circles around a much larger circle.

We are going to rely on radial-gradient and some math, specifically trigonometric functions. Bramus Van Damme provides an excellent primer on trigonometric functions over at web.dev. It’s very much worth your while to brush up on the concept with that article.

We are going to define two variables to control the flower shape. N represents the number of the small circles, and R is the diameter of the small circles (Illustrated by the black arrow in the figure above). If we have the diameter, then we can calculate the radius by dividing R by 2. This is everything we need to create the shape!

Here is what the code of a small circle looks like:

img {
  --r: 50px;
  mask:
    radial-gradient(#000 70%, #0000 72%) no-repeat
    {position} / var(--r) var(--r);
}

All of the small circles can use the same radial gradient. The only difference between them is the position. Here comes the math:

(50% + 50% * cos(360deg * i/N)) (50% + 50% * sin(360deg * i/N))

N is the number of circles, and i is the index of each circle. We could manually position each circle individually, but that’s a lot of work, and I believe in leveraging tools to help do some of the heavy lifting. So, I’m going to switch from CSS to Sass to use its ability to write loops and generate all of the circle positions in one fell swoop.

$n: 15; /* number of circles */

img {
  --r: 50px; /* control the small circles radius */
  $m: ();
  @for $i from 1 through ($n) {
    $m: append($m, 
         radial-gradient(#000 70%,#0000 72%) no-repeat
          calc(50% + 50% * cos(360deg * #{$i / $n})) 
          calc(50% + 50% * sin(360deg * #{$i / $n})) /
            var(--r) var(--r), 
        comma);
   }
  mask: $m;
}

We’re essentially looping through the number of circles ($n) to define each one by chaining the radial gradient for each one as comma-separated values on the mask ($m) that is applied to the image element.

We still need the large circle that the small circles are positioned around. So, in addition to the loop’s output via the $m variable, we chain the larger circle’s gradient on the same mask declaration:

img {
  /* etc */
  mask: $m, radial-gradient(#000 calc(72% - var(--r)/2),#0000 0);
}

Finally, we define the size of the image element itself using the same variables. Calculating the image’s width also requires the use of trigonometric functions. Then, rather than doing the same thing for the height, we can make use of the relatively new aspect-ratio property to get a nice 1:1 ratio:

img {
  /* etc */
  width: calc(var(--r) * (1 + 1/tan(180deg / #{$n})));
  aspect-ratio: 1;
}

Check it out. We have the shape we want and can easily control the size and number of circles with only two variables.

We’re basically reducing the distance of the small circles, making them closer to the center. Then, we reduce the size of the larger circle as well. This produces an effect that appears to change the roundness of the smaller circles on hover.

The final trick is to scale the entire image element to make sure the size of the hovered shape is the same as the non-hovered shape. Scaling the image means that the avatar will get bigger and will pop out from the frame that we made smaller.

$n: 15; /* number of circles */

@property --i {
  syntax: "<length>";
  initial-value: 0px;
  inherits: true;
}

img {
  /* CSS variables */
  --r: 50px; /* controls the small circle radius and initial size */
  --f: 1.7; /* controls the scale factor */
  --c: #E4844A; /* controls the main color */

  $m: ();
  /* Sass loop */
  @for $i from 1 through ($n) {
    $m: append($m, 
      radial-gradient(var(--c) 70%, #0000 72%) no-repeat
      calc(50% + (50% - var(--i, 0px)) * cos(360deg * #{$i/$n} + var(--a, 0deg))) 
      calc(50% + (50% - var(--i, 0px)) * sin(360deg * #{$i/$n} + var(--a, 0deg))) /
      var(--r) var(--r), 
    comma);
  }

  mask: 
    linear-gradient(#000 0 0) top/100% 50% no-repeat,
    radial-gradient(var(--c) calc(72% - var(--r)/2 - var(--i, 0px)), #0000 0),
    $m;
  background:
    radial-gradient(var(--c) calc(72% - var(--r)/2 - var(--i, 0px)), #0000 0),
    $m;
  transition: --i .4s, scale .4s;
}

img:hover {
  --i: calc(var(--r)/var(--f));
  scale: calc((1 + 1/tan(180deg/#{$n}))/(1 - 2/var(--f) + 1/tan(180deg/#{$n})));
}

Here’s what’s changed:

  • The Sass loop that defines the position of the circle uses an equation of 50% - var(--i, 0px) instead of a value of 50%.
  • The larger circle uses the same variable, --i, to set the color stop of the main color in the gradients that are applied to the mask and background properties.
  • The --i variable is updated from 0px to a positive value. This way, the small circles move position while the large circle becomes smaller in size.
  • The --i variable is registered as a custom @property that allows us to interpolate its values on hover.

You may have noticed that I didn’t mention anything about the --f variable that’s defined on the image element. Truthfully, there is no special logic to it. I could have defined any positive value for the variable --i on hover, but I wanted a value that depends on --r, so I came up with a formula (var(--r) / var(--f)), where --f allows controls the scale.

Does the equation on the scale property on hover give you a little bit of panic? It sure looks complex, but I promise you it’s not. We divide the size of the initial shape (which is also the size of the element) by the size of the new shape to get the scale factor.

  • The initial size: calc(var(--r)*(1 + 1 / tan(180deg / #{$n})))
  • The size of the new shape: calc(var(--r) * (1 + 1 / tan(180deg / #{$n})) - 2 * var(--r) / var(--f))

I am skipping a lot of math details to not make the article lengthy, but feel free to comment on the article if you want more detail on the formulas I am using.

That’s all! We have a nice “pop out” effect on hover:

See the Pen Fancy Pop Out hover effect! by Temani Afif.

Wrapping Up

Does all of this seem a bit much? I see that and know this is a lot to throw at anyone in a single article. We’re working with some pretty new CSS features, so there’s definitely a learning curve with new syntaxes, not to mention some brushing up on math functions you probably haven’t seen in years.

But we learned a lot of stuff! We used gradients with some math to create a fancy shape that we applied as a mask and background. We introduced @property to animate CSS variables and bring our shape to life. We also learned a nice trick using animation-composition to control the speed of the rotation.

We still have a second part of this article where we will reuse the same CSS techniques to create a fancier hover effect, so stay tuned!

I’ll leave you with one last demo as a sign-off.

See the Pen Pop out hover effect featuring Lea and Una by Temani Afif.

The Path To Awesome CSS Easing With The linear() Function

To paraphrase a saying that has always stuck with me: “The best animation is that which goes unnoticed.” One of the most important concepts of motion design on the web is making motion “feel right.” At the same time, CSS has been fairly limited when it comes to creating animations and transitions that feel natural and are unobtrusive to the user experience.

Fortunately, that’s changing. Today, let’s look at new easing capabilities arriving in CSS. Specifically, I want to demonstrate the easing superpowers of linear() — a new easing function that is currently defined in the CSS Easing Level 2 specification in the Editor’s Draft. Together, we’ll explore its ability to craft custom easing curves that lead to natural-feeling UI movement.

The fact that linear() is in the Editor’s Draft status means we’re diving into something still taking shape and could change by the time it reaches the Candidate Recommendation. As you might imagine, that means linear() has limited support at this moment in time. It is supported in Chrome and Firefox, however, so be sure to bear that in mind as we get into some demos.

Before we jump straight in, there are a couple of articles I recommend checking out. They’ve really influenced how I approach UI motion design as a whole:

There are plenty of great resources for designing motion in UI, but those are two that I always keep within reach in my browser’s bookmarks, and they have certainly influenced this article.

The Current State Of Easing In CSS

We define CSS easing with either the animation-timing-function or transition-timing-function properties, depending on whether we are working with an animation or transition respectively.

Duration is all about timing, and timing has a big impact on the movement’s naturalness.

But, until recently, CSS has limited us to the following easing functions:

  • linear,
  • steps,
  • ease,
  • ease-in,
  • ease-out,
  • ease-in-out,
  • cubic-bezier().

For a refresher, check out this demo that shows the effect of different timings on how this car travels down the track.

Easing curves can also be viewed in Chromium DevTools, allowing you to inspect any curve applied to a transition or animation.

Getting “Extra” Easing With linear()

But what if you need something a little extra than what’s available? For example, what about a bounce? Or a spring? These are the types of easing functions that we are unable to achieve with a cubic-bezier() curve.

This is where the new linear() easing function comes into play, pioneered by Jake Archibald and defined in the CSS Easing Level 2 specification, which is currently in the Editor’s Draft. MDN describes it well:

The linear() function defines a piecewise linear function that interpolates linearly between its points, allowing you to approximate more complex animations like bounce and elastic effects.

In other words, it’s a way to plot a graph with as many points as you like to define a custom easing curve. That’s pretty special and opens new possibilities we could not do before with CSS animations and transitions.

For example, the easing for a bounce could look like this:

:root {
  --bounce-easing: linear(
    0, 0.004, 0.016, 0.035, 0.063, 0.098, 0.141 13.6%, 0.25, 0.391, 0.563, 0.765,
    1, 0.891 40.9%, 0.848, 0.813, 0.785, 0.766, 0.754, 0.75, 0.754, 0.766, 0.785,
    0.813, 0.848, 0.891 68.2%, 1 72.7%, 0.973, 0.953, 0.941, 0.938, 0.941, 0.953,
    0.973, 1, 0.988, 0.984, 0.988, 1
  );
}

Here’s how that looks in action:

Where’s All Of This Going?

For as long as I can remember, if I’ve needed some special easing for the work I’m doing, GreenSock has been my go-to solution. Its ease visualizer is one of my favorite examples of interactive documentation.

As soon as I heard about the linear() function, my mind went straight to: “How can I convert GreenSock eases to CSS?” Imagine how awesome it would be to have access to a popular set of eases that can be used directly in CSS without reaching for JavaScript.

GreenSock’s visualizer accepts JavaScript or an SVG path. So, my first thought was to open DevTools, grab the SVG paths from the visualizer, and drop them into the tool. However, I encountered a hurdle because I needed to scale down the path coordinates for a viewBox of 0 0 1 1. GreenSock’s visualizer has a viewBox set to 0 0 500 500. I wrote a function to convert the coordinates and reverse the path to go in the right direction. Then, I reached out to Jake with some questions about the generator. The code is available on GitHub.

In my head, I thought the SVG route made sense. But, then I created a path that wouldn’t work in the tool. So, I reached back out to Jake, and we both thought the issue was a bug in the tool.

Jake then asked, “Why do you need to go via SVG?”. His question was spot on! The JavaScript input for the tool expects an easing function. An easing function maps time to a progress value. And we can get the easing functions straight out of GreenSock and pass them to the generator. Jake managed to dig the back easing function out of the GreenSock GitHub repo and create the easing I was originally after.

Generating GSAP Eases For CSS

Now that I’ve given you a bunch of context, we have all the parts of the puzzle we need to make something that can convert GSAP easing to CSS code.

First, we extract the parts from Jake’s linear() generator tool into a script. The idea is to loop over a set of keys and generate a block of CSS with linear() easings. GreenSock has a lovely utility method called parseEase. It takes a string and returns the easing function. The accepted strings are the GreenSock easing functions.

const ease = gsap.parseEase('power1.in')
ease(0.5) // === 0.25

As this loops over an object with different easing functions, we can pass them into the extracted code from the tool. We modify that extracted code to our needs.

const easings = ''
const simplified = 0.0025
const rounded = 4
const EASES = {
  'power-1--out': gsap.parseEase('power1.out')
  // Other eases
}
// Loop over the easing keys and generate results.
for (const key of Object.keys(EASES)) {
  // Pass the easing function through the linear-generator code.
  const result = processEase(key, EASES[key])
  const optimised = useOptimizedPoints(result.points, simplified, rounded)
  const linear = useLinearSyntax(optimised, rounded)
  const output = useFriendlyLinearCode(linear, result.name, 0)
  easings += output
}
// Generate an output CSS string.
let outputStart = ':root {'
let outputEnd = '}' 
let styles = ${outputStart}
  ${easings}
  ${outputEnd}
// Write it to the body.
document.body.innerHTML = styles

The functions we extracted from the linear generator do different things:

  • processEase
    This is a modified version of processScriptData. It takes the easing functions and returns points for our graph.
  • useOptimizedPoints
    This optimizes those points based on the simplied and rounded values. This was where I learned about the Douglas Peucker algorithm from Jake.
  • useLinearSyntax
    This takes the optimized points and returns the values for the linear() function.
  • useFriendlyLinearCode
    This takes the linear values and returns a CSS string that we can use with the ease’s custom property name.

It’s worth noting that I’ve tried not to touch these too much. But it’s also worth digging in and dropping in a breakpoint or console.info at various spots to understand how things are working.

After running things, the result gives us CSS variables containing the linear() easing functions and values. The following example shows the elastic and bounce eases.

:root {
  --elastic-in: linear( 0, 0.0019 13.34%, -0.0056 27.76%, -0.0012 31.86%, 0.0147 39.29%, 0.0161 42.46%, 0.0039 46.74%, -0.0416 54.3%, -0.046 57.29%, -0.0357, -0.0122 61.67%, 0.1176 69.29%, 0.1302 70.79%, 0.1306 72.16%, 0.1088 74.09%, 0.059 75.99%, -0.0317 78.19%, -0.3151 83.8%, -0.3643 85.52%, -0.3726, -0.3705 87.06%, -0.3463, -0.2959 89.3%, -0.1144 91.51%, 0.7822 97.9%, 1 );
  --elastic-out: linear( 0, 0.2178 2.1%, 1.1144 8.49%, 1.2959 10.7%, 1.3463 11.81%, 1.3705 12.94%, 1.3726, 1.3643 14.48%, 1.3151 16.2%, 1.0317 21.81%, 0.941 24.01%, 0.8912 25.91%, 0.8694 27.84%, 0.8698 29.21%, 0.8824 30.71%, 1.0122 38.33%, 1.0357, 1.046 42.71%, 1.0416 45.7%, 0.9961 53.26%, 0.9839 57.54%, 0.9853 60.71%, 1.0012 68.14%, 1.0056 72.24%, 0.9981 86.66%, 1 );
  --elastic-in-out: linear( 0, -0.0028 13.88%, 0.0081 21.23%, 0.002 23.37%, -0.0208 27.14%, -0.023 28.64%, -0.0178, -0.0061 30.83%, 0.0588 34.64%, 0.0651 35.39%, 0.0653 36.07%, 0.0514, 0.0184 38.3%, -0.1687 42.21%, -0.1857 43.04%, -0.181 43.8%, -0.1297 44.93%, -0.0201 46.08%, 1.0518 54.2%, 1.1471, 1.1853 56.48%, 1.1821 57.25%, 1.1573 58.11%, 0.9709 62%, 0.9458, 0.9347 63.92%, 0.9349 64.61%, 0.9412 65.36%, 1.0061 69.17%, 1.0178, 1.023 71.36%, 1.0208 72.86%, 0.998 76.63%, 0.9919 78.77%, 1.0028 86.12%, 1 );
    --bounce-in: linear( 0, 0.0117, 0.0156, 0.0117, 0, 0.0273, 0.0468, 0.0586, 0.0625, 0.0586, 0.0468, 0.0273, 0 27.27%, 0.1093, 0.1875 36.36%, 0.2148, 0.2343, 0.2461, 0.25, 0.2461, 0.2344, 0.2148 52.28%, 0.1875 54.55%, 0.1095, 0, 0.2341, 0.4375, 0.6092, 0.75, 0.8593, 0.9375 90.91%, 0.9648, 0.9843, 0.9961, 1 );
  --bounce-out: linear( 0, 0.0039, 0.0157, 0.0352, 0.0625 9.09%, 0.1407, 0.25, 0.3908, 0.5625, 0.7654, 1, 0.8907, 0.8125 45.45%, 0.7852, 0.7657, 0.7539, 0.75, 0.7539, 0.7657, 0.7852, 0.8125 63.64%, 0.8905, 1 72.73%, 0.9727, 0.9532, 0.9414, 0.9375, 0.9414, 0.9531, 0.9726, 1, 0.9883, 0.9844, 0.9883, 1 );
  --bounce-in-out: linear( 0, 0.0078, 0, 0.0235, 0.0313, 0.0235, 0.0001 13.63%, 0.0549 15.92%, 0.0938, 0.1172, 0.125, 0.1172, 0.0939 27.26%, 0.0554 29.51%, 0.0003 31.82%, 0.2192, 0.3751 40.91%, 0.4332, 0.4734 45.8%, 0.4947 48.12%, 0.5027 51.35%, 0.5153 53.19%, 0.5437, 0.5868 57.58%, 0.6579, 0.7504 62.87%, 0.9999 68.19%, 0.9453, 0.9061, 0.8828, 0.875, 0.8828, 0.9063, 0.9451 84.08%, 0.9999 86.37%, 0.9765, 0.9688, 0.9765, 1, 0.9922, 1 );
}

We’re able to adjust this output to our heart’s desire with different keys or accuracy. The really cool thing is that we can now drop these GreenSock eases into CSS!

How To Get Your Very Own CSS linear() Ease

Here’s a little tool I put together. It allows you to select the type of animation you want, apply a linear() ease to it, and determine its speed. From there, flip the card over to view and copy the generated code.

See the Pen GreenSock Easing with CSS linear() ⚡️ [forked] by Jhey.

In cases where linear() isn’t supported by a browser, we could use a fallback value for the ease using @supports:

:root {
  --ease: ease-in-out;
}
@supports(animation-timing-function: linear(0, 1)) {
  :root {
    --ease: var(--bounce-easing);
  }
}

And just for fun, here’s a demo that takes the GreenSock ease string as an input and gives you the linear() function back. Try something like elastic.out(1, 0.1) and see what happens!

See the Pen Convert GSAP Ease to CSS linear() [forked] by Jhey.

Bonus: Linear Eases For Tailwind

You don’t think we’d leave out those of you who use Tailwind, do you? Not a chance. In fact, extending Tailwind with our custom eases isn’t much trouble at all.

/** @type {import('tailwindcss').Config} */
const plugin = require('tailwindcss/plugin')
const EASES = {
  "power1-in": "linear( 0, 0.0039, 0.0156, 0.0352, 0.0625, 0.0977, 0.1407, 0.1914, 0.2499, 0.3164, 0.3906 62.5%, 0.5625, 0.7656, 1 )",
  /* Other eases */
}
const twease = plugin(
  function ({addUtilities, theme, e}) {
    const values = theme('transitionTimingFunction')
    var utilities = Object.entries(values).map(([key, value]) => {
      return {
        [.${e(animation-timing-${key})}]: {animationTimingFunction: ${value}},
      }
    })
    addUtilities(utilities)
  }
)
module.exports = {
  theme: {
    extend: {
      transitionTimingFunction: {
        ...EASES,
      }
    },
  },
  plugins: [twease],
}

I’ve put something together in Tailwind Play for you to see this in action and do some experimenting. This will give you classes like animation-timing-bounce-out and ease-bounce-out.

Conclusion

CSS has traditionally only provided limited control over the timing of animations and transitions. The only way to get the behavior we wanted was to reach for JavaScript solutions. Well, that’s soon going to change, thanks to the easing superpowers of the new linear() timing function that’s defined in the CSS Easing Level 2 draft specification. Be sure to drop those transitions into your demos, and I look forward to seeing what you do with them!

Stay awesome. ┬┴┬┴┤•ᴥ•ʔ├┬┴┬┴

Generating Real-Time Audio Sentiment Analysis With AI

In the previous article, we developed a sentiment analysis tool that could detect and score emotions hidden within audio files. We’re taking it to the next level in this article by integrating real-time analysis and multilingual support. Imagine analyzing the sentiment of your audio content in real-time as the audio file is transcribed. In other words, the tool we are building offers immediate insights as an audio file plays.

So, how does it all come together? Meet Whisper and Gradio — the two resources that sit under the hood. Whisper is an advanced automatic speech recognition and language detection library. It swiftly converts audio files to text and identifies the language. Gradio is a UI framework that happens to be designed for interfaces that utilize machine learning, which is ultimately what we are doing in this article. With Gradio, you can create user-friendly interfaces without complex installations, configurations, or any machine learning experience — the perfect tool for a tutorial like this.

By the end of this article, we will have created a fully-functional app that:

  • Records audio from the user’s microphone,
  • Transcribes the audio to plain text,
  • Detects the language,
  • Analyzes the emotional qualities of the text, and
  • Assigns a score to the result.

Note: You can peek at the final product in the live demo.

Automatic Speech Recognition And Whisper

Let’s delve into the fascinating world of automatic speech recognition and its ability to analyze audio. In the process, we’ll also introduce Whisper, an automated speech recognition tool developed by the OpenAI team behind ChatGPT and other emerging artificial intelligence technologies. Whisper has redefined the field of speech recognition with its innovative capabilities, and we’ll closely examine its available features.

Automatic Speech Recognition (ASR)

ASR technology is a key component for converting speech to text, making it a valuable tool in today’s digital world. Its applications are vast and diverse, spanning various industries. ASR can efficiently and accurately transcribe audio files into plain text. It also powers voice assistants, enabling seamless interaction between humans and machines through spoken language. It’s used in myriad ways, such as in call centers that automatically route calls and provide callers with self-service options.

By automating audio conversion to text, ASR significantly saves time and boosts productivity across multiple domains. Moreover, it opens up new avenues for data analysis and decision-making.

That said, ASR does have its fair share of challenges. For example, its accuracy is diminished when dealing with different accents, background noises, and speech variations — all of which require innovative solutions to ensure accurate and reliable transcription. The development of ASR systems capable of handling diverse audio sources, adapting to multiple languages, and maintaining exceptional accuracy is crucial for overcoming these obstacles.

Whisper: A Speech Recognition Model

Whisper is a speech recognition model also developed by OpenAI. This powerful model excels at speech recognition and offers language identification and translation across multiple languages. It’s an open-source model available in five different sizes, four of which have an English-only variant that performs exceptionally well for single-language tasks.

What sets Whisper apart is its robust ability to overcome ASR challenges. Whisper achieves near state-of-the-art performance and even supports zero-shot translation from various languages to English. Whisper has been trained on a large corpus of data that characterizes ASR’s challenges. The training data consists of approximately 680,000 hours of multilingual and multitask supervised data collected from the web.

The model is available in multiple sizes. The following table outlines these model characteristics:

Size Parameters English-only model Multilingual model Required VRAM Relative speed
Tiny 39 M tiny.en tiny ~1 GB ~32x
Base 74 M base.en base ~1 GB ~16x
Small 244 M small.en small ~2 GB ~6x
Medium 769 M medium.en medium ~5 GB ~2x
Large 1550 M N/A large ~10 GB 1x

For developers working with English-only applications, it’s essential to consider the performance differences among the .en models — specifically, tiny.en and base.en, both of which offer better performance than the other models.

Whisper utilizes a Seq2seq (i.e., transformer encoder-decoder) architecture commonly employed in language-based models. This architecture’s input consists of audio frames, typically 30-second segment pairs. The output is a sequence of the corresponding text. Its primary strength lies in transcribing audio into text, making it ideal for “audio-to-text” use cases.

Real-Time Sentiment Analysis

Next, let’s move into the different components of our real-time sentiment analysis app. We’ll explore a powerful pre-trained language model and an intuitive user interface framework.

Hugging Face Pre-Trained Model

I relied on the DistilBERT model in my previous article, but we’re trying something new now. To analyze sentiments precisely, we’ll use a pre-trained model called roberta-base-go_emotions, readily available on the Hugging Face Model Hub.

Gradio UI Framework

To make our application more user-friendly and interactive, I’ve chosen Gradio as the framework for building the interface. Last time, we used Streamlit, so it’s a little bit of a different process this time around. You can use any UI framework for this exercise.

I’m using Gradio specifically for its machine learning integrations to keep this tutorial focused more on real-time sentiment analysis than fussing with UI configurations. Gradio is explicitly designed for creating demos just like this, providing everything we need — including the language models, APIs, UI components, styles, deployment capabilities, and hosting — so that experiments can be created and shared quickly.

Initial Setup

It’s time to dive into the code that powers the sentiment analysis. I will break everything down and walk you through the implementation to help you understand how everything works together.

Before we start, we must ensure we have the required libraries installed and they can be installed with npm. If you are using Google Colab, you can install the libraries using the following commands:

!pip install gradio
!pip install transformers
!pip install git+https://github.com/openai/whisper.git

Once the libraries are installed, we can import the necessary modules:

import gradio as gr
import whisper
from transformers import pipeline

This imports Gradio, Whisper, and pipeline from Transformers, which performs sentiment analysis using pre-trained models.

Like we did last time, the project folder can be kept relatively small and straightforward. All of the code we are writing can live in an app.py file. Gradio is based on Python, but the UI framework you ultimately use may have different requirements. Again, I’m using Gradio because it is deeply integrated with machine learning models and APIs, which is ideal for a tutorial like this.

Gradio projects usually include a requirements.txt file for documenting the app, much like a README file. I would include it, even if it contains no content.

To set up our application, we load Whisper and initialize the sentiment analysis component in the app.py file:

model = whisper.load_model("base")

sentiment_analysis = pipeline(
  "sentiment-analysis",
  framework="pt",
  model="SamLowe/roberta-base-go_emotions"
)

So far, we’ve set up our application by loading the Whisper model for speech recognition and initializing the sentiment analysis component using a pre-trained model from Hugging Face Transformers.

Defining Functions For Whisper And Sentiment Analysis

Next, we must define four functions related to the Whisper and pre-trained sentiment analysis models.

Function 1: analyze_sentiment(text)

This function takes a text input and performs sentiment analysis using the pre-trained sentiment analysis model. It returns a dictionary containing the sentiments and their corresponding scores.

def analyze_sentiment(text):
  results = sentiment_analysis(text)
  sentiment_results = {
    result[’label’]: result[’score’] for result in results
  }
return sentiment_results

Function 2: get_sentiment_emoji(sentiment)

This function takes a sentiment as input and returns a corresponding emoji used to help indicate the sentiment score. For example, a score that results in an “optimistic” sentiment returns a “😊” emoji. So, sentiments are mapped to emojis and return the emoji associated with the sentiment. If no emoji is found, it returns an empty string.

def get_sentiment_emoji(sentiment):
  # Define the mapping of sentiments to emojis
  emoji_mapping = {
    "disappointment": "😞",
    "sadness": "😢",
    "annoyance": "😠",
    "neutral": "😐",
    "disapproval": "👎",
    "realization": "😮",
    "nervousness": "😬",
    "approval": "👍",
    "joy": "😄",
    "anger": "😡",
    "embarrassment": "😳",
    "caring": "🤗",
    "remorse": "😔",
    "disgust": "🤢",
    "grief": "😥",
    "confusion": "😕",
    "relief": "😌",
    "desire": "😍",
    "admiration": "😌",
    "optimism": "😊",
    "fear": "😨",
    "love": "❤️",
    "excitement": "🎉",
    "curiosity": "🤔",
    "amusement": "😄",
    "surprise": "😲",
    "gratitude": "🙏",
    "pride": "🦁"
  }
return emoji_mapping.get(sentiment, "")

Function 3: display_sentiment_results(sentiment_results, option)

This function displays the sentiment results based on a selected option, allowing users to choose how the sentiment score is formatted. Users have two options: show the score with an emoji or the score with an emoji and the calculated score. The function inputs the sentiment results (sentiment and score) and the selected display option, then formats the sentiment and score based on the chosen option and returns the text for the sentiment findings (sentiment_text).

def display_sentiment_results(sentiment_results, option):
sentiment_text = ""
for sentiment, score in sentiment_results.items():
  emoji = get_sentiment_emoji(sentiment)
  if option == "Sentiment Only":
    sentiment_text += f"{sentiment} {emoji}\n"
  elif option == "Sentiment + Score":
    sentiment_text += f"{sentiment} {emoji}: {score}\n"
return sentiment_text

Function 4: inference(audio, sentiment_option)

This function performs Hugging Face’s inference process, including language identification, speech recognition, and sentiment analysis. It inputs the audio file and sentiment display option from the third function. It returns the language, transcription, and sentiment analysis results that we can use to display all of these in the front-end UI we will make with Gradio in the next section of this article.

def inference(audio, sentiment_option):
  audio = whisper.load_audio(audio)
  audio = whisper.pad_or_trim(audio)

  mel = whisper.log_mel_spectrogram(audio).to(model.device)

  _, probs = model.detect_language(mel)
  lang = max(probs, key=probs.get)

  options = whisper.DecodingOptions(fp16=False)
  result = whisper.decode(model, mel, options)

  sentiment_results = analyze_sentiment(result.text)
  sentiment_output = display_sentiment_results(sentiment_results, sentiment_option)

return lang.upper(), result.text, sentiment_output
Creating The User Interface

Now that we have the foundation for our project — Whisper, Gradio, and functions for returning a sentiment analysis — in place, all that’s left is to build the layout that takes the inputs and displays the returned results for the user on the front end.

The following steps I will outline are specific to Gradio’s UI framework, so your mileage will undoubtedly vary depending on the framework you decide to use for your project.

Defining The Header Content

We’ll start with the header containing a title, an image, and a block of text describing how sentiment scoring is evaluated.

Let’s define variables for those three pieces:

title = """🎤 Multilingual ASR 💬"""
image_path = "/content/thumbnail.jpg"

description = """
  💻 This demo showcases a general-purpose speech recognition model called Whisper. It is trained on a large dataset of diverse audio and supports multilingual speech recognition and language identification tasks.

📝 For more details, check out the [GitHub repository](https://github.com/openai/whisper).

⚙️ Components of the tool:

     - Real-time multilingual speech recognition
     - Language identification
     - Sentiment analysis of the transcriptions

🎯 The sentiment analysis results are provided as a dictionary with different emotions and their corresponding scores.

😃 The sentiment analysis results are displayed with emojis representing the corresponding sentiment.

✅ The higher the score for a specific emotion, the stronger the presence of that emotion in the transcribed text.

❓ Use the microphone for real-time speech recognition.

⚡️ The model will transcribe the audio and perform sentiment analysis on the transcribed text.
"""

Applying Custom CSS

Styling the layout and UI components is outside the scope of this article, but I think it’s important to demonstrate how to apply custom CSS in a Gradio project. It can be done with a custom_css variable that contains the styles:

custom_css = """
  #banner-image {
    display: block;
    margin-left: auto;
    margin-right: auto;
  }
  #chat-message {
    font-size: 14px;
    min-height: 300px;
  }
"""

Creating Gradio Blocks

Gradio’s UI framework is based on the concept of blocks. A block is used to define layouts, components, and events combined to create a complete interface with which users can interact. For example, we can create a block specifically for the custom CSS from the previous step:

block = gr.Blocks(css=custom_css)

Let’s apply our header elements from earlier into the block:

block = gr.Blocks(css=custom_css)

with block:
  gr.HTML(title)

with gr.Row():
  with gr.Column():
    gr.Image(image_path, elem_id="banner-image", show_label=False)
  with gr.Column():
    gr.HTML(description)

That pulls together the app’s title, image, description, and custom CSS.

Creating The Form Component

The app is based on a form element that takes audio from the user’s microphone, then outputs the transcribed text and sentiment analysis formatted based on the user’s selection.

In Gradio, we define a Group() containing a Box() component. A group is merely a container to hold child components without any spacing. In this case, the Group() is the parent container for a Box() child component, a pre-styled container with a border, rounded corners, and spacing.

with gr.Group():
  with gr.Box():

With our Box() component in place, we can use it as a container for the audio file form input, the radio buttons for choosing a format for the analysis, and the button to submit the form:

with gr.Group():
  with gr.Box():
    # Audio Input
    audio = gr.Audio(
      label="Input Audio",
      show_label=False,
      source="microphone",
      type="filepath"
    )

    # Sentiment Option
    sentiment_option = gr.Radio(
      choices=["Sentiment Only", "Sentiment + Score"],
      label="Select an option",
      default="Sentiment Only"
    )

    # Transcribe Button
    btn = gr.Button("Transcribe")

Output Components

Next, we define Textbox() components as output components for the detected language, transcription, and sentiment analysis results.

lang_str = gr.Textbox(label="Language")
text = gr.Textbox(label="Transcription")
sentiment_output = gr.Textbox(label="Sentiment Analysis Results", output=True)

Button Action

Before we move on to the footer, it’s worth specifying the action executed when the form’s Button() component — the "Transcribe" button — is clicked. We want to trigger the fourth function we defined earlier, inference(), using the required inputs and outputs.

btn.click(
  inference,
  inputs=[
    audio,
    sentiment_option
  ],
  outputs=[
    lang_str,
    text,
    sentiment_output
  ]
)

Footer HTML

This is the very bottom of the layout, and I’m giving OpenAI credit with a link to their GitHub repository.

gr.HTML(’’’
  <div class="footer">
    <p>Model by <a href="https://github.com/openai/whisper" style="text-decoration: underline;" target="_blank">OpenAI</a>
    </p>
  </div>
’’’)

Launch the Block

Finally, we launch the Gradio block to render the UI.

block.launch()
Hosting & Deployment

Now that we have successfully built the app’s UI, it’s time to deploy it. We’ve already used Hugging Face resources, like its Transformers library. In addition to supplying machine learning capabilities, pre-trained models, and datasets, Hugging Face also provides a social hub called Spaces for deploying and hosting Python-based demos and experiments.

You can use your own host, of course. I’m using Spaces because it’s so deeply integrated with our stack that it makes deploying this Gradio app a seamless experience.

In this section, I will walk you through Space’s deployment process.

Creating A New Space

Before we start with deployment, we must create a new Space.

The setup is pretty straightforward but requires a few pieces of information, including:

  • A name for the Space (mine is “Real-Time-Multilingual-sentiment-analysis”),
  • A license type for fair use (e.g., a BSD license),
  • The SDK (we’re using Gradio),
  • The hardware used on the server (the “free” option is fine), and
  • Whether the app is publicly visible to the Spaces community or private.

Once a Space has been created, it can be cloned, or a remote can be added to its current Git repository.

Deploying To A Space

We have an app and a Space to host it. Now we need to deploy our files to the Space.

There are a couple of options here. If you already have the app.py and requirements.txt files on your computer, you can use Git from a terminal to commit and push them to your Space by following these well-documented steps. Or, If you prefer, you can create app.py and requirements.txt directly from the Space in your browser.

Push your code to the Space, and watch the blue “Building” status that indicates the app is being processed for production.

Final Demo

Conclusion

And that’s a wrap! Together, we successfully created and deployed an app capable of converting an audio file into plain text, detecting the language, analyzing the transcribed text for emotion, and assigning a score that indicates that emotion.

We used several tools along the way, including OpenAI’s Whisper for automatic speech recognition, four functions for producing a sentiment analysis, a pre-trained machine learning model called roberta-base-go_emotions that we pulled from the Hugging Space Hub, Gradio as a UI framework, and Hugging Face Spaces to deploy the work.

How will you use these real-time, sentiment-scoping capabilities in your work? I see so much potential in this type of technology that I’m interested to know (and see) what you make and how you use it. Let me know in the comments!

Further Reading On SmashingMag

Falling For Oklch: A Love Story Of Color Spaces, Gamuts, And CSS

I woke up one morning in early 2022 and caught an article called “A Whistle-Stop Tour of 4 New CSS Color Features” over at CSS-Tricks.

Wow, what a gas! A new and wider color gamut! New color spaces! New color functions! New syntaxes! It is truly a lot to take in.

Now, I’m no color expert. But I enjoyed adding new gems to my CSS toolbox and made a note to come back to that article later for a deeper read. That, of course, led to a lot of fun rabbit holes that helped put the CSS Color Module Level 4 updates in a better context for me.

That’s where Oklch comes into the picture. It’s a new color space in CSS that, according to experts smarter than me, offers upwards of 50% more color than the sRGB gamut we have worked with for so long because it supports a wider gamut of color.

Color spaces? Gamuts? These are among many color-related terms I’m familiar with but have never really understood. It’s only now that my head is wrapping around these concepts and how they relate back to CSS, and how I use color in my own work.

That’s what I want to share with you. This article is less of a comprehensive “how-to” guide than it is my own personal journey grokking new CSS color features. I actually like to this of this more as a “love story” where I fall for Oklch.

The Deal With Gamuts And Color Spaces

I quickly learned that there’s no way to understand Oklch without at least a working understanding of the difference between gamuts and color spaces. My novice-like brain thinks of them as the same: a spectrum of colors. In fact, my mind goes straight to the color pickers we all know from apps like Figma and Sketch.

I’ve always assumed that gamut is just a nerdier term for the available colors in a color picker and that a color picker is simply a convenient interface for choosing colors in the gamut.

(Assumed. Just. Simply. Three words you never want to see in the same sentence.)

Apparently not. A gamut really boils down to a range of something, which in this case, is a range of colors. That range might be based on a single point if we think of it on a single axis.

Or it might be a range of multiple coordinates like we would see on a two-axe grid. Now the gamut covers a wider range that originates from the center and can point in any direction.

The levels of those ranges can also constitute an axis, which results in some form of 3D space.

sRGB is a gamut with an available range of colors. Display P3 is another gamut offering a wider range of colors.

So, gamuts are ranges, and ranges need a reference to determine the upper and lower limits of those axes. That’s where we start talking about color spaces. A color space is what defines the format for plotting points on the gamut. While more trained folks certainly have more technical explanations, my basic understanding of color spaces is that they provide the map — or perhaps the “shape” — for the gamut and define how color is manipulated in it. So, sRGB is a color gamut that spans a range of colors, and Hex, RGB, and HSL (among others, of course) are the spaces we have to explore the gamut.

That’s why you may hear a color space as having a “wider” or “narrower” gamut than another — it’s a range of possibilities within a shape.

If I’ve piqued your interest enough, I’ve compiled a list of articles that will give you more thorough definitions of gamuts and color spaces at the end of this article.

Why We Needed New Color Spaces

The short answer is that the sRGB gamut serves as the reference point for color spaces like Hex, RGB, and HSL that provide a narrower color gamut than what is available in the newer Display P3 gamut.

We’re well familiar with many of sRGB-based color notations and functions in CSS. The values are essentially setting points along the gamut space with different types of coordinates.

  /* Hex */ #f8a100
  /* RGB */ rgb(248, 161, 2)
  /* HSL */ hsl(38.79 98% 49%)

For example, the rgb() function is designed to traverse the RGB color space by mixing red, blue, and green values to produce a point along the sRGB gamut.

If the difference between the two ranges in the image above doesn’t strike you as particularly significant or noticeable, that’s fair. I thought they were the same at first. But the Display P3 stripe is indeed a wider and smoother range of colors than the sRGB stripe above it when you examine it up close.

The problem is that Hex, RGB, and HSL (among other existing spaces) only support the sRGB gamut. In other words, they are unable to map colors outside of the range of colors that sRGB offers. That means there’s no way to map them to colors in the Display P3 gamut. The traditional color formats we’ve used for a long time are simply incompatible with the range of colors that has started rolling out in new hardware. We needed a new space to accommodate the colors that new technology is offering us.

Dead Grey Zones

I love this term. It accurately describes an issue with the color spaces in the sRGB gamut — greyish areas between two color points. You can see it in the following demo.

Oklch (as well as the other new spaces in the Level 4 spec) doesn’t have that issue. Hues are more like mountains, each with a different elevation.

That’s why we needed new color spaces — to get around those dead grey zones. And we needed new color functions in CSS to produce coordinates on the space to select from the newly available range of colors.

But there’s a catch. That mountain-shaped gamut of Oklch doesn’t always provide a straight path between color points which could result in clipped or unexpected colors between points. The issue appears to be case-specific depending on the colors in use, but that also seems to indicate that there are situations where using a different color space is going to yield better gradients.

Consistent Lightness

It’s the consistent range of saturation in HSL muddying the waters that leads to another issue along this same train of thought: inconsistent levels of lightness between colors.

The classic example is showing two colors in HSL with the same lightness value:

The Oklab and Oklch color spaces were created to fix that shift. Black is more, well, black because the hues are more consistent in Oklab and Oklch than they are in LAB and LCH.

So, that’s why it’s likely better to use the oklch() and oklab() functions in CSS than it is to use their lch() and lab() counterparts. There’s less of a shift happening in the hues.

So, while Oklch/LCH and Oklab/LAB all use the same general color space, the Cartesian coordinates are the key difference. And I agree with Sitnik and Turner, who make the case that Oklch and LCH are easier to understand than LAB and Oklab. I wouldn’t be able to tell you the difference between LAB’s a and b values on the Cartesian coordinate system. But chroma and hue in LCH and Oklch? Sure! That’s as easy to understand as HSL but better!

The reason I love Oklch over Oklab is that lightness, chroma, and hue are much more intuitive to me than lightness and a pair of Cartesian coordinates.

And the reason I like Oklch better than HSL is because it produces more consistent results over a wider color gamut.

OKLCH And CSS

This is why you’re here, right? What’s so cool about all this is that we can start using Oklch in CSS today — there’s no need to wait around.

“Browser support?” you ask. We’re well covered, friends!

In fact, Firefox 113 shipped support for Oklch a mere ten days before I started writing the first draft of this article. It’s oven fresh!

Using oklch() is a whole lot easier to explain now that we have all the context around color spaces and gamuts and how the new CSS Color Module Level 4 color functions fit into the picture.

I think the most difficult thing for me is working with different ranges of values. For example, hsl() is easy for me to remember because the hue is measured in degrees, and both saturation and lightness use the same 0% to 100% range.

oklch() is different, and that’s by design to not only access the wider gamut but also produce perceptively consistent results even as values change. So, while we get what I’m convinced is a way better tool for specifying color in CSS, there is a bit of a learning curve to remembering the chroma value because it’s what separates OKLCH from HSL.

The oklch() Values

Here they are:

  • l: This controls the lightness of the color, and it’s measured in a range of 0% to 100% just like HSL.
  • c: This is the chroma value, measured in decimals between 0 and 0.37.
  • h: This is the same ol’ hue we have in HSL, measured in the same range of 0deg to 360deg.

Again, it’s chroma that is the biggest learning curve for me. Yes, I had to look it up because I kept seeing it used somewhat synonymously with saturation.

Chroma and saturation are indeed different. And there are way better definitions of them out there than what I can provide. For example, I like how Cameron Chapman explains it:

“Chroma refers to the purity of a color. A hue with high chroma has no black, white, or gray added to it. Conversely, adding white, black, or gray reduces its chroma. It’s similar to saturation but not quite the same. Chroma can be thought of as the brightness of a color in comparison to white.”

— Cameron Chapman

I mentioned that chroma has an upper limit of 0.37. But it’s actually more nuanced than that, as Sitnik and Turner explain:

“[Chroma] goes from 0 (gray) to infinity. In practice, there is actually a limit, but it depends on a screen’s color gamut (P3 colors will have bigger values than sRGB), and each hue has a different maximum chroma. For both P3 and sRGB, the value will always be below 0.37.”

— Andrey Sitnik and Travis Turner

I’m so glad there are smart people out there to help sort this stuff out.

The oklch() Syntax

The formal syntax? Here it is, straight from the spec:

oklab() = oklab( [ <percentage> | <number> | none]
    [ <percentage> | <number> | none]
    [ <percentage> | <number> | none]
    [ / [<alpha-value> | none] ]? )

Maybe we can “dumb” it down a bit:

oklch( [ lightness ] [ chroma ] [ hue ] )

And those values, again, are measured in different units:

oklch( [ lightness = <percentage> ] [ chroma <number> ] [ hue <degrees> ]  )

Those units have min and max limits:

oklch( [ lightness = <percentage (0%-100%)> ] [ chroma <number> (0-0.37) ] [ hue <degrees> (0deg-360deg) ]  )

An example might be the following:

color: oklch(70.9% 0.195 47.025);

Did you notice that there are no commas between values? Or that there is no unit on the hue? That’s thanks to the updated syntax defined in the CSS Color Module Level 4 spec. It also applies to functions in the sRGB gamut:

/* Old Syntax */
hsl(26.06deg, 99%, 51%)

/* New Syntax */
hsl(26.06 99% 51%)

Something else that’s new? There’s no need for a separate function to set alpha transparency! Instead, we can indicate that with a / before the alpha value:

/* Old Syntax */
hsla(26.06deg, 99%, 51%, .75)

/* New Syntax */
hsl(26.06 99% 51% / .75)

That’s why there is no oklcha() function — the new syntax allows oklch() to handle transparency on its own, like a grown-up.

Providing A Fallback

Yeah, it’s probably worth providing a fallback value for oklch() even if it does enjoy great browser support. Maybe you have to support a legacy browser like IE, or perhaps the user’s monitor or screen simply doesn’t support colors in the Display P3 gamut.

Providing a fallback doesn’t have to be hard:

color: hsl(26.06 99% 51%);
color: oklch(70.9% 0.195 47.025);

There are “smarter” ways to provide a fallback, like, say, using @supports:

.some-class {
  color: hsl(26.06 99% 51%);
}

@supports (oklch(100% 0 0)) {
  .some-class {
    color: oklch(70.9% 0.195 47.025);
  }
}

Or detecting Display P3 support on the @media side of things:

.some-class {
  color: hsl(26.06 99% 51%);
}

@media (color-gamut: p3) {
  .some-class {
    color: oklch(70.9% 0.195 47.025);
  }
}

Those all seem overly verbose compared to letting the cascade do the work. Maybe there’s a good reason for using media queries that I’m overlooking.

There’s A Polyfill

Of course, there’s one! There are two, in fact, that I am aware of: postcss-oklab-function and color.js. The PostCSS plugin will preprocess support for you when compiling to CSS. Alternatively, color.js will convert it on the client side.

That’s Oklch 🥰

O, Oklch! How much do I love thee? Let me count the ways:

  • You support a wider gamut of colors that make my designs pop.
  • Your space transitions between colors smoothly, like soft butter.
  • You are as easy to understand as my former love, HSL.
  • You are well-supported by all the major browsers.
  • You provide fallbacks for handling legacy browsers that will never have the pleasure of knowing you.

I know, I know. Get a room, right?!

Resources

How to Create Special Text Areas with CSS in WordPress Post

Would you like to draw readers’ attention to certain text in your blog posts? One effective way to achieve this is by creating eye-catching and stylish text boxes. In this tutorial, I’ll show you how to easily create text boxes using CSS. We’ll create two classes, right_box and left_box, which will float the text boxes […]

Designing Accessible Text Over Images: Best Practices, Techniques And Resources (Part 2)

What is the text over images design pattern? How do we apply this pattern to our designs without sacrificing legibility and readability?

The text over images design pattern is a design technique used to place text on top of images. It is often used to provide information about the image or to serve as the main website navigation. However, this technique can quickly sacrifice legibility and readability if there is not enough contrast between the text and the image. To prevent this, designers need to ensure that the text and the image have a high enough contrast ratio to be legible and readable. Additionally, designers should also make sure the text is positioned in the right place, away from any image elements that might cause confusion, distraction, or make it difficult to read.

“Incorporating text with imagery is a balancing act. To create professional, compelling content, the image and text must reach a visual harmony. At the same time, strong contrast between text and image will increase legibility and will make your content stand out.”

— “Tips for Overlaying Text on Imagery,” Getty Images

In Part 1 of the series, we have reviewed in detail five techniques (using an overlay over the entire image, text with scrim overlay, strips/highlight, copy space, and text over blurred background effect) and now we’ll continue with reviewing in detail five more (frame the image, soft-colored gradients, text styles and text position, solid color shapes, use of colored backgrounds). In the end, I will also provide you with a long list of useful tools and resources related to this accessibility topic.

Frame That Image

Another simple design technique you can try is by framing the image in a flat-colored shape that includes your text. This kind of style is mostly used in thumbnails and cards.

Examples From The Wild

Additional Resources On This Topic

  • Bento Grids
    Bento Grids is a nice curated collection of tiles-based layouts (that were initially popularized by Apple). The main idea behind this is to present the key takeaways in a visual and easy-to-consume way. Bento layouts are great for showcasing brand identity, summarizing product features, and much more.
  • Godly → Websites → Grid style
    A large collection of some of the best grid-style websites.
Soft-colored Gradients Technique Over Images

When black or white gradients don’t work well, you can use the soft-colored gradients text over image technique. These soft-colored gradients are created when two or more different colors are blended to create a soft and gentle transition from one color to another. They are commonly used on websites and page designs to make them look modern and creative.

Examples From The Wild

Additional resources on this topic

Play Around With Text Styles And Text Position

Achieving the 1.4.3 success criterion might be difficult even if we have used some of the techniques outlined in the examples above, or when any combination of those techniques still fails. In such cases, one of the safest options is to play around with text styles and with the text position outside of the image.

Various text styles (bigger or smaller text; emphasize, low-key, bold, regular, or light text style; playing with margins and letter spacing, etc.) and combining these text styles in different ways may help you achieve a powerful impact with regards to your design while not sacrificing any accessibility. You can also position your text to the left or right, the top or bottom, and you’ll have an accessible and visually appealing website or app.

This technique is great if you combine your text styling techniques and play around with the images. With text positioned outside of the image, you have control over how to make it more accessible by using real text that can be zoomed in to maximum for those people who may have trouble reading small text on the screen or for those who wish to use their voice assistants.

Examples From The Wild

Additional Resources On This Topic

Play With Solid Shapes

Using only simple shapes can make a lot of difference. Play around with solid shapes by creating strong harmony between the color of your text and the background colored shape. Once you learn how to balance the text styles and the colors, the readability of the text will greatly improve.

Examples From The Wild

Additional Resources On This Topic

Ditch The Image And Just Use Colored Backgrounds

In case you cannot fulfill the 1.4.3 success criterion, you have the option of replacing images with colored (uniform color) or gradient (two or more colors) backgrounds. This technique facilitates screen readers to read actual text instead of images, hence enhancing accessibility for visually impaired users. Furthermore, using this technique, you can adapt the text to meet your user's preferences, optimize it for various screen sizes, and even adjust the size or zoom level without compromising its quality.

Furthermore, with this technique, you can easily customize real text according to the user’s requirements, such as colors or styles, and much more. It might be difficult to achieve this criterion in certain circumstances, but giving users an option to customize those things instead of using images would be better for everyone involved, and that is why this technique does not require much effort on your part at all.

Examples From The Wild

Additional Resources On This Topic

Just Use The Actual Text!

Lastly, while all of these design techniques will help you make the text over images more accessible, I still think that using the actual text is the way to go.

Providing special care to ensure that text over an image remains perfectly readable and accessible is a must, and, as you have seen in sections 110 of the article, there are plenty of design techniques for that purpose. But again, if you want to make your website or mobile app accessible right from the start, why not simplify things a bit and do it properly? Use real text, make sure there is plenty of contrast between the text and the background, and make your website or app accessible to everyone.

Using text over images provides a combination of benefits, and yet it has some limitations. Text placed over images is harder to read for visually impaired users, especially at smaller text sizes, because the content gets more compressed. There are also some accessibility requirements for different colors under the WCAG 2.1 guidelines. If you have trouble implementing the success criterion mentioned in the design techniques we’ve presented, ditching everything and just using real text will do the trick.

Conclusion

As with any new design trends popping out in different places, we need to make sure that what we’re creating is not only pretty but is also helping our users. Always consider the accessibility aspect to be “baked in” right from the start rather than being an afterthought in your design process.

In any case, if someone asks you why the real text is better to use rather than text over images or images of text, here are a few key things to remember:

  • Real text can be zoomed in to any size without distortion and pixelation, and (what’s also very important) it can be read by assistive tech software.
  • Additionally, you can easily increase the contrast of the text, which will help your users access the content easier.
  • With actual text, you have the freedom to create your own styling and make use of CSS to format the text elements. I can highly recommend you to read the very detailed hands-on tutorial on how to use CSS styling, “Handling Text Over Images in CSS” by Ahmad Shadeed.

Thank you for joining me in this accessibility journey. We covered many design techniques that will hopefully help you work better with accessible text over images. And if you have some additional tips or advice to share — please do so in the comment section below, or ping me on Twitter (@humbleuidesigns)!

Further Reading: Tools & Resources

Useful Accessibility Tools

Accessible Carousels

Accessible Images

Accessible Text over Images through CSS and HTML

Accessible Text and Typography

Guides for Accessible Documentation And Annotations

WCAG Reference

Working with Color

  • The psychology behind shapes and colors,” by Rob Postema (UX Collective)
    Design influences the way we perceive the world, the way we feel, and the choices we make. To communicate to your target audience effectively as a designer, having knowledge of the psychological principles of human behavior (revolving around the use of shapes, colors, typography, and compositions) can be very helpful.
  • Apply color theory to your designs,” by Pranav Ambwani (UX Collective)
    Color is a very strong tool that we can apply to solve many design challenges. Since color plays such a major role in shaping the aesthetics and usability of websites, changing a single color can change a user’s perception of the same design.
  • Your ultimate guide to background design” (Canva Design)
    The background design you choose can dramatically change your design and make your graphics feel complete. Colors can be used as overlays to enhance brand awareness amongst your audience, while images don’t need to just sit alongside your graphic elements — they make for excellent backgrounds when placed correctly. Backgrounds are the backbone of great design!
  • Tips for Overlaying Text on Imagery” (Getty Images)
    Pay attention to color, contrast, and brightness; blur the imagery; weigh your text correctly; put more thought into your image; utilize the image’s perspective to your advantage — this and several other tips, combined with some examples, are shared in this concise and practical article.

Color Contrast and Accessibility (Smashing Magazine)

15 Best CSS Tools for Perfection

CSS (Cascading Style Sheets) is a fundamental language used for styling webpages. To make the most of your CSS development, having the right tools is essential. Here, we present the 15 best CSS tools that can enhance your workflow and productivity. 1. CodePen 2. Sass 3. PostCSS 4. Tailwind CSS 5. CSS Grid Generator Certainly! […]

How We Optimized Performance To Serve A Global Audience

I work for Bookaway, a digital travel brand. As an online booking platform, we connect travelers with transport providers worldwide, offering bus, ferry, train, and car transfers in over 30 countries. We aim to eliminate the complexity and hassle associated with travel planning by providing a one-stop solution for all transportation needs.

A cornerstone of our business model lies in the development of effective landing pages. These pages serve as a pivotal tool in our digital marketing strategy, not only providing valuable information about our services but also designed to be easily discoverable through search engines. Although landing pages are a common practice in online marketing, we were trying to make the most of it.

SEO is key to our success. It increases our visibility and enables us to draw a steady stream of organic (or “free”) traffic to our site. While paid marketing strategies like Google Ads play a part in our approach as well, enhancing our organic traffic remains a major priority. The higher our organic traffic, the more profitable we become as a company.

We’ve known for a long time that fast page performance influences search engine rankings. It was only in 2020, though, that Google shared its concept of Core Web Vitals and how it impacts SEO efforts. Our team at Bookaway recently underwent a project to improve Web Vitals, and I want to give you a look at the work it took to get our existing site in full compliance with Google’s standards and how it impacted our search presence.

SEO And Web Vitals

In the realm of search engine optimization, performance plays a critical role. As the world’s leading search engine, Google is committed to delivering the best possible search results to its users. This commitment involves prioritizing websites that offer not only relevant content but also an excellent user experience.

Google’s Core Web Vitals is a set of performance metrics that site owners can use to evaluate performance and diagnose performance issues. These metrics provide a different perspective on user experience:

  • Largest Contentful Paint (LCP)
    Measures the time it takes for the main content on a webpage to load.
  • First Input Delay (FID)
    Assesses the time it takes for a page to become interactive.
    Note: Google plans to replace this metric with another one called Interaction to Next Paint (INP) beginning in 2024.
  • Cumulative Layout Shift (CLS)
    Calculates the visual stability of a page.

While optimizing for FID and CLS was relatively straightforward, LCP posed a greater challenge due to the multiple factors involved. LCP is particularly vital for landing pages, which are predominantly content and often the first touch-point a visitor has with a website. A low LCP ensures that visitors can view the main content of your page sooner, which is critical for maintaining user engagement and reducing bounce rates.

Largest Contentful Paint (LCP)

LCP measures the perceived load speed of a webpage from a user’s perspective. It pinpoints the moment during a page’s loading phase when the primary — or “largest” — content has been fully rendered on the screen. This could be an image, a block of text, or even an embedded video. LCP is an essential metric because it gives a real-world indication of the user experience, especially for content-heavy sites.

However, achieving a good LCP score is often a multi-faceted process that involves optimizing several stages of loading and rendering. Each stage has its unique challenges and potential pitfalls, as other case studies show.

Here’s a breakdown of the moving pieces.

Time To First Byte (TTFB)

This is the time it takes for the first piece of information from the server to reach the user’s browser. You need to beware that slow server response times can significantly increase TTFB, often due to server overload, network issues, or un-optimized logic on the server side.

Download Time of HTML

This is the time it takes to download the page’s HTML file. You need to beware of large HTML files or slow network connections because they can lead to longer download times.

HTML Processing

Once a web page’s HTML file has been downloaded, the browser begins to process the contents line by line, translating code into the visual website that users interact with. If, during this process, the browser encounters a <script> or <style> tag that lacks either an async or deferred attribute, the rendering of the webpage comes to a halt.

The browser must then pause to fetch and parse the corresponding files. These files can be complex and potentially take a significant amount of time to download and interpret, leading to a noticeable delay in the loading and rendering of the webpage. This is why the async and deferred attributes are crucial, as they ensure an efficient, seamless web browsing experience.

Fetching And Decoding Images

This is the time taken to fetch, download, and decode images, particularly the largest contentful image. You need to look out for large image file sizes or improperly optimized images that can delay the fetching and decoding process.

First Contentful Paint (FCP)

This is the time it takes for the browser to render the first bit of content from the DOM. You need to beware of slow server response times, particularly render-blocking JavaScript or CSS, or slow network connections, all of which can negatively affect FCP.

Rendering the Largest Contentful Element

This is the time taken until the largest contentful element (like a hero image or heading text) is fully rendered on the page. You need to watch out for complex design elements, large media files, or slow browser rendering can delay the time it takes for the largest contentful element to render.

Understanding and optimizing each of these stages can significantly improve a website’s LCP, thereby enhancing the user experience and SEO rankings.

I know that is a lot of information to unpack in a single sitting, and it definitely took our team time to wrap our minds around what it takes to achieve a low LCP score. But once we had a good understanding, we knew exactly what to look for and began analyzing the analytics of our user data to identify areas that could be improved.

Analyzing User Data

To effectively monitor and respond to our website’s performance, we need a robust process for collecting and analyzing this data.

Here’s how we do it at Bookaway.

Next.js For Performance Monitoring

Many of you reading this may already be familiar with Next.js, but it is a popular open-source JavaScript framework that allows us to monitor our website’s performance in real-time.

One of the key Next.js features we leverage is the reportWebVitals function, a hook that allows us to capture the Web Vitals metrics for each page load. We can then forward this data to a custom analytics service. Most importantly, the function provides us with in-depth insights into our user experiences in real-time, helping us identify any performance issues as soon as they arise.

Storing Data In BigQuery For Comprehensive Analysis

Once we capture the Web Vitals metrics, we store this data in BigQuery, Google Cloud’s fully-managed, serverless data warehouse. Alongside the Web Vitals data, we also record a variety of other important details, such as the date of the page load, the route, whether the user was on a mobile or desktop device, and the language settings. This comprehensive dataset allows us to examine our website’s performance from multiple angles and gain deeper insights into the user experience.

The screenshot features an SQL query from a data table, focusing on the LCP web vital. It shows the retrieval of LCP values (in milliseconds) for specific visits across three unique page URLs that, in turn, represent three different landing pages we serve:

These values indicate how quickly major content items on these pages become fully visible to users.

Visualizing Data with Looker Studio

We visualize performance data using Google’s Looker Studio (formerly called Data Studio). By transforming our raw data into interactive dashboards and reports, we can easily identify trends, pinpoint issues, and monitor improvements over time. These visualizations empower us to make data-driven decisions that enhance our website’s performance and, ultimately, improve our users’ experience.

Looker Studio offers a few key advantages:

  • Easy-to-use interface
    Looker Studio is intuitive and user-friendly, making it easy for anyone on our team to create and customize reports.
  • Real-time data
    Looker Studio can connect directly to BigQuery, enabling us to create reports using real-time data.
  • Flexible and customizable
    Looker Studio enables us to create customized reports and dashboards that perfectly suit our needs.

Here are some examples:

This screenshot shows a crucial functionality we’ve designed within Looker Studio: the capability to filter data by specific groups of pages. This custom feature proves to be invaluable in our context, where we need granular insights about different sections of our website. As the image shows, we’re honing in on our “Route Landing Page” group. This subset of pages has experienced over one million visits in the last week alone, highlighting the significant traffic these pages attract. This demonstration exemplifies how our customizations in Looker Studio help us dissect and understand our site’s performance at a granular level.

The graph presents the LCP values for the 75th percentile of our users visiting the Route Landing Page group. This percentile represents the user experience of the “average” user, excluding outliers who may have exceptionally good or poor conditions.

A key advantage of using Looker Studio is its ability to segment data based on different variables. In the following screenshot, you can see that we have differentiated between mobile and desktop traffic.

Understanding The Challenges

In our journey, the key performance data we gathered acted as a compass, pointing us toward specific challenges that lay ahead. Influenced by factors such as global audience diversity, seasonality, and the intricate balance between static and dynamic content, these challenges surfaced as crucial areas of focus. It is within these complexities that we found our opportunity to refine and optimize web performance on a global scale.

Seasonality And A Worldwide Audience

As an international platform, Bookaway serves a diverse audience from various geographic locations. One of the key challenges that come with serving a worldwide audience is the variation in network conditions and device capabilities across different regions.

Adding to this complexity is the effect of seasonality. Much like physical tourism businesses, our digital platform also experiences seasonal trends. For instance, during winter months, our traffic increases from countries in warmer climates, such as Thailand and Vietnam, where it’s peak travel season. Conversely, in the summer, we see more traffic from European countries where it’s the high season for tourism.

The variation in our performance metrics, correlated with geographic shifts in our user base, points to a clear area of opportunity. We realized that we needed to consider a more global and scalable solution to better serve our global audience.

This understanding prompted us to revisit our approach to content delivery, which we’ll get to in a moment.

Layout Shifts From Dynamic And Static Content

We have been using dynamic content serving, where each request reaches our back-end server and triggers processes like database retrievals and page renderings. This server interaction is reflected in the TTFB metric, which measures the duration from the client making an HTTP request to the first byte being received by the client’s browser. The shorter the TTFB, the better the perceived speed of the site from the user’s perspective.

While dynamic serving provides simplicity in implementation, it imposes significant time costs due to the computational resources required to generate the pages and the latency involved in serving these pages to users at distant locations.

We recognize the potential benefits of serving static content, which involves delivering pre-generated HTML files like you would see in a Jamstack architecture. This could significantly improve the speed of our content delivery as it eliminates the need for on-the-fly page generation, thereby reducing TTFB. It also opens up the possibility for more effective use of caching strategies, potentially enhancing load times further.

As we envisage a shift from dynamic to static content serving, we anticipate it to be a crucial step toward improving our LCP metrics and providing a more consistent user experience across all regions and seasons.

In the following sections, we’ll explore the potential challenges and solutions we could encounter as we consider this shift. We’ll also discuss our thoughts on implementing a Content Delivery Network (CDN), which could allow us to fully leverage the advantages of static content serving.

Leveraging A CDN For Content Delivery

I imagine many of you already understand what a CDN is, but it is essentially a network of servers, often referred to as “edges.” These edge servers are distributed in data centers across the globe. Their primary role is to store (or “cache”) copies of web content — like HTML pages, JavaScript files, and multimedia content — and deliver it to users based on their geographic location.

When a user makes a request to access a website, the DNS routes the request to the edge server that’s geographically closest to the user. This proximity significantly reduces the time it takes for the data to travel from the server to the user, thus reducing latency and improving load times.

A key benefit of this mechanism is that it effectively transforms dynamic content delivery into static content delivery. When the CDN caches a pre-rendered HTML page, no additional server-side computations are required to serve that page to the user. This not only reduces load times but also reduces the load on our origin servers, enhancing our capacity to serve high volumes of traffic.

If the requested content is cached on the edge server and the cache is still fresh, the CDN can immediately deliver it to the user. If the cache has expired or the content isn’t cached, the CDN will retrieve the content from the origin server, deliver it to the user, and update its cache for future requests.

This caching mechanism also improves the website’s resilience to distributed denial-of-service (DDoS) attacks. By serving content from edge servers and reducing the load on the origin server, the CDN provides an additional layer of security. This protection helps ensure the website remains accessible even under high-traffic conditions.

CDN Implementation

Recognizing the potential benefits of a CDN, we decided to implement one for our landing pages. As our entire infrastructure is already hosted by Amazon Web Services (AWS), choosing Amazon AWS CloudFront as our CDN solution was an immediate and obvious choice. Its robust infrastructure, scalability, and a wide network of edge locations around the world made it a strong candidate.

During the implementation process, we configured a key setting known as max-age. This determines how long a page remains “fresh.” We set this property to three days, and for those three days, any visitor who requests a page is quickly served with the cached version from the nearest edge location. After the three-day period, the page would no longer be considered “fresh.” The next visitor requesting that page wouldn’t receive the cached version from the edge location but would have to wait for the CDN to reach our origin servers and generate a fresh page.

This approach offered an exciting opportunity for us to enhance our web performance. However, transitioning to a CDN system also posed new challenges, particularly with the multitude of pages that were rarely visited. The following sections will discuss how we navigated these hurdles.

Addressing Many Pages With Rare Visits

Adopting the AWS CloudFront CDN significantly improved our website’s performance. However, it also introduced a unique problem: our “long tail” of rarely visited pages. With over 100,000 landing pages, each available in seven different languages, we managed a total of around 700,000 individual pages.

Many of these pages were rarely visited. Individually, each accounted for a small percentage of our total traffic. Collectively, however, they made up a substantial portion of our web content.

The infrequency of visits meant that our CDN’s max-age setting of three days would often expire without a page being accessed in that timeframe. This resulted in these pages falling out of the CDN’s cache. Consequently, the next visitor requesting that page would not receive the cached version. Instead, they would have to wait for the CDN to reach our origin server and fetch a fresh page.

To address this, we adopted a strategy known as stale-while-revalidate. This approach allows the CDN to serve a stale (or expired) page to the visitor, while simultaneously validating the freshness of the page with the origin server. If the server’s page is newer, it is updated in the cache.

This strategy had an immediate impact. We observed a marked and continuous enhancement in the performance of our long-tail pages. It allowed us to ensure a consistently speedy experience across our extensive range of landing pages, regardless of their frequency of visits. This was a significant achievement in maintaining our website’s performance while serving a global audience.

I am sure you are interested in the results. We will examine them in the next section.

Performance Optimization Results

Our primary objective in these optimization efforts was to reduce the LCP metric, a crucial aspect of our landing pages. The implementation of our CDN solution had an immediate positive impact, reducing LCP from 3.5 seconds to 2 seconds. Further applying the stale-while-revalidate strategy resulted in an additional decrease in LCP, bringing it down to 1.7 seconds.

A key component in the sequence of events leading to LCP is the TTFB, which measures the time from the user’s request to the receipt of the first byte of data by the user’s browser. The introduction of our CDN solution prompted a dramatic decrease in TTFB, from 2 seconds to 1.24 seconds.

Stale-While-Revalidate Improvement

This substantial reduction in TTFB was primarily achieved by transitioning to static content delivery, eliminating the need for back-end server processing for each request, and by capitalizing on CloudFront’s global network of edge locations to minimize network latency. This allowed users to fetch assets from a geographically closer source, substantially reducing processing time.

Therefore, it’s crucial to highlight that

The significant improvement in TTFB was one of the key factors that contributed to the reduction in our LCP time. This demonstrates the interdependent nature of web performance metrics and how enhancements in one area can positively impact others.

The overall LCP improvement — thanks to stale-while-revalidate — was around 15% for the 75th percentile.

User Experience Results

The “Page Experience” section in Google Search Console evaluates your website’s user experience through metrics like load times, interactivity, and content stability. It also reports on mobile usability, security, and best practices such as HTTPS. The screenshot below illustrates the substantial improvement in our site’s performance due to our implementation of the stale-while-revalidate strategy.

Conclusion

I hope that documenting the work we did at Bookaway gives you a good idea of the effort that it takes to tackle improvements for Core Web Vitals. Even though there is plenty of documentation and tutorials about them, I know it helps to know what it looks like in a real-life project.

And since everything I have covered in this article is based on a real-life project, it’s entirely possible that the insights we discovered at Bookaway will differ from yours. Where LCP was the primary focus for us, you may very well find that another Web Vital metric is more pertinent to your scenario.

That said, here are the key lessons I took away from my experience:

  • Optimize Website Loading and Rendering.
    Pay close attention to the stages of your website’s loading and rendering process. Each stage — from TTFB, download time of HTML, and FCP, to fetching and decoding of images, parsing of JavaScript and CSS, and rendering of the largest contentful element — needs to be optimized. Understand potential pitfalls at each stage and make necessary adjustments to improve your site’s overall user experience.
  • Implement Performance Monitoring Tools.
    Utilize tools such as Next.js for real-time performance monitoring and BigQuery for storing and analyzing data. Visualizing your performance data with tools like Looker Studio can help provide valuable insights into your website’s performance, enabling you to make informed, data-driven decisions.
  • Consider Static Content Delivery and CDN.
    Transitioning from dynamic to static content delivery can greatly reduce the TTFB and improve site loading speed. Implementing a CDN can further optimize performance by serving pre-rendered HTML pages from edge servers close to the user’s location, reducing latency and improving load times.

Further Reading On SmashingMag

CSS And Accessibility: Inclusion Through User Choice

We make a series of choices every day. Get up early to work out or hit the snooze button? Double foam mocha latte or decaf green tea? Tabs or spaces? Our choices, even the seemingly insignificant ones, shape our identities and influence our perspectives on the world. In today’s modern landscape, we have come to expect a broad range of choices, regardless of the products or services we seek. However, this has not always been the case.

For example, there was a time when the world had only one font family. The first known typeface, a variant of Blackletter, graced Johannes Gutenberg’s pioneering printing press in 1440. The first set of publicly-available GUI colors shipped with the 10th version of the X Window System consisted of 69 primary shades and 138 entries to account for various color variations (e.g., “dark red”). In September 1995, a Netscape programmer, Brendan Eich, introduced “Mocha,” a scripting language that would later be renamed LiveScript and eventually JavaScript.

Fast forward to the present day, and we now have access to over 650,000 web fonts, a hexadecimal system capable of representing 16,777,216 colors, and over 100 public-facing JavaScript frameworks and libraries to choose from. While this is great news for professionals designing and building user interfaces, what choices are we giving actual users? Shouldn’t they have a say in their experience?

CSS Media Features

While designers and developers may have some insights into user needs, it is very challenging to understand the actual user preferences of 7.8 billion people at any given time. Supporting the needs of individuals with disabilities and assistive technology adds a layer of complexity to an already complex situation. Nonetheless, designers and developers are responsible for addressing these user needs as best we can by providing accessible choices. One promising solution is user-focused CSS media features that allow us to customize the user experience and cater to individual preferences.

Media Features For Color

Let’s first focus on CSS media features for color. Color plays a vital role in design, impacting how we perceive brands. Studies suggest that color alone can influence up to 90% of snap judgments about products. Considering the large number of people worldwide with visual deficiencies such as color blindness and low vision, developers and designers have a significant opportunity to improve accessibility with this element alone.

@prefers-color-scheme

The @prefers-color-scheme CSS media feature helps identify whether users prefer light or dark color themes. Users can indicate their preferences through settings in the operating system or user agent.

There are two values for this CSS media feature: light and dark. Typically, the default theme presented to users is the light version, even if the user expresses no preference. However, the opposite can also be true, and websites or apps default to a dark theme and switch to a light theme using the @media (prefers-color-scheme: light) media feature and corresponding code.

Users opting for a dark mode signifies their preference for a dark-themed page. Using @media (prefers-color-scheme: dark), various theme elements, such as text, links, and buttons, are adjusted to ensure they stand out against darker background colors.

In the past, there was also a no-preference value to indicate when users had no theme preference. However, user agents now treat light themes as the default, rendering the no-preference value obsolete.

@media (prefers-color-scheme: dark) {
  body {
    background-color: #282828;
  }

  .without [data-word="without"] .char:before,
  .without [data-word="without"] .char:after {
    color: #fff;
  }
}

The @prefers-color-scheme is one of the most widely used CSS media features today and it has a very large percentage of browser support at 94%. It is so popular that additional values may be introduced in the future to express more specific preferences or color schemes, such as sepia or grayscale.

Switching from the default light mode to dark mode is relatively straightforward. Consult the user setting guides for Mac and Windows operating systems to learn more (select the relevant hardware and operating system version), then navigate to a browser that supports this CSS media feature.

Pro-tip: A more sophisticated solution to demo user preference settings is using Chrome’s Rendering tab coupled with CSS media features emulator to easily switch from light to dark modes to emulate @prefers-color-scheme as users experience it. This solution is convenient for live demos where you need to show the user preference changes quickly or emulate media features not fully supported by your OS or browser.

@forced-colors

The @forced-colors CSS media feature enables the detection of the forced colors mode enforced by the user agent. This mode imposes a limited color palette the user chooses onto the page. This newer media feature provides an alternative approach to handle colors for non-Window devices, and we expect it will replace Windows High Contrast Mode in the future.

There are two values for the forced-colors media feature: none and active. The @media (forced-colors: none) value indicates that the forced colors mode is inactive and uses the default color scheme, while the @media (forced-colors: active) value means that the forced colors mode is active and the user agent enforces the user-selected limited color palette.

It’s worth noting that enabling @forced-colors mode does not necessarily imply a preference for higher contrast. The color adjustments align with the user’s choice, which may not strictly fit into the low or high-contrast categories.

Note: There are some properties affected by the forced-color mode that you need to be aware of when designing and testing your forced-colors theme. Check out Eric Bailey’s article “Windows High Contrast Mode, Forced Colors Mode And CSS Custom Properties” for more information about this media feature and its integration with CSS custom properties.

@media (forced-colors: active) {
  body {
    background-color: #fcba03;
  }

  .without [data-word="without"] .char:before,
  .without [data-word="without"] .char:after {
    color: #ac1663;
  }

  .without {
    color: #004a72;
  }
}

The @forced-colors CSS media feature is currently supported by 31% of the most popular browsers, including desktop versions of Chrome, Edge, and Firefox. Although the browser support for this feature is increasing, not all operating systems currently offer a setting to activate the forced colors mode. The Windows operating system is the only exception, as it provides the necessary functionality for users to create customized themes that override the default ones by utilizing the Windows High Contrast mode.

If you are using a non-Windows machine, you can emulate the behavior of this media feature by following the steps mentioned earlier in the @prefers-color-scheme section using Chrome’s Rendering tab and emulator, but with a focus on emulating @forced-colors instead.

@inverted-colors

The @inverted-colors CSS media feature determines whether to show the content in its standard colors or if it reverses the colors.

Two modes are available for the @inverted-colors media feature: none and inverted. The @media (inverted-colors: none) value indicates that the forced colors mode is not activated and uses the default color scheme. Using the @media (inverted-colors: inverted) value indicates that all pixels within the displayed area have been inverted and renders the inverted color theme when a user chooses this option.

When writing code for the @inverted-colors CSS media feature, one option is to write your code using the inverted value of what you want a user to see to ensure correct rendering after applying the user’s setting.

For example, you want your element’s background to be #e87b2d, which is a tangerine orange. In the theme code, you would write the opposite color, #1784d2, powder blue. Incorporating this inverse color into the code renders the intended tangerine orange instead of its inverse when users enable the @inverted-colors setting.

@media (inverted-colors: inverted) {
  body {
    background-color: #99cc66;
  }

  .without [data-word="without"] .char:before,
  .without [data-word="without"] .char:after {
    color: #ee1166;
  }

  .without {
    color: #111111;
  }
}

Current browser support for @inverted-colors is 20% for Safari desktop and iOS browsers. While Chrome’s Rendering tab and emulator do not work for this particular media feature, you can emulate @inverted-colors using Firefox (version 114 or newer).

  1. Open a new tab in Firefox and type or paste about:config in the address bar, and press Enter/Return. Click the button acknowledging that you will be careful.
  2. In the search box, enter layout.css.inverted-colors and wait for the list to be filtered.
  3. Use the toggle button to switch the preference from false to true.
  4. Enable the inverted colors setting in your operating system and navigate to a webpage or code example with the @inverted-colors theme to observe the inverted effect.

The setting for the @inverted-colors media feature is available on Mac and Windows operating systems.

Media Features For Contrast

Next, let’s talk about CSS media features related to contrast. Contrast plays a crucial role in conveying visual information to users, working hand in hand with color. When proper levels of color contrast are not implemented, it becomes difficult to distinguish essential elements such as text, icons, and important graphics. As a result, the design can become inaccessible not only to the 46 million people worldwide with low vision but also to older adults, individuals using monochrome displays, or those in specific situations like low lighting in a room.

@prefers-contrast

The @prefers-contrast CSS media feature detects the user’s preference for higher or lower contrast on a page. The feature uses the information to make appropriate adjustments, such as modifying the contrast ratio between colors nearby or altering the visual prominence of elements, such as adjusting their borders, to better suit the user’s contrast requirements.

There are four values for this CSS media feature: no-preference, less, more, and custom. The @media (prefers-contrast: no-preference) value indicates that the user has no preference (or did not choose one since it is the default setting), and the @media (prefers-contrast: less) value indicates a user’s preference for less contrast. Conversely, the @media (prefers-contrast: more) value indicates a user’s preference for stronger contrast.

The @media (prefers-contrast: custom) value is a bit more complex as it allows users to use a custom set of colors — which could be specific to contrast — or choose a palette. For example, a user could select a theme composed entirely of shades of blue, primary colors, or even a rainbow theme — anything they choose.

Note: When a user selects the custom contrast setting, it will align with the color palette defined by users of forced-colors: active value, so be sure to account for that in the code.

@media (prefers-contrast: more) {
  .title2 {
    color: var(--clr-6);
  }

  .aurora2__item:nth-of-type(1),
  .aurora2__item:nth-of-type(2),
  .aurora2__item:nth-of-type(3),
  .aurora2__item:nth-of-type(4) {
    background-color: var(--clr-6);
  }
}

@media (prefers-contrast: less) {
  .title {
    color: var(--clr-5);
  }

  .aurora__item:nth-of-type(1),
  .aurora__item:nth-of-type(2),
  .aurora__item:nth-of-type(3),
  .aurora__item:nth-of-type(4) {
    background-color: var(--clr-5);
  }
}

@media (prefers-contrast: custom) {
  .aurora2__item:nth-of-type(1) {
    background-color: var(--clr-1);
  }
  .aurora2__item:nth-of-type(2) {
    background-color: var(--clr-2);
  }
  .aurora2__item:nth-of-type(3) {
    background-color: var(--clr-3);
  }
  .aurora2__item:nth-of-type(4) {
    background-color: var(--clr-4);
  }
}

Currently, 91% of the most widely used browsers offer support for the @prefers-contrast media feature. However, the majority of this support is focused on enhancing contrast rather than reducing it or allowing for personalized contrast themes.

To effectively demo and test all the different contrast options for this CSS media feature, use the Chrome Rendering tab and emulator as described earlier, but with a specific emphasis on emulating the @prefers-contrast media feature this time.

@prefers-reduced-transparency

The @prefers-reduced-transparency CSS media feature determines if the user has requested the system to use fewer transparent or translucent layer effects.

It takes one of two possible values: no-preference and reduce. The @media (prefers-reduced-transparency: no-preference) value indicates that the user has not specified any preference for the system (this is also the default setting). On the other hand, the @media (prefers-reduced-transparency: reduce) value indicates that the user has informed the system about their preference for an interface that minimizes the application of transparent or translucent layer effects.

@media (prefers-reduced-transparency: reduce) {
  .title,
  .title2 {
    opacity: 0.7;
  }
}

The current browser support for @prefers-reduced-transparency stands at 0%. This CSS media feature is highly experimental and should not be utilized in production code at the time I’m writing this article.

However, if you wish to emulate the @prefers-reduced-transparency media feature behavior, you can follow these steps using Firefox (version 113 or newer).

  1. Open a new tab in Firefox and type or paste about:config in the address bar, and press Enter/Return. Click the button acknowledging that you will be careful.
  2. In the search box, type or paste layout.css.prefers-reduced-transparency and wait for the list to be filtered.
  3. Use the toggle button to switch the preference from the default state of false to true.
  4. Adjust your operating system’s transparency settings and navigate to a webpage or code example with the @prefers-reduced-transparency theme to observe the effect of reduced transparency.

Media Features For Motion

Lastly, let’s turn our focus to motion. Whether it involves videos, GIFs, or SVGs, movement can enrich our online experiences. However, this media type can also adversely affect many individuals. People with vestibular disabilities, seizure disorders, and migraine disorders can benefit from accessible media. CSS media features for motion allow us to incorporate both dynamic movement and static states for elements, enabling us to have the best of both worlds.

@prefers-reduced-motion

Using the @prefers-reduced-motion CSS media feature helps determine whether the user has requested the system to minimize the usage of non-essential motion.

This CSS media feature accepts one of two values: no-preference and reduce. The @media (prefers-reduced-motion: no-preference) value indicates that the user has not specified any preference for the system (this is also the default setting). Conversely, the @media (prefers-reduced-motion: reduce) value indicates that the user has informed the system about their preference for an interface that eliminates or substitutes motion-based animations that may cause discomfort or serve as distractions for them.

@media (prefers-reduced-motion: reduce) {
  .bg-rainbow {
    animation: none;
  }

  .perfection {
    .word {
      .char {
        animation: slide-down 5s cubic-bezier(0.75, 0, 0.25, 1) both;
        animation-delay: calc(#{$delay} + (0.5s * var(--word-index)));
      }
    }

    [data-word="perfection"] {
      animation: slide-over 4.5s cubic-bezier(0.5, 0, 0.25, 1) both;
      animation-delay: $delay;

      .char {
        animation: none;
        visibility: hidden;
      }

      .char:before,
      .char:after {
        animation: split-in 4.5s cubic-bezier(0.75, 0, 0.25, 1) both alternate;
        animation-delay: calc(
          3s + -0.2s * (var(--char-total) - var(--char-index))
        );
      }
    }
  }
}

You can compare the difference in the following videos and by viewing a live demo.

@prefers-reduced-data

Last but certainly not least, let’s examine the @prefers-reduced-data CSS media feature. This media feature determines whether the user prefers to receive alternate content that consumes less data when rendering the page.

This CSS media feature has two possible values: no-preference and reduce. The @media (prefers-reduced-motion: no-preference) value indicates that the user has not specified any preference for the system (which is also the default setting). On the other hand, the @media (prefers-reduced-data: reduce) value indicates that the user has expressed a preference for lightweight alternate content.

Unlike other CSS media features, a user’s preference for the @prefers-reduced-data media feature could vary. It may be a system-wide setting exposed by the operating system or settings controlled by the user agent. In the case of the user agent, they may determine this value based on the same user or system preference used for setting the Save-Data HTTP request header.

Note that the Save-Data network client request header is still considered experimental technology, but it has achieved a remarkable 72% browser support across various browsers, except Safari and Firefox on desktop and mobile.

@media (prefers-reduced-data: reduce) {
  .bg-rainbow {
    animation: none;
  }

  .perfection {
    .word {
      .char {
        animation: none;
      }
    }

    [data-word="perfection"] {
      animation: none;

      .char {
        animation: none;
        visibility: hidden;
      }

      .char:before,
      .char:after {
        animation: none;
      }
    }
  }
}

Similar to @prefers-reduced-transparency, the @prefers-reduced-data CSS media feature is highly experimental and should not be utilized in production code at this time as the current browser support for it stands at 0%.

However, if you wish to emulate the @prefers-reduced-data behavior, you can follow these steps using Chrome (version 85 or newer).

  1. Open a new tab in Chrome and type or paste chrome://flags in the address bar and press Enter/Return.
  2. In the search box, type or paste experimental-web-platform-features and wait for the list to be filtered.
  3. Use the dropdown option to switch the preference from the default state of disabled to enabled.
  4. Use the Chrome Rendering tab and choose the appropriate CSS media feature to emulate.

Note that you can also enable the @prefers-reduced-data feature in Edge, Opera, and Chrome Android (all behind the same experimental-web-platform-features flag), but it is less clear how you would emulate the media feature without the rendering tab and emulator found in the desktop version of Chrome.

Amplifying Inclusion Through User Choice

In the tech world, accessibility often receives criticism, particularly with aesthetics and advanced features. However, this negative perception can be changed. It is possible to incorporate stunning design and innovative functionality while prioritizing accessibility by leveraging CSS user-focused media features that address color, contrast, and motion.

Today, by incorporating all available options for each CSS media feature currently supported by browsers (with support exceeding 90%), you can provide users with 16 combinations of options. However, when the browsers and operating systems implement and support more experimental media features, the impact on user customization expands significantly to a staggering 256 combinations of options. A large number of possible options truly amplifies the potential impact designers and developers can have on user experiences.

As professionals within the technology industry, our goal should be to ensure that digital products are accessible to all individuals. By offering users the ability to personalize their experience, we can include an array of remarkable features in a responsible manner. Our job is to provide options and let people choose their own adventure.

Further Reading On SmashingMag

Writing CSS In 2023: Is It Any Different Than A Few Years Ago?

Is there anything in the front-end world that’s evolving faster than CSS these days? After what seemed like a long lull following blockbusters Flexbox and Grid, watching CSS release new features over the past few years has been more like watching a wild game of rugby on the telly. The pace is exciting, if not overwhelming at the same time.

But have all these bells and whistles actually changed the way you write CSS? New features have certainly influenced the way I write CSS today, but perhaps not quite as radically as I would have expected.

And while I’ve seen no shortage of blog posts with high-level examples and creative experiments of all these newfangled things that are available to us, I have yet to see practical applications make their way into production or everyday use. I remember when Sass started finding its way into CSS tutorials, often used as the go-to syntax for code examples and snippets. I’m not exactly seeing that same organic adoption happen with, say, logical properties, and we’ve had full browser support for them for about two years now.

This isn’t to rag on anyone or anything. I, for one, am stoked beyond all heck about how CSS is evolving. Many of the latest features are ones we have craved for many, many years. And indeed, there are several of them finding their way into my CSS. Again, not drastically, but enough that I’m enjoying writing CSS more now than ever.

Let me count the ways.

More And More Container Queries

I’ll say it: I’ve never loved writing media queries for responsive layouts. Content responds differently to the width of the viewport depending on the component it’s in. And balancing the content in one component has always been a juggling act with balancing the content in other components, adding up to a mess of media queries at seemingly arbitrary breakpoints. Nesting media queries inside a selector with Sass has made it tolerable, but not to the extent that I “enjoyed” writing new queries and modifying existing ones each time a new design with UI changes is handed to me.

Container queries are the right answer for me. Now I can scope child elements to a parent container and rely on the container’s size for defining where the layout shifts without paying any mind to other surrounding components.

The other thing I like about container queries is that they feel very CSS-y. Defining a container directly on a selector matches a natural property-value syntax and helps me avoid having to figure out math upfront to determine breakpoints.

.parent {
  container-type: inline-size;
}

@container (min-width: 600px) {
  .child {
    align-self: center;
  }
}

I still use media queries for responsive layouts but tend to reserve them for “bigger” layouts that are made up of assembled containers. Breakpoints are more predictable (and can actually more explicitly target specific devices) when there’s no need to consider what is happening inside each individual container.

Learn About Container Queries

Grouping Styles In Layers

I love this way of managing the cascade! Now, if I have a reset or some third-party CSS from a framework or whatever, I can wrap those in a cascade layer and chuck them at the bottom of a file so my own styles are front and center.

I have yet to ship anything using cascade layers, but I now reach for them for nearly every CodePen demo I make. The browser support is there, so that’s no issue. It’s more that I still rely on Sass on projects for certain affordances, and maintaining styles in partialized files still feels nice to me, at least for that sort of work.

But in an isolated demo where all my styles are in one place, like CodePen? Yeah, all the cascade layers, please! Well, all I really need is one layer for the base styles since un-layered styles have higher specificity than layered ones. That leaves my demo-specific styles clean, uncluttered, and still able to override the base at the top, which makes it way more convenient to access them.

body {
  display: grid;
  gap: 3rem;
  place-items: center;
}

@layer base {
  body {
    font-size: 1.25rem;
    line-height: 1.35;
    padding: 3rem;
  }
}

Learn More About Cascade Layers

:is() And :where()

I definitely reach for these newer relational pseudo-selectors, but not really for the benefits of selecting elements conditionally based on relationships.

Instead, I use them most often for managing specificity. But unlike cascade layers, I actually use these in production.

Why? Because with :is(), specificity is determined not by the main selector but by the most specific selector in its argument list.

/* Specificity: 0 1 0 */
:is(ol, .list, ul) li {}

/* Specificity: 0 0 2 */
ol li {}

The .list selector gives the first ruleset a higher specificity score meaning it “beats” the second ruleset even though the first ruleset is higher in the cascade.

On the flip side, the specificity of :where() is a big ol’ score of zero, so it does not add to the overall score of whatever selector it’s on. It simply doesn’t matter at all what’s in its argument list. For the same reason I use :is() to add specificity, I use :where() to strip it out. I love keeping specificity generally low because I still want the cascade to operate with as little friction as possible, and :where() makes that possible, especially for defining global styles.

A perfect example is wrapping :not() inside :where() to prevent :not() from bumping up specificity:

/* Specificity: 0 0 0 */
:where(:not(.some-element)) {}

Taken together, :is() and :where() not only help manage specificity but also take some cognitive load from “naming” things.

I’m one of those folks who still love the BEM syntax. But naming is one of the hardest things about it. I often find myself running out of names that help describe the function of an element and its relationship to elements around it. The specificity-wrangling powers of :is() and :where() means I can rely less on elaborate class names and more on element selectors instead.

Learn More About :is() And :where()

The New Color Function Syntax

The updated syntax for color functions like rgb() and hsl() (and the evolving oklch() and oklab()) isn’t the sort of attention-grabbing headline that leads to oo’s and aw’s, but it sure does make it a lot better to define color values.

For one, I never have to reach for rgba() or hsla() when I need an alpha value. In fact, I always used those whether or not I needed alpha because I didn’t want to bother deciding which version to use.

color: hsl(50deg, 100%, 50%);

/* Same */
color: hsla(50deg, 100%, 50% / 1)

Yes, writing the extra a, /, and 1 was worth the cost of not having to think about which function to use.

But the updated color syntax is like a honey badger: it just doesn’t care. It doesn’t care about the extra a in the function name. It doesn’t even care about commas.

color: hsl(50deg 100% 50% / .5);

So, yeah. That’s definitely changed the way I write colors in CSS.

What I’m really excited to start using is the newer oklch() and oklab() color spaces now that they have full browser support!

Learn More About CSS Color 4 Features

Sniffing Out User Preferences

I think a lot of us were pretty stoked when we got media queries that respect a user’s display preferences, the key one being the user’s preferred color scheme for quickly creating dark and light interfaces.

:root {
  --bg-color: hsl(0deg 0% 100%);
  --text-color: hsl(0deg 0% 0%);
}

@media (prefers-color-scheme: dark) {
  :root {
    --bg-color: hsl(0deg 0% 0%);
    --text-color: hsl(0deg 0% 100%);
  }
}

body {
  background: var(--bg-color);
  color: var(--text-color);
}

But it’s the prefers-reduced-motion query that has changed my CSS the most. It’s the first thing I think about any time a project involves CSS animations and transitions. I love the idea that a reduced motion preference doesn’t mean nuking all animation, so I’ll often use prefers-reduced-motion to slow everything down when that’s the preference. That means I have something like this (usually in a cascade layer for base styles):

@layer base {
  :root {
    --anim-duration: 1s;
  }

  /* Reduced motion by default */
  body {
    animation-duration: --anim-duration;
    transition: --anim-duration;
  }

  /* Opt into increased motion */
  @media screen and (prefers-reduced-motion: no-preference) {
    body {
      --anim-duration: .25s;
    }
  }
}

Learn More About User Preference Queries

Defining Color Palettes

I’ve used variables for defining and assigning colors ever since I adopted Sass and was thrilled when CSS custom properties came. I’d give generic names to the colors in a palette before passing them into variables with more functional names.

/* Color Palette */
--red: #ff0000;
/* etc. */

/* Brand Colors */
--color-primary: var(--red);
/* etc. */

I still do this, but now I will abstract things even further using color functions on projects with big palettes:

:root {
  /* Primary Color HSL */
  --h: 21deg;
  --s: 100%;
  --l: 50%;

  --color-primary: hsl(var(--h) var(--s) var(--l) / 1);
}

.bg-color {
  background: var(--color-primary);
}

.bg-color--secondary {
  --h: 56deg;
  background: hsl(var(--h) var(--s) var(--l) / 1);
}

A little too abstract? Maybe. But for those projects where you might have ten different varieties of red, orange, yellow, and so on, it’s nice to have this level of fine-grained control to manipulate them. Perhaps there is more control with color-mix() that I just haven’t explored yet.

Learn More About Defining Color Palettes

What I’m Not Using

Huh, I guess I am writing CSS a bit differently than I used to! It just doesn’t feel like it, but that probably has to do with the fact that there are so many other new features I am not currently using. The number of new features I am using is much, much lower than the number of features I have yet to pick up, whether it’s because of browser support or because I just haven’t gotten to it yet.

CSS Nesting

I’m really looking forward to this because it just might be the tipping point where I completely drop Sass for vanilla CSS. It’s waiting for Firefox to support it at the time of this writing, so it could be right around the corner.

Style Queries

I’ve made no secret that applying styles to elements based on the styles of other elements is something that I’m really interested in. That might be more of an academic interest because specific use cases for style queries elude me. Maybe that will change as they gain browser support, and we see a lot more blog posts where smart folks experiment with them.

:has()

I’ll definitely use this when Firefox supports it. Until then, I’ve merely tinkered with it and have enjoyed how others have been experimenting with it. Without full support, though, it hasn’t changed the way I write CSS. I expect that it will, though, because how can having the ability to select a parent element based on the child it contains be a bad thing, right?

Dynamic Viewport Units

I’ve started sprinkling these in my styles since they gained wide support at the end of 2022. Like style queries, I only see limited use cases — most notably when setting elements to full height on a mobile device. So, instead of using height: 100vh, I’m starting to write height: 100dvh more and more. I guess that has influenced how I write CSS, even if it’s subtle.

Media Query Range Syntax

Honestly, I just haven’t thought much about the fact that there’s a nicer way to write responsive media queries on the viewport. I’m aware of it but haven’t made it a part of my everyday CSS for no other reason than ignorance.

OKLCH/OKLAB Color Spaces

oklch() will most definitely be my go-to color function. It gained wide support in March of this year, so I’ve only had a couple of months and no projects to use it. But given the time, I expect it will be the most widely used way to define colors in my CSS.

The only issue with it, I see, is that oklch() is incompatible with another color feature I’m excited about...

color()

It’s widely supported now, as of May 2023! That’s just too new to make its way into my everyday CSS, but you can bet that it will. The ability to tap into any color space — be it sRGB, Display P3, or Rec2020 — is just so much nicer than having to reach for a specific color function, at least for colors in a color space with RGB channels (that’s why color() is incompatible with oklch() and other non-RGB color spaces).

--primary-color: color(display-p3 1 0.4 0);

I’m not in love with RGB values because they’re tough to understand, unlike, say, HSL. I’m sure I’ll still use oklch() or hsl() in most cases for that very reason. It’s a bummer we can’t do something like this:

/* 👎 */
--primary-color: color(oklch 70% 0.222 41.29);

We have to do this instead:

/* 👍 */
--primary-color: oklch(70% 0.222 41.29);

The confusing thing about that is it’s not like Display P3 has its own function like OKLCH:

/* 👎 */
--primary-color: display-p3(1 0.434 0.088);

We’re forced to use color() to tap into Display P3. That’s at odds with OKLCH/OKLAB, where we’re forced to reach for those specific functions.

Maybe one day we’ll have a global color() function that supports them all! Until then, my CSS will use both color() and specific functions like oklch() and decide which is best for whatever I’m working on.

I’ll also toss color-mix() in this bucket, as it gained full support at the same time as color(). It’s not something I write regularly yet, but I’ll certainly play with it, likely for creating color palettes.

Honorable Mentions

It would be quite a feat to comment on every single new CSS feature that has shipped over the past five or so years. The main theme when it comes to which features I am not using in my day-to-day work is that they are simply too new or they lack browser support. That doesn’t mean I won’t use them (I likely will!), but for the time being, I’m merely keeping a side-eye on them or simply having a fun time dabbling in them.

Those include:

  • Trigonometric functions,
  • Anchor position,
  • Scroll-linked animations,
  • initial-letter,
  • <selectmenu> and <popover>,
  • View transitions,
  • Scoped Styles.

What about you? You must be writing CSS differently now than you were five years ago, right? Are you handling the cascade differently? Do you write more vanilla CSS than reaching for a preprocessor? How about typography, like managing line heights and scale? Tell me — or better yet, show me — how you’re CSS-ing these days.

Retro CSS Text Effect: A Step-by-Step Tutorial

CSS offers an array of tools that, when used correctly, can improve the visual experience on your website. In this tutorial, we’ll explore a straightforward way to design a retro text effect with pure CSS. The approach, while not overly complex, yields a visually appealing result and serves as a foundation for further customization.

Your Web Designer Toolbox
Unlimited Downloads: 500,000+ Web Templates, Icon Sets, Themes & Design Assets


The HTML Setup

We’ll begin with our markup, containing the text we’ll be styling – “1stWebDesigner“.

<div class="retro-text"> 1stWebDesigner</div>

The div class .retro-text will be the hook for our CSS styling.

Designing the Retro Style with CSS

Next, let’s move on to our CSS file to create the retro text effect.

@import url('https://fonts.googleapis.com/css2?family=Lobster+Two:wght@700&display=swap');

body {
    background: #6868AC; /* Retro background color */
}

.retro-text {
    font-family: 'Lobster Two', serif; /* Stylish, retro font */
    font-size: 10vw; /* Responsive font size */
    position: relative; /* Enables use of z-index */
    color: #F9f1cc; /* Primary color of the text */
    text-shadow: -2px 2px 0 #FFB650, /* Orange shadow */
                 -4px 4px 0 #FF80BF, /* Pink shadow */
                 -6px 6px 0 #6868AC; /* Dark blue shadow */
    transform: skewX(-10deg); /* Skew the text on the X-axis */
    transition: all 0.5s ease; /* Smooth transition for hover effects */
    z-index: 2; /* Ensures text is layered above any potential background or border */
}

.retro-text:hover {
    color: #FFFFFF; /* Brighter color on hover */
    font-size: 15vw; /* Slightly larger text on hover */
    text-shadow: -2px 2px 0 #FFC162, /* Brighter orange shadow on hover */
                 -4px 4px 0 #FF92D0, /* Brighter pink shadow on hover */
                 -6px 6px 0 #8888D3; /* Brighter blue shadow on hover */
}

To explain our CSS setup:

  • font-family: 'Lobster Two', serif;: We’re using Lobster Two, a stylish retro font.
  • font-size: 10vw;: Sets a responsive font size that adapts to the viewport width.
  • position: relative;: The relative positioning is necessary for the use of the z-index property.
  • color: #F9f1cc;: This determines the primary color of the text. Here, we’re using #F9f1cc, a light cream color.
  • text-shadow: -2px 2px 0 #FFB650, -4px 4px 0 #FF80BF, -6px 6px 0 #6868AC;: Three layers of text-shadow (orange, pink, and dark blue) are added, creating a 3D effect that enhances the retro feel.
  • transform: skewX(-10deg);: The text is skewed on the X-axis to add a dynamic touch.
  • transition: all 0.5s ease;: Smooth transition for hover effects.
  • z-index: 2;: A z-index of 2 ensures the text is always layered above any potential background or border.
  • :hover: The hover state includes a brighter color, slightly larger text size, and brighter shadows.

The Result

Here’s how the above code renders:

See the Pen
Retro CSS Text Effects
by 1stWebDesigner (@firstwebdesigner)
on CodePen.0

Final Thoughts

As you can see, CSS provides numerous opportunities to enhance your design. Using our retro text effect as a launching pad, you could experiment with further tweaks like altering text shadows, adjusting opacities or incorporating gradient backgrounds to intensify the retro vibe.

However, it’s crucial to remember the function of your text. The aim is to create a visually engaging site while maintaining readability. This is particularly important when using viewport units like vw for font sizes, which we used in our example. These units allow your text to adjust with the viewport size, ensuring a responsive design.

Yet, care is required. In some contexts, such as headings, vw units could cause your text to appear disproportionately large or small. To prevent this, consider using a mix of viewport and fixed units like em or rem, or setting max/min font sizes with media queries. Always remember: while design is important, it should never compromise the user’s ability to comfortably read and understand your content.

So, whether you’re introducing new elements, tweaking existing ones, or harnessing advanced techniques, every step you take helps you create unique styles that reflect your design aspirations.

Making an Underwater CSS Text Effect

Web design can serve as a playful exploration ground for learning new techniques. In today’s guide, we’ll dive into the creation of an underwater CSS text effect, not just for the visual outcome, but to deepen our understanding of how different CSS properties harmonize to create dynamic text effects.

Your Web Designer Toolbox

Unlimited Downloads: 500,000+ Web Templates, Icon Sets, Themes & Design Assets Starting at only $16.50/month!

Setting up the Structure

Our journey into the deep sea starts with a simple HTML structure: a div element with the class underwater, wrapping around an h1 tag.

<div class="underwater">
	<h1>1stWebDesigner</h1>
</div>

Achieving the Underwater Effect

For our underwater CSS text effect, we introduce a range of CSS properties such as background-image, animation, and -webkit-background-clip.

@import url('https://fonts.googleapis.com/css2?family=Maven+Pro:wght@700&amp;display=swap');

body{
	/* Using a dark background color for optimal contrast */
	background-color: #000;
	font-family: 'Maven Pro', sans-serif;
}

.underwater h1{
	/* Font settings: sizing and a semi-transparent color */
	font-size: 2.5rem;
	color: #2c3e5010;
	
	/* Assigning an underwater image as the background */
	background-image: url('https://w7.pngwing.com/pngs/183/509/png-transparent-under-water-scenery-sunlight-underwater-ray-star-ocean-atmosphere-cloud-computer-wallpaper.png');
	
	/* Clipping the background image to the outline of the text */
	-webkit-background-clip:text;
	
	/* Setting a 10s infinite animation for a dynamic effect */
	animation: waterEffect 10s infinite;
}

/* Animation to simulate flowing water */
@keyframes waterEffect {
	0% { background-position: left 0 top 0; }
	100% { background-position: left 100% top 0; }
}

Explaining Key CSS Properties and Values

Breaking down our CSS code, the first point of interest is the background-image property. By setting an underwater image as the background, we immediately set the tone for our effect.

The -webkit-background-clip:text property clips the background image to the shape of the text. It allows the underwater image to fill the text, setting the stage for our effect.

The color property plays a vital role as well. We’re using a semi-transparent color (color: #2c3e5010;), where the last two digits 10 represent the alpha channel in hexadecimal format, controlling the transparency. This enables the background image to shine through, enhancing the underwater illusion.

The animation property sets our waterEffect animation into motion. Defined by the @keyframes rule, it continuously shifts the background-position from left 0 top 0 to left 100% top 0, creating the illusion of water flowing over the text.

The Result

See the Pen
Underwater Text Effect by 1stWebDesigner (@firstwebdesigner)
on CodePen.0

Exploring Other Methods

Different methods can achieve similar effects. An alternate approach involves utilizing the clip-path property with CSS animations, yielding a wavy text appearance akin to an underwater CSS text effect. This method manipulates the clip region of an element over time, evoking a dynamic sense of movement reminiscent of water’s rhythmic flow. In addition, the technique doesn’t necessitate a background image, instead, it transforms the appearance of the text directly. By turning to this method, you’re exposed to yet another aspect of CSS and its potential for dynamic text effects.

Ripple Button Effect Using Pure CSS

Google’s Material Design guidelines introduced the ripple effect, a subtle animation that indicates user action. The ripple effect rapidly gained popularity in web design as a sophisticated visual feedback form that refines user interaction, particularly on buttons. Today, we’ll show you how to create a ripple button effect using nothing but pure CSS.

Your Web Designer Toolbox
Unlimited Downloads: 500,000+ Web Templates, Icon Sets, Themes & Design Assets


Building the Button

The basic structure of our button is quite simple. It’s a single line of HTML:

<button class="btn-ripple">CLICK ME</button>

This is a standard button element with a class btn-ripple attached to it, which will be our reference when we define the ripple effect in CSS.

Casting Ripples With CSS

/* Styling for the ripple button */
.btn-ripple {
  border: none; /* Removing the default button border */
  border-radius: 6px; /* Giving our button rounded corners */
  padding: 12px 16px; /* Providing some padding around the button text */
  font-size: 1.2em; /* Increasing the font size of the button text */
  cursor: pointer; /* Changing the cursor to a hand icon when hovering over the button */
  color: white; /* Making the button text color white */
  background-color: #fa6e83; /* Setting the initial button background color */
  outline: none; /* Removing the outline from the button */
  background-position: center; /* Setting the position of the background image to center */
  transition: background 1s; /* Adding a transition to the background color */
}

/* Defining the hover state */
.btn-ripple:hover {
  background: #f94b71 radial-gradient(circle, transparent 1%, #f94b71 1%)
    center/15000%; /* Creating a radial gradient background on hover */
}

/* Defining the active (clicked) state */
.btn-ripple:active {
  background-color: #f97c85; /* Changing the button color when active */
  background-size: 100%; /* Increasing the size of the background image */
  transition: background 0s; /* Removing the transition from the background color */
}

Let’s break down the CSS setup:

  • The .btn-ripple class sets up the basic appearance of the button. The background-color is initially set to #FA6E83, a light color, and the background-position is centered to ensure our ripple effect starts from the middle of the button.
  • When you hover over the button, the :hover pseudo-class is activated. It changes the background to a radial gradient that’s centered where the pointer is located, simulating the ripple effect. The gradient starts as transparent (transparent 1%) and transitions to the button color (#F94B71 1%), creating a soft ripple effect.
  • Upon clicking the button, the :active pseudo-class takes effect. It changes the background-color to a darker shade (#F97C85) and expands the background-size to 100%, reinforcing the ripple effect. The transition for the background is also set to 0s, making the effect appear instantaneously when the button is clicked.

The Result

See the Pen
Pure CSS Ripple Button Effect
by 1stWebDesigner (@firstwebdesigner)
on CodePen.0

Final Thoughts

We demonstrated a classic example of how simple CSS can be used to create appealing interactivity in a user interface. But as you strive to refine your UI, it’s critical to remember that each interface element might require different tweaks.

Consider the context in which your buttons are used. A button for submitting form data might benefit from a more subdued ripple effect, while a call-to-action button could be more prominent with a stronger one.

For more intricate animations or synchronizing with other UI events, JavaScript could be leveraged for more granular control. CSS provides a solid base for styling and basic animations, but JavaScript opens up more advanced possibilities.

And of course, customization is key. While we used specific colors for our ripple button here, feel free to experiment with colors, shapes, and transitions that align with your brand and design aesthetic.

How to Animate Gradient Text Using CSS

Web design takes a captivating turn when CSS comes into play. It enables a world of transformations, such as taking static text elements and infusing them with life. Our focus today is one such engaging transformation – animate gradient text using CSS.

So, let’s demonstrate how a seemingly complex effect can be achieved with a few lines of code.

UNLIMITED DOWNLOADS: 400,000+ Fonts & Design Assets

Starting at only $16.50 per month!

Setting Up the Text in the HTML

We begin by defining our text element in HTML, which in this case is a simple heading:

<h1 class="animated-gradient">1stWebDesigner</h1>

Here, we create an <h1> element with a class called “animated-gradient”. This class becomes our anchor for creating the gradient animation in CSS.

Unfolding the Gradient Animation

The core part lies within our CSS. Let’s define the gradient and set it in motion with the following code:

/* Google Fonts for Open Sans */
@import url('https://fonts.googleapis.com/css2?family=Open+Sans:wght@700&amp;display=swap');

/* Define animation */
@keyframes gradient-shift {
  0% {background-position: 0%}
  100% {background-position: 100%}
}

/* Styling our animated gradient text */
.animated-gradient {
  font-family: 'Open Sans', sans-serif;
  font-size: 2em;
  background: linear-gradient(270deg, #ff4b59, #ff9057, #ffc764, #50e3c2, #4a90e2, #b8e986, #ff4b59);
  background-size: 200%;
  -webkit-background-clip: text;
  -webkit-text-fill-color: transparent;
  animation: gradient-shift 3s ease-in-out infinite;
}

Our CSS setup does the following:

  • @import url: This directive fetches the Open Sans font from Google Fonts, noted for its modern and clean aesthetics.
  • @keyframes: Here, we define an animation named gradient-shift. This animation creates the illusion of motion in the gradient by shifting the background’s position from 0% to 100%.
  • font-family and font-size: These properties set our text’s font to Open Sans and its size to 2em.
  • background: This property generates a linear gradient using a striking array of colors. The gradient direction is set to 270 degrees, providing a left-to-right color flow.
  • background-size: This property, set to 200%, enlarges the background, contributing to the illusion of movement.
  • -webkit-background-clip and -webkit-text-fill-color: These properties render the text transparent, allowing the animated gradient to shine through.
  • animation: Lastly, we deploy our gradient-shift animation. It uses an ease-in-out timing function for smooth transitions and loops indefinitely, creating an ever-changing cascade of colors.

The Result

And there we have it! Check out the vibrant, animated gradient text:

See the Pen
Animated Gradient Text
by 1stWebDesigner (@firstwebdesigner)
on CodePen.0

Final Thoughts

The process of creating the animated gradient text effect is surprisingly straightforward, but the creative opportunities it unveils are far-reaching. With this foundational knowledge, you can experiment with different color schemes and gradient directions, apply the animation to various elements like buttons or headers, and even incorporate subtle animated accents into your design.

Remember, the real beauty of CSS is in its flexibility and power – it provides a vast canvas for creativity. You could also explore further with CSS keyframes to manipulate other properties and add more dynamic animations to your design. Feel free to dive deeper into the world of CSS animations with our guide on CSS keyframes.

Create Neon Style Buttons Using CSS

CSS truly is a remarkable tool in a web designer’s toolkit, capable of bringing even the most vibrant creative visions to life. Today, we’re immersing ourselves in the radiant world of neon style buttons, showcasing the impressive spectrum of CSS capabilities. Ready to set your CSS knowledge aglow? Let’s get started!

Your Web Designer Toolbox

Unlimited Downloads: 500,000+ Web Templates, Icon Sets, Themes & Design Assets Starting at only $16.50/month!

HTML: Building the Neon Button

Our HTML structure for the neon button is quite straightforward:

<button class="neon-button">NEON</button>

We’ve just set up a button with the class “neon-button” which we’ll use to apply our CSS styles.

CSS: Crafting the Neon Glow

Let’s now dive into the CSS code to give our button that neon look:

/* Load custom font from Google Fonts */
@import url("https://fonts.googleapis.com/css2?family=Montserrat:wght@700&display=swap");

body {
  background-color: #1a1a1a; /* Dark background for neon contrast */
}

/* Styling for our neon button */
.neon-button {
  color: #ff4b59; /* Text color */
  background-color: #1a1a1a; /* Same as the background for a seamless look */
  border: 4px solid #ff4b59; /* Solid border with neon color */
  border-radius: 10px; /* Slight rounding of corners */
  padding: 15px 30px; /* Padding around the text */
  font-size: 25px; /* Visible and impactful text size */
  font-family: "Montserrat", sans-serif; /* Stylish font */
  letter-spacing: 3px; /* Space between letters for better readability */
  cursor: pointer; /* Changes cursor to a pointer on hover */
  font-weight: bold; /* Bold text */
  filter: drop-shadow(0 0 10px #ff4b59) drop-shadow(0 0 30px #ff4b59)
    contrast(1.8) brightness(1.8); /* Adds a subtle glow effect and enhances the vibrancy */
  transition: 0.5s; /* Smooth color change on hover */
}

/* Styling for hover state */
.neon-button:hover {
  color: #1a1a1a; /* Text color changes on hover */
  background-color: #ff4b59; /* Button color fills on hover */
  filter: drop-shadow(0 0 10px #ff4b59) drop-shadow(0 0 40px #ff4b59)
    contrast(1.8) brightness(1.8); /* Glow effect is enhanced on hover */
}

Let’s break down this CSS snippet:

  • Color & Background: We use color to set the text color to #FF4B59, our chosen neon shade. The background-color is set to #1A1A1A, which is a dark tone to enhance the neon glow.
  • Border & Border Radius: We have border set to 4px and the same color as our text to give our button a neon border. The border-radius property is used to give the button slightly rounded corners.
  • Font Size & Family: font-size is set to 25px to ensure our text is large enough to be impactful, and font-family is set to ‘Montserrat’, a stylish sans-serif font, to give our text an appealing look.
  • Letter Spacing & Font Weight: We used letter-spacing to provide some space between letters for better readability, and font-weight is set to bold for more emphasis.
  • Filter & Transition: The filter property is employed to apply the drop-shadow function twice to create a glowing effect around the text and the border. This glow effect intensifies upon hovering. The transition property ensures a smooth transformation of colors when the button is hovered over.

The Result

See the Pen
Neon Style Button
by 1stWebDesigner (@firstwebdesigner)
on CodePen.0

Final Thoughts

This approach provides a straightforward way to create a neon-style button. However, it’s only one of many possible techniques.

In the broader scope of CSS, there are numerous ways to enhance this effect. For instance, using transform property for animated scaling effects, controlling opacity for more depth, using CSS variables for easier management of values, and leveraging pseudo-elements like :before and :after for more complex effects.

If the neon button is meant to serve as a link, it might be more semantically appropriate and beneficial for SEO to use an <a> element instead of a <button>.

Also, to make designs more responsive, consider using relative units like em or rem instead of px, which allows for more fluid scaling across different screen sizes.

Playing around with different box-shadow values can lead to different glow intensities and spread. Combining all these methods can yield an even more impressive and dynamic neon button.

Don’t hesitate to take what you’ve learned here and push it a step further. CSS is full of such opportunities for those willing to explore!

CSS Keyframes: From Static to Dynamic Designs

Web designers often seek tools that can bring static elements to life, and CSS keyframes are a great ally for this task. Keyframes enable us to animate elements over a certain duration, providing our designs with a dynamic feel. Below, we’ll cover the basics of using keyframes, from defining animations to applying them to our elements.

Your Web Designer Toolbox

Unlimited Downloads: 500,000+ Web Templates, Icon Sets, Themes & Design Assets Starting at only $16.50/month!

Understanding the Structure of CSS Keyframes

At the core of every CSS animation are keyframes, which define the stages of an animation sequence. Keyframes are declared using the @keyframes rule, followed by an animation name of your choice. The name we use in the example below, changeBackground, is arbitrary – you could name it anything that suits your needs.

Here’s an illustration:

/* Keyframe declaration */
@keyframes changeBackground {
  0%   { background: #ff0000; } /* Red at the start */
  50%  { background: #00ff00; } /* Green in the middle */
  100% { background: #0000ff; } /* Blue at the end */
}

The changeBackground keyframe dictates how the background color of an element will transition during the animation. At the start of the animation (0%), the background is red. At the midway point (50%), the background changes to green. Finally, at the end of the animation (100%), the background transitions to blue.

Applying CSS Keyframes to an Element

Now, let’s apply our keyframes to an HTML element using the animation shorthand property:

/* Applying keyframe to an element */
.myElement {
  animation: changeBackground 2s ease-in-out 1s infinite alternate;
}

In this case, we’ve applied the changeBackground keyframe to an element with the .myElement class. The animation alters the background color of this element over a defined period, according to the stages we set in the keyframe.

Dissecting the Animation Shorthand

The animation shorthand property encapsulates several animation-related properties:

/* The animation shorthand */
animation: changeBackground 2s ease-in-out 1s infinite alternate;
  • changeBackground: The keyframe we defined earlier.
  • 2s: One cycle of the animation will last 2 seconds.
  • ease-in-out: The pace of the animation, starting slow, becoming fast in the middle, and then ending slow.
  • 1s: The animation will start after a delay of 1 second.
  • infinite: The animation will repeat indefinitely.
  • alternate: The animation will alternate directions each cycle.

These are the most commonly used properties but remember that you can also specify animation-fill-mode, animation-play-state, and more. Each property can also be specified separately if you want more control over the animation.

Manipulating Animation Timeline with Percentages and Keywords

Keyframe animations allow changes in style to be dictated using either percentages or the from and to keywords. from represents the start (0%), and to represents the end (100%) of the animation:

/* Keyframe declaration using keywords */
@keyframes fadeInOut {
from { opacity: 0; } /* The element is fully transparent at the start */
to { opacity: 1; } /* The element is fully visible at the end */
}

.myElement {
  animation: fadeInOut 3s ease-in-out 1s infinite alternate;
}

In the fadeInOut keyframe above, we’re changing the element’s opacity. It starts with being fully transparent (opacity: 0) and transitions to being fully visible (opacity: 1). The from and to keywords can be used interchangeably with 0% and 100%, respectively.

So, when this animation is applied to .myElement, the element will gradually fade in over a 3-second duration, from being completely transparent to fully visible. After a 1-second delay, the process will reverse, causing the element to fade out, creating an ongoing cycle of fading in and out due to the infinite and alternate keywords.

Bringing It All Together

Let’s look at a slightly more detailed example:

/* Keyframe declaration */
@keyframes spin {
  0% { transform: rotate(0deg); } /* Element starts at its original position */
  50% { transform: rotate(180deg); } /* Rotates 180 degrees halfway through the animation */
  100% { transform: rotate(360deg); } /* Completes a full rotation at the end */
}

.box {
  width: 100px;
  height: 100px;
  background: #FF4B59; /* Specific shade of red */
  animation: spin 2s linear infinite; /* Applies the spin animation */
}

And here’s our HTML element:

<div class="box"></div>

In the above example, we define an animation named spin that rotates an element. We apply this animation to a <div> element with the class .box. This <div> is a square with a specific shade of red. It will continue to rotate, creating a loop because of the infinite keyword. The transform property with the rotate() function is used to alter the position of the element, providing the rotation effect. The linear keyword ensures that the rotation speed is consistent throughout the animation.

See the Pen CSS Text Embossing Effect by 1stWebDesigner (@firstwebdesigner) on CodePen.0

Conclusion

CSS keyframes form the foundation of most CSS animations. Naturally, there’s more to learn and experiment with beyond the aspects we covered. For instance, consider exploring the steps() function in CSS animations, which allows you to break your animation into segments, giving you “frame by frame” control.

When it comes to interactive animations, JavaScript can be combined with CSS keyframes to trigger animations based on user actions like clicks or scrolls. Meanwhile, SVG animations offer more complex graphical animations beyond standard HTML elements, allowing you to animate individual parts of an SVG image for intricate visual effects.

As your understanding of CSS keyframes deepens, you’ll be able to leverage them more effectively to improve your designs and their user experience. Consider using animations for user guidance, interaction feedback, or simply to elevate your designs.

However, remember that animations can be resource-intensive. Strive for a balance between the aesthetic appeal of animations and your website’s performance. Techniques such as reducing the number of animated properties or minimizing the number of keyframes can help you achieve this balance.

How to Create a CSS Text Embossing Effect

Embossing is a graphical effect used to give the impression that the surface of an image has been raised or pressed in. In web design, an embossed text effect can give your typography a three-dimensional look and feel, often lending an elegant and sophisticated touch to your web pages. With the power of CSS, we can create an embossing text effect without the need for any images or additional software. Let’s explore how to accomplish this.

Your Designer Toolbox Unlimited Downloads: 500,000+ Web Templates, Icon Sets, Themes & Design Assets

HTML Setup

We start with a basic HTML setup – a <div> element with a class of embossed-text:

<div class="embossed-text">
  Embossed
</div>

Creating the CSS Text Embossing Effect

Next, we turn our attention to the CSS, which gives us the desired embossing effect. We’re using the bold and distinctive Truculenta font:

@import url("https://fonts.googleapis.com/css2?family=Truculenta:wght@900&display=swap");

.embossed-text {
 font-family: "Truculenta", sans-serif; /* Set the font to Truculenta */
 font-size: 4em; /* Increase the text size */
 background: #f8bf32; /* Set the warm, summer-like background color */
 color: #2b1e0d; /* Set a rich dark color for the text */
 text-align: center; /* Center align the text */
 padding: 50px; /* Add padding around the text */
 box-shadow: 3px 3px 5px rgba(0, 0, 0, 0.3); /* Create depth with a box shadow */
 text-shadow: -2px -2px 1px rgba(255, 255, 255, 0.6),
  3px 3px 3px rgba(0, 0, 0, 0.4); /* Create the embossed effect */
}

Let’s break down each CSS property:

  • font-family: 'Truculenta', sans-serif; – This sets our text font to Truculenta, a bold and punchy font that is excellent for effects like this.
  • font-size: 4em; – This sets the size of our text, making it large enough and noticeable. An embossed effect works well with larger font sizes, and 4em is a suitable size for demonstration.
  • background: #F8BF32; and color: #2B1E0D; – These set the background color of our container to a warm summer color, and the text color to a rich dark tone. The contrast between the two colors enhances the embossed effect.
  • text-align: center; and padding: 50px; – These center our text and provide padding around it, ensuring the embossed text is well-positioned and well-spaced.
  • box-shadow: 3px 3px 5px rgba(0, 0, 0, 0.3); – This adds a box shadow around our container, enhancing the depth effect.
  • text-shadow: -2px -2px 1px rgba(255, 255, 255, 0.6), 3px 3px 3px rgba(0, 0, 0, 0.4); – This property is the main focus, creating the embossed effect. The text-shadow property is defined by two shadows here:
    • A light shadow is positioned at the top left (-2px -2px 1px rgba(255, 255, 255, 0.6)). This acts like a light source, contributing to the illusion of depth.
    • A darker shadow is applied at the bottom right (3px 3px 3px rgba(0, 0, 0, 0.4)). This adds to the effect by mimicking a shadow, further enhancing the embossed look.

Through these simple steps, you’ve created an embossed text effect using CSS.

The Result

See the Pen
Spinner Loader with Pure CSS
by 1stWebDesigner (@firstwebdesigner)
on CodePen.0

Final Thoughts

Adding an embossed effect to your text with CSS can introduce a subtle, tactile element to your website. As a designer, it’s one more tool in your toolkit to help differentiate your site. Remember, though, that like all design elements, it should be used thoughtfully and not in excess. It works best when applied to headers or highlighted text, where it can add emphasis without being overbearing.

The beauty of CSS lies in its flexibility and depth. With some experimentation, you can adapt this CSS text embossing effect to suit your design aesthetic. Enjoy exploring the possibilities!

Useful DevTools Tips and Tricks

When it comes to browser DevTools, we all have our own preferences and personal workflows, and we pride ourselves in knowing that “one little trick” that makes our debugging lives easier.

But also — and I know this from having worked on DevTools at Mozilla and Microsoft for the past ten years — most people tend to use the same three or four DevTools features, leaving the rest unused. This is unfortunate as there are dozens of panels and hundreds of features available in DevTools across all browsers, and even the less popular ones can be quite useful when you need them.

As it turns out, I’ve maintained the DevTools Tips website for the past two years now. More and more tips get added over time, and traffic keeps growing. I recently started tracking the most popular tips that people are accessing on the site, and I thought it would be interesting to share some of this data with you!

So, here are the top 15 most popular DevTools tips from the website.

If there are other tips that you love and that make you more productive, consider sharing them with our community in the comments section!

Let’s count down, starting with…

15: Zoom DevTools

If you’re like me, you may find the text and buttons in DevTools too small to use comfortably. I know I’m not alone here, judging by the number of people who ask our team how to make them bigger!

Well, it turns out you can actually zoom into the DevTools UI.

DevTools’ user interface is built with HTML, CSS, and JavaScript, which means that it’s rendered as web content by the browser. And just like any other web content in browsers, it can be zoomed in or out by using the Ctrl+ and Ctrl- keyboard shortcuts (or Cmd+ and Cmd- on macOS).

So, if you find the text in DevTools too small to read, click anywhere in DevTools to make sure the focus is there, and then press Ctrl+ (or Cmd+ on macOS).

Chromium-based browsers such as Chrome, Edge, Brave, or Opera can also display the font used by an element that contains the text:

  • Select an element that only contains text children.
  • Open the Computed tab in the sidebar of the Elements tool.
  • Scroll down to the bottom of the tab.
  • The rendered fonts are displayed.

Note: To learn more, see “List the fonts used on a page or an element.”

12: Measure Arbitrary Distances On A Page

Sometimes it can be useful to quickly measure the size of an area on a webpage or the distance between two things. You can, of course, use DevTools to get the size of any given element. But sometimes, you need to measure an arbitrary distance that may not match any element on the page.

When this happens, one nice way is to use Firefox’s measurement tool:

  1. If you haven’t done so already, enable the tool. This only needs to be done once: Open DevTools, go into the Settings panel by pressing F1 and, in the Available Toolbox Buttons, check the Measure a portion of the page option.
  2. Now, on any page, click the new Measure a portion of the page icon in the toolbar.
  3. Click and drag with the mouse to measure distances and areas.

Note: To learn more, see “Measure arbitrary distances in the page.”

11: Detect Unused Code

One way to make a webpage appear fast to your users is to make sure it only loads the JavaScript and CSS dependencies it truly needs.

This may seem obvious, but today’s complex web apps often load huge bundles of code, even when only a small portion is needed to render the first page.

In Chromium-based browsers, you can use the Coverage tool to identify which parts of your code are unused. Here is how:

  1. Open the Coverage tool. You can use the Command Menu as a shortcut: press Ctrl+Shift+P (or Cmd+Shift+P on macOS), type “coverage” and then press Enter.)
  2. Click Start instrumenting coverage and refresh the page.
  3. Wait for the page to reload and for the coverage report to appear.
  4. Click any of the reported files to open them in the Sources tool.

The file appears in the tool along with blue and red bars that indicate whether a line of code is used or unused, respectively.

Note: To learn more, see “Detect unused CSS and JavaScript code.”

10: Change The Playback Rate Of A Video

Usually, when a video appears on a webpage, the video player that displays it also provides buttons to control its playback, including a way to speed it up or slow it down. But that’s not always the case.

In cases when the webpage makes it difficult or impossible to control a video, you can use DevTools to control it via JavaScript istead.

  1. Open DevTools.
  2. Select the <video> element in the Elements tool (called Inspector in Firefox).
  3. Open the Console tool.
  4. Type the following: $0.playbackRate = 2; and press Enter.

The $0 expression is a shortcut that refers to whatever element is currently selected in DevTools; in this case, it refers to the <video> HTML element.

By using the playbackRate property of the <video> element, you can speed up or slow down the video. Note that you could also use any of the other <video> element properties or methods, such as:

  • $0.pause() to pause the video;
  • $0.play() to resume playing the video;
  • $0.loop = true to repeat the video in a loop.

Note: To learn more, see “Speed up or slow down a video.”

9: Use DevTools In Another Language

If, like me, English isn’t your primary language, using DevTools in English might make things harder for you.

If that’s your case, know that you can actually use a translated version of DevTools that either matches your operating system, your browser, or a language of your choice.

The procedure differs per browser.

In Safari, both the browser and Web Inspector (which is what DevTools is called in Safari) inherit the language of the operating system. So if you want to use a different language for DevTools, you’ll need to set it globally by going into System preferencesLanguage & RegionApps.

In Firefox, DevTools always matches the language of the browser. So, if you want to use DevTools in, say, French, then download Firefox in French.

Finally, in Chrome or Edge, you can choose to either match the language of the browser or set a different language just for DevTools.

To make your choice:

  1. Open DevTools and press F1 to open the Settings.
  2. In the Language drop-down, choose either Browser UI language to match the browser language or choose another language from the list.

Note: To learn more, see “Use DevTools in another language.”

8: Disable Event Listeners

Event listeners can sometimes get in the way of debugging a webpage. If you’re investigating a particular issue, but every time you move your mouse or use the keyboard, unrelated event listeners are triggered, this could make it harder to focus on your task.

A simple way to disable an event listener is by selecting the element it applies to in the Elements tool (or Inspector in Firefox). Once you’ve found and selected the element, do either of the following:

  • In Firefox, click the event badge next to the element, and in the popup that appears, uncheck the listeners you want to disable.
  • In Chrome or Edge, click the Event Listeners tab in the sidebar panel, find the listener you want to remove, and click Remove.

Note: To learn more, see “Remove or disable event listeners.”

7: View Console Logs On Non-Safari Browsers On iOS

As you might know, Safari isn’t the only browser you can install and use on an iOS device. Firefox, Chrome, Edge, and others can also be used. Technically, they all run on the same underlying browser rendering engine, WebKit, so a website should more or less look the same in all of these browsers in iOS.

However, it’s possible to have bugs on other browsers that don’t replicate in Safari. This can be quite tricky to investigate. While it’s possible to debug Safari on an iOS device by attaching the device to a Mac with a USB cable, it’s impossible to debug non-Safari browsers.

Thankfully, there is a way to at least see your console logs in Chrome and Edge (and possibly other Chromium-based browsers) when using iOS:

  1. Open Chrome or Edge on your iOS device and go to the special about:inspect page.
  2. Click Start Logging.
  3. Keep this tab open and then open another one.
  4. In the new tab, go to the page you’re trying to debug.
  5. Return to the previous tab. Your console logs should now be displayed.

Note: To learn more, see “View console logs from non-Safari browsers on an iPhone.”

6: Copy Element Styles

Sometimes it’s useful to extract a single element from a webpage, maybe to test it in isolation. To do this, you’ll first need to extract the element’s HTML code via the Elements tool by right-clicking the element and choosing CopyCopy outer HTML.

Extracting the element’s styles, however, is a bit more difficult as it involves going over all of the CSS rules that apply to the element.

Chrome, Edge, and other Chromium-based browsers make this step a lot faster:

  1. In the Elements tool, select the element you want to copy styles from.
  2. Right-click the selected element.
  3. Click CopyCopy styles.
  4. Paste the result in your text editor.

You now have all the styles that apply to this element, including inherited styles and custom properties, in a single list.

Note: To learn more, see “Copy an element’s styles.”

5: Download All Images On The Page

This nice tip isn’t specific to any browser and can be run anywhere as long as you can execute JavaScript. If you want to download all of the images that are on a webpage, open the Console tool, paste the following code, and press Enter:

$$('img').forEach(async (img) => {
 try {
   const src = img.src;
   // Fetch the image as a blob.
   const fetchResponse = await fetch(src);
   const blob = await fetchResponse.blob();
   const mimeType = blob.type;
   // Figure out a name for it from the src and the mime-type.
   const start = src.lastIndexOf('/') + 1;
   const end = src.indexOf('.', start);
   let name = src.substring(start, end === -1 ? undefined : end);
   name = name.replace(/[^a-zA-Z0-9]+/g, '-');
   name += '.' + mimeType.substring(mimeType.lastIndexOf('/') + 1);
   // Download the blob using a <a> element.
   const a = document.createElement('a');
   a.setAttribute('href', URL.createObjectURL(blob));
   a.setAttribute('download', name);
   a.click();
 } catch (e) {}
});

Note that this might not always succeed: the CSP policies in place on the web page may cause some of the images to fail to download.

If you happen to use this technique often, you might want to turn this into a reusable snippet of code by pasting it into the Snippets panel, which can be found in the left sidebar of the Sources tool in Chromium-based browsers.

In Firefox, you can also press Ctrl+I on any webpage to open Page Info, then go to Media and select Save As to download all the images.

Note: To learn more, see “Download all images from the page.”

4: Visualize A Page In 3D

The HTML and CSS code we write to create webpages gets parsed, interpreted, and transformed by the browser, which turns it into various tree-like data structures like the DOM, compositing layers, or the stacking context tree.

While these data structures are mostly internal in-memory representations of a running webpage, it can sometimes be helpful to explore them and make sure things work as intended.

A three-dimensional representation of these structures can help see things in a way that other representations can’t. Plus, let’s admit it, it’s cool!

Edge is the only browser that provides a tool dedicated to visualizing webpages in 3D in a variety of ways.

  1. The easiest way to open it is by using the Command Menu. Press Ctrl+Shift+P (or Cmd+Shift+P on macOS), type “3D” and then press Enter.
  2. In the 3D View tool, choose between the three different modes: Z-Index, DOM, and Composited Layers.
  3. Use your mouse cursor to pan, rotate, or zoom the 3D scene.

The Z-Index mode can be helpful to know which elements are stacking contexts and which are positioned on the z-axis.

The DOM mode can be used to easily see how deep your DOM tree is or find elements that are outside of the viewport.

The Composited Layers mode shows all the different layers the browser rendering engine creates to paint the page as quickly as possible.

Consider that Safari and Chrome also have a Layers tool that shows composited layers.

Note: To learn more, see “See the page in 3D.”

3: Disable Abusive Debugger Statements

Some websites aren’t very nice to us web developers. While they seem normal at first, as soon as you open DevTools, they immediately get stuck and pause at a JavaScript breakpoint, making it very hard to inspect the page!

These websites achieve this by adding a debugger statement in their code. This statement has no effect as long as DevTools is closed, but as soon as you open it, DevTools pauses the website’s main thread.

If you ever find yourself in this situation, here is a way to get around it:

  1. Open the Sources tool (called Debugger in Firefox).
  2. Find the line where the debugger statement is. That shouldn’t be hard since the debugger is currently paused there, so it should be visible right away.
  3. Right-click on the line number next to this line.
  4. In the context menu, choose Never pause here.
  5. Refresh the page.

Note: To learn more, see “Disable abusive debugger statements that prevent inspecting websites.”

2: Edit And Resend Network Requests

When working on your server-side logic or API, it may be useful to send a request over and over again without having to reload the entire client-side webpage and interact with it each time. Sometimes you just need to tweak a couple of request parameters to test something.

One of the easiest ways to do this is by using Edge’s Network Console tool or Firefox’s Edit and Resend feature of the Network tool. Both of them allow you to start from an existing request, modify it, and resend it.

In Firefox:

  • Open the Network tool.
  • Right-click the network request you want to edit and then click Edit and Resend.
  • A new sidebar panel opens up, which lets you change things like the URL, the method, the request parameters, and even the body.
  • Change anything you need and click Send.

In Edge:

  • First, enable the Network Console tool by going into the Settings panel (press F1) → ExperimentsEnable Network Console.
  • Then, in the Network tool, find the request you want to edit, right-click it and then click Edit and Resend.
  • The Network Console tool appears, which lets you change the request just like in Firefox.
  • Make the changes you need, and then click Send.

Here is what the feature looks like in Firefox:

Note: To learn more, see “Edit and resend faulty network requests to debug them.”

If you need to resend a request without editing it first, you can do so too. (See: Replay a XHR request)

And the honor of being the Number One most popular DevTools tip in this roundup goes to… 🥁

1: Simulate Devices

This is, by far, the most widely viewed DevTools tip on my website. I’m not sure why exactly, but I have theories:

  • Cross-browser and cross-device testing remain, to this day, one of the most important pain points that web developers face, and it’s nice to be able to simulate other devices from the comfort of your development browser.
  • People might be using it to achieve non-dev tasks. For example, people use it to post photos on Instagram from their laptops or desktop computers!

It’s important to realize, though, that DevTools can’t simulate what your website will look like on another device. Underneath it, it is all still the same browser rendering engine. So, for example, when you simulate an iPhone by using Firefox’s Responsive Design Mode, the page still gets rendered by Firefox’s rendering engine, Gecko, rather than Safari’s rendering engine, WebKit.

Always test on actual browsers and actual devices if you don’t want your users to stumble upon bugs you could have caught.

That being said,

Simulating devices in DevTools is very useful for testing how a layout works at different screen sizes and device pixel ratios. You can even use it to simulate touch inputs and other user agent strings.

Here are the easiest ways to simulate devices per browser:

  • In Safari, press Ctrl+Cmd+R, or click Develop in the menu bar and then click Enter Responsive Design Mode.
  • In Firefox, press Ctrl+Shift+M (or Cmd+Shift+M), or use the browser menu → More toolsResponsive design mode.
  • In Chrome or Edge, open DevTools first, then press Ctrl+Shift+M (or Cmd+Shift+M), or click the Device Toolbar icon.

Here is how simulating devices looks in Safari:

Note: To learn more, see “Simulate different devices and screen sizes.”

Finally, if you find yourself simulating screen sizes often, you might be interested in using Polypane. Polypane is a great development browser that lets you simulate multiple synchronized viewports at the same time, so you can see how your website renders at different sizes at the same time.

Polypane comes with its own set of unique features, which you can also find on DevTools Tips.

Conclusion

I’m hoping you can see now that DevTools is very versatile and can be used to achieve as many tasks as your imagination allows. Whatever your debugging use case is, there’s probably a tool that’s right for the job. And if there isn’t, you may be able to find out what you need to know by running JavaScript in the Console!

If you’ve discovered cool little tips that come in handy in specific situations, please share them in the comments section, as they may be very useful to others too.

Further Reading on Smashing Magazine