Tag: Custom

Let’s Create a Custom Audio Player

HTML has a built-in native audio player interface that we get simply using the <audio> element. Point it to a sound file and that’s all there is to it. We even get to specify multiple files for better browser support, as well as a little CSS flexibility to style things up, like giving the audio player a border, some rounded corners, and maybe a little padding and margin.

But even with all that… the rendered audio player itself can look a little, you know, plain.

Did you know it’s possible to create a custom audio player? Of course we can! While the default <audio> player is great in many cases, having a custom player might suit you better, like if you run a podcast and an audio player is the key element on a website for the podcast. Check out the sweet custom player Chris and Dave set up over at the ShopTalk Show website.

Showing a black audio player with muted orange controls, including options to jump or rewind 30 seconds on each side of a giant play button, which sits on top of a timeline showing the audio current time and duration at both ends of the timeline, and an option to set the audio speed in the middle.
The audio player fits in seamlessly with other elements on the page, sporting controls that complement the overall design.

We’re going to take stab at making our own player in this post. So, put on your headphones, crank up some music, and let’s get to work!

The elements of an audio player

First, let’s examine the default HTML audio players that some of the popular browsers provide.

Google Chrome, Opera, and Microsoft Edge
Blink

Mozilla Firefox
Firefox

Internet Explorer
Internet Explorer

If our goal is to match the functionality of these examples, then we need to make sure our player has:

  • a play/pause button,
  • a seek slider,
  • the current time indicator,
  • the duration of the sound file,
  • a way to mute the audio, and
  • a volume control slider.

Let’s say this is the design we’re aiming for:

We’re not going for anything too fancy here: just a proof of concept sorta thing that we can use to demonstrate how to make something different than what default HTML provides.

Basic markup, styling and scripts for each element

We should first go through the semantic HTML elements of the player before we start building features and styling things. We have plenty of elements to work with here based on the elements we just listed above.

Play/pause button

I think the HTML element appropriate for this button is the <button> element. It will contain the play icon, but the pause icon should also be in this button. That way, we’re toggling between the two rather than taking up space by displaying both at the same time.

Something like this in the markup:

<div id="audio-player-container">   <p>Audio Player</p>   <!-- swaps with pause icon -->   <button id="play-icon"></button> </div>

So, the question becomes: how do we swap between the two buttons, both visually and functionally? the pause icon will replace the play icon when the play action is triggered. The play button should display when the audio is paused and the pause button should display when the audio is playing.

Of course, a little animation could take place as the icon transitions from the play to pause. What would help us accomplish that is Lottie, a library that renders Adobe After Effects animations natively. We don’t have to create the animation on After Effects though. The animated icon we’re going to use is provided for free by Icons8.

New to Lottie? I wrote up a thorough overview that covers how it works.

In the meantime, permit me to describe the following Pen:

The HTML section contains the following:

  • a container for the player,
  • text that briefly describes the container, and
  • a <button> element for the play and pause actions.

The CSS section includes some light styling. The JavaScript is what we need to break down a bit because it’s doing several things:

// imports the Lottie library via Skypack import lottieWeb from 'https://cdn.skypack.dev/lottie-web';  // variable for the button that will contain both icons const playIconContainer = document.getElementById('play-icon'); // variable that will store the button’s current state (play or pause) let state = 'play';  // loads the animation that transitions the play icon into the pause icon into the referenced button, using Lottie’s loadAnimation() method const animation = lottieWeb.loadAnimation({   container: playIconContainer,   path: 'https://maxst.icons8.com/vue-static/landings/animated-icons/icons/pause/pause.json',   renderer: 'svg',   loop: false,   autoplay: false,   name: "Demo Animation", });  animation.goToAndStop(14, true);  // adds an event listener to the button so that when it is clicked, the the player toggles between play and pause playIconContainer.addEventListener('click', () => {   if(state === 'play') {     animation.playSegments([14, 27], true);     state = 'pause';   } else {     animation.playSegments([0, 14], true);     state = 'play';   } });

Here’s what the script is doing, minus the code:

  • It imports the Lottie library via Skypack.
  • It references the button that will contain both icons in a variable.
  • It defines a variable that will store the button’s current state (play or pause).
  • It loads the animation that transitions the play icon into the pause icon into the referenced button, using Lottie’s loadAnimation() method.
  • It displays the play icon on load since the audio is initially paused.
  • It adds an event listener to the button so that when it is clicked, the the player toggles between play and pause.

Current time and duration

The current time is like a progress indicate that shows you how much time has elapsed from the start of the audio file. The duration? That’s just how long the sound file is.

A <span> element is okay to display these. The <span> element for the current time, which is to be updated every second, has a default text content of 0:00. On the other side, the one for duration is the duration of the audio in mm:ss format.

<div id="audio-player-container">   <p>Audio Player</p>   <button id="play-icon"></button>   <span id="current-time" class="time">0:00</span>   <span id="duration" class="time">0:00</span> </div>

Seek slider and volume control slider

We need a way to move to any point in time in the sound file. So, if I want to skip ahead to the halfway point of the file, I can simply click and drag a slider to that spot in the timeline.

We also need a way to control the sound volume. That, too, can be some sort of click-and-drag slider thingy.

I would say <input type="range"> is the right HTML element for both of these features.

<div id="audio-player-container">   <p>Audio Player</p>   <button id="play-icon"></button>   <span id="current-time" class="time">0:00</span>   <input type="range" id="seek-slider" max="100" value="0">   <span id="duration" class="time">0:00</span>   <input type="range" id="volume-slider" max="100" value="100"> </div>

Styling range inputs with CSS is totally possible, but I’ll tell you what: it is difficult for me to wrap my head around. This article will help. Mad respect to you, Ana. Handling browser support with all of those vendor prefixes is a CSS trick in and of itself. Look at all the code needed on input[type="range"] to get a consistent experience:

input[type="range"] {   position: relative;   -webkit-appearance: none;   width: 48%;   margin: 0;   padding: 0;   height: 19px;   margin: 30px 2.5% 20px 2.5%;   float: left;   outline: none; } input[type="range"]::-webkit-slider-runnable-track {   width: 100%;   height: 3px;   cursor: pointer;   background: linear-gradient(to right, rgba(0, 125, 181, 0.6) var(--buffered-width), rgba(0, 125, 181, 0.2) var(--buffered-width)); } input[type="range"]::before {   position: absolute;   content: "";   top: 8px;   left: 0;   width: var(--seek-before-width);   height: 3px;   background-color: #007db5;   cursor: pointer; } input[type="range"]::-webkit-slider-thumb {   position: relative;   -webkit-appearance: none;   box-sizing: content-box;   border: 1px solid #007db5;   height: 15px;   width: 15px;   border-radius: 50%;   background-color: #fff;   cursor: pointer;   margin: -7px 0 0 0; } input[type="range"]:active::-webkit-slider-thumb {   transform: scale(1.2);   background: #007db5; } input[type="range"]::-moz-range-track {   width: 100%;   height: 3px;   cursor: pointer;   background: linear-gradient(to right, rgba(0, 125, 181, 0.6) var(--buffered-width), rgba(0, 125, 181, 0.2) var(--buffered-width)); } input[type="range"]::-moz-range-progress {   background-color: #007db5; } input[type="range"]::-moz-focus-outer {   border: 0; } input[type="range"]::-moz-range-thumb {   box-sizing: content-box;   border: 1px solid #007db5;   height: 15px;   width: 15px;   border-radius: 50%;   background-color: #fff;   cursor: pointer; } input[type="range"]:active::-moz-range-thumb {   transform: scale(1.2);   background: #007db5; } input[type="range"]::-ms-track {   width: 100%;   height: 3px;   cursor: pointer;   background: transparent;   border: solid transparent;   color: transparent; } input[type="range"]::-ms-fill-lower {   background-color: #007db5; } input[type="range"]::-ms-fill-upper {   background: linear-gradient(to right, rgba(0, 125, 181, 0.6) var(--buffered-width), rgba(0, 125, 181, 0.2) var(--buffered-width)); } input[type="range"]::-ms-thumb {   box-sizing: content-box;   border: 1px solid #007db5;   height: 15px;   width: 15px;   border-radius: 50%;   background-color: #fff;   cursor: pointer; } input[type="range"]:active::-ms-thumb {   transform: scale(1.2);   background: #007db5; }

Whoa! What does even mean, right?

Styling the progress section of range inputs is a tricky endeavor. Firefox provides the ::-moz-range-progress pseudo-element while Internet Explorer provides ::-ms-fill-lower. As WebKit browsers do not provide any similar pseudo-element, we have to use the ::before pseudo-element to improvise the progress. That explains why, if you noticed, I added event listeners in the JavaScript section to set custom CSS properties (e.g. --before-width) that update when the input event is fired on each of the sliders.

One of the native HTML <audio> examples we looked at earlier shows the buffered amount of the audio. The --buffered-width property specifies the amount of the audio, in percentage, that the user can play through to without having to wait for the browser to download. I imitated this feature with the linear-gradient() function on the track of the seek slider. I used the rgba() function in the linear-gradient() function for the color stops to show transparency. The buffered width has a much deeper color compared to the rest of the track. However, we would treat the actual implementation of this feature much later.

Volume percentage

This is to display the percentage volume. The text content of this element is updated as the user changes the volume through the slider. Since it is based on user input, I think this element should be the <output> element.

<div id="audio-player-container">   <p>Audio Player</p>   <button id="play-icon"></button>   <span id="current-time" class="time">0:00</span>   <input type="range" id="seek-slider" max="100" value="0">   <span id="duration" class="time">0:00</span>   <output id="volume-output">100</output>   <input type="range" id="volume-slider" max="100" value="100"> </div>

Mute button

Like for the play and pause actions, this should be in a <button> element. Luckily for us, Icons8 also has an animated mute icon. So we would use the Lottie library here just as we did for the play/pause button.

<div id="audio-player-container">   <p>Audio Player</p>   <button id="play-icon"></button>   <span id="current-time" class="time">0:00</span>   <input type="range" id="seek-slider" max="100" value="0">   <span id="duration" class="time">0:00</span>   <output id="volume-output">100</output>   <input type="range" id="volume-slider" max="100" value="100">   <button id="mute-icon"></button> </div>

That’s all of the basic markup, styling and scripting we need at the moment!

Working on the functionality

The HTML <audio> element has a preload attribute. This attribute gives the browser instructions for how to load the audio file. It accepts one of three values:

  • none – indicates that the browser should not load the audio at all (unless the user initiates the play action)
  • metadata – indicates that only the metadata (like length) of the audio should be loaded
  • auto – loads the complete audio file

An empty string is equivalent to the auto value. Note, however, that these values are merely hints to the browser. The browser does not have to agree to these values. For example, if a user is on a cellular network on iOS, Safari does not load any part of an audio, regardless of the preload attribute, except the user triggers the play action. For this player, we would use the metadata value since it doesn’t require much overhead and we want to display the length of the audio.

What would help us accomplish the features our audio player should have is the JavaScript HTMLMediaElement interface, which the HTMLAudioElement interface inherits. For our audio player code to be as self-explanatory as possible, I’d divide the JavaScript into two sections: presentation and functionality.

First off, we should create an <audio> element in the audio player that has the basic features we want:

<div id=”audio-player-container”>   <audio src=”my-favourite-song.mp3” preload=”metadata” loop>   <button id="play-icon"></button>   <!-- ... --> </div>

Display the audio duration

The first thing we want to display on the browser is the duration of the audio, when it is available. The HTMLAudioElement interface has a duration property, which returns the duration of the audio, returned in seconds units. If it is unavailable, it returns NaN.

We’ve set preload to metadata in this example, so the browser should provide us that information up front on load… assuming it respects preload. Since we’d be certain that the duration will be available when the browser has downloaded the metadata of the audio, we display it in our handler for the loadedmetadata event, which the interface also provides:

const audio = document.querySelector('audio');  audio.addEventListener('loadedmetadata', () => {   displayAudioDuration(audio.duration); });

That’s great but, again, we get the duration in second. We probably should convert that to a mm:ss format:

const calculateTime = (secs) => {   const minutes = Math.floor(secs / 60);   const seconds = Math.floor(secs % 60);   const returnedSeconds = seconds < 10 ? `0$ {seconds}` : `$ {seconds}`;   return `$ {minutes}:$ {returnedSeconds}`; }

We’re using Math.floor() because the seconds returned by the duration property typically come in decimals.

The third variable, returnedSeconds, is necessary for situations where the duration is something like 4 minutes and 8 seconds. We would want to return 4:08, not 4:8.

More often than not, the browser loads the audio faster than usual. When this happens, the loadedmetadata event is fired before its listener can be added to the <audio> element. Therefore, the audio duration is not displayed on the browser. Nevertheless, there’s a hack. The HTMLMediaElement has a property called readyState. It returns a number that, according to MDN Web Docs, indicates the readiness state of the media. The following describes the values:

  • 0 – no data about the media is available.
  • 1 – the metadata attributes of the media are available.
  • 2 – data is available, but not enough to play more than a frame.
  • 3 – data is available, but for a little amount of frames from the current playback position.
  • 4 – data is available, such that the media can be played through to the end without interruption.

We want to focus on the metadata. So our approach is to display the duration if the metadata of the audio is available. If it is not available, we add the event listener. That way, the duration is always displayed.

const audio = document.querySelector('audio'); const durationContainer = document.getElementById('duration');  const calculateTime = (secs) => {   const minutes = Math.floor(secs / 60);   const seconds = Math.floor(secs % 60);   const returnedSeconds = seconds < 10 ? `0$ {seconds}` : `$ {seconds}`;   return `$ {minutes}:$ {returnedSeconds}`; }  const displayDuration = () => {   durationContainer.textContent = calculateTime(audio.duration); }  if (audio.readyState > 0) {   displayDuration(); } else {   audio.addEventListener('loadedmetadata', () => {     displayDuration();   }); }

Seek slider

The default value of the range slider’s max property is 100. The general idea is that when the audio is playing, the thumb is supposed to be “sliding.” Also, it is supposed to move every second, such that it gets to the end of the slider when the audio ends.

Notwithstanding, if the audio duration is 150 seconds and the value of the slider’s max property is 100, the thumb will get to the end of the slider before the audio ends. This is why it is necessary to set the value of the slider’s max property to the audio duration in seconds. This way, the thumb gets to the end of the slider when the audio ends. Recall that this should be when the audio duration is available, when the browser has downloaded the audio metadata, as in the following:

const seekSlider = document.getElementById('seek-slider');  const setSliderMax = () => {   seekSlider.max = Math.floor(audio.duration); }  if (audio.readyState > 0) {   displayDuration();   setSliderMax(); } else {   audio.addEventListener('loadedmetadata', () => {     displayDuration();     setSliderMax();   }); }

Buffered amount

As the browser downloads the audio, it would be nice for the user to know how much of it they can seek to without delay. The HTMLMediaElement interface provides the buffered and seekable properties. The buffered property returns a TimeRanges object, which indicates the chunks of media that the browser has downloaded. According to MDN Web Docs, a TimeRanges object is a series of non-overlapping ranges of time, with start and stop times. The chunks are usually contiguous, unless the user seeks to another part in the media. The seekable property returns a TimeRanges object, which indicates “seekable” parts of the media, irrespective of whether they’ve been downloaded or not.

Recall that the preload="metadata" attribute is present in our <audio> element. If, for example the audio duration is 100 seconds, the buffered property returns a TimeRanges object similar to the following:

Displays a line from o to 100, with a marker at 20. A range from 0 to 20 is highlighted on the line.

When the audio has started playing, the seekable property would return a TimeRanges object similar to the following:

Another timeline, but with four different ranges highlighted on the line, 0 to 20, 30 to 40, 60 to 80, and 90 to 100.

It returns multiple chunks of media because, more often than not, byte-range requests are enabled on the server. What this means is that multiple parts of the media can be downloaded simultaneously. However, we want to display the buffered amount closest to the current playback position. That would be the first chunk (time range 0 to 20). That would be the first and last chunk from the first image. As the audio starts playing, the browser begins to download more chunks. We would want to display the one closest to the current playback position, which would be the current last chunk returned by the buffered property. The following snippet would store in the variable, bufferedAmount, i.e. the time for the end of the last range in the TimeRanges object returned by the buffered property.

const audio = document.querySelector('audio'); const bufferedAmount = audio.buffered.end(audio.buffered.length - 1);

This would be 20 from the 0 to 20 range in the first image. The following snippet stores in the variable, seekableAmount, the time for the end of the last range in the TimeRanges object returned by the seekable property.

const audio = document.querySelector('audio'); const seekableAmount = audio.seekable.end(audio.seekable.length - 1);

Nevertheless, this would be 100 from the 90 to 100 range in the second image, which is the entire audio duration. Note that there are some holes in the TimeRanges object as the browser only downloads some parts of the audio. What this means is that the entire duration would be displayed to the user as the buffered amount. Meanwhile, some parts in the audio are not available yet. Because this won’t provide the best user experience, the first snippet is what we should use.

As the browser downloads the audio, the user should expect that the buffered amount on the slider increases in width. The HTMLMediaElement provides an event, the progress event, which fires as the browser loads the media. Of course, I’m thinking what you’re thinking! The buffered amount should be incremented in the handler for the audio’s progress event.

Finally, we should actually display the buffered amount on the seek slider. We do that by setting the property we talked about earlier, --buffered-width, as a percentage of the value of the slider’s max property. Yes, in the handler for the progress event too. Also, because of the browser loading the audio faster than usual, we should update the property in the loadedmetadata event and its preceding conditional block that checks for the readiness state of the audio. The following Pen combines all that we’ve covered so far:

Current time

As the user slides the thumb along the range input, the range value should be reflected in the <span> element containing the current time of the audio. This tells the user the current playback position of the audio. We do this in the handler of the slider’s input event listener.

If you think the correct event to listen to should be the change event, I beg to differ. Say the user moved the thumb from value 0 to 20. The input event fires at values 1 through to 20. However, the change event will fire only at value 20. If we use the change event, it will not reflect the playback position from values 1 to 19. So, I think the input event is appropriate. Then, in the handler for the event, we pass the slider’s value to the calculateTime() function we defined earlier.

We created the function to take time in seconds and return it in a mm:ss format. If you’re thinking, Oh, but the slider’s value is not time in seconds, let me explain. Actually, it is. Recall that we set the value of the slider’s max property to the audio duration, when it is available. Let’s say the audio duration is 100 seconds. If the user slides the thumb to the middle of the slider, the slider’s value will be 50. We wouldn’t want 50 to appear in the current time box because it is not in accordance with the mm:ss format. When we pass 50 to the function, the function returns 0:50 and that would be a better representation of the playback position.

I added the snippet below to our JavaScript.

const currentTimeContainer = document.getElementById('current-time');  seekSlider.addEventListener('input', () => {   currentTimeContainer.textContent = calculateTime(seekSlider.value); }); 

To see it in action, you can move the seek slider’s thumb back and forth in the following Pen:

Play/pause

Now we’re going to set the audio to play or pause according to the respective action triggered by the user. If you recall, we created a variable, playState, to store the state of the button. That variable is what will help us know when to play or pause the audio. If its value is play and the button is clicked, our script is expected to perform the following actions:

  • play the audio
  • change the icon from play to pause
  • change the playState value to pause

We already implemented the second and third actions in the handler for the button’s click event. What we need to do is to add the statements to play and pause the audio in the event handler:

playIconContainer.addEventListener('click', () => {   if(playState === 'play') {     audio.play();     playAnimation.playSegments([14, 27], true);     playState = 'pause';   } else {     audio.pause();     playAnimation.playSegments([0, 14], true);     playState = 'play';   } });

It is possible that the user will want to seek to a specific part in the audio. In that case, we set the value of the audio’s currentTime property to the seek slider’s value. The slider’s change event will come in handy here. If we use the input event, various parts of the audio will play in a very short amount of time.

Recall our scenario of 1 to 20 values. Now imagine the user slides the thumb from 1 to 20 in, say, two seconds. That’s 20 seconds audio playing in two seconds. It’s like listening to Busta Rhymes on 3× speed. I’d suggest we use the change event. The audio will only play after the user is done seeking. This is what I’m talking about:

seekSlider.addEventListener('change', () => {   audio.currentTime = seekSlider.value; });

With that out of the way, something needs to be done while the audio is playing. That is to set the slider’s value to the current time of the audio. Or move the slider’s thumb by one tick every second. Since the audio duration and the slider’s max value are the same, the thumb gets to the end of the slider when the audio ends. Now the timeupdate event of the HTMLMediaElement interface should be the appropriate event for this. This event is fired as the value of the media’s currentTime property is updated, which is approximately four times in one second. So in the handler for this event, we could set the slider’s value to the audio’s current time. This should work just fine:

audio.addEventListener('timeupdate', () => {   seekSlider.value = Math.floor(audio.currentTime); });

However, there are some things to take note of here:

  1. As the audio is playing, and the seek slider’s value is being updated, a user is unable to interact with the slider. If the audio is paused, the slider won’t be able to receive input from the user because it is constantly being updated.
  2. In the handler, we update the value of the slider but its input event does not fire. This is because the event only fires when a user updates the slider’s value on the browser, and not when it is updated programmatically.

Let’s consider the first issue.

To be able to interact with the slider while the audio is playing, we would have to pause the process of updating it’s value when it receives input. Then, when the slider loses focus, we resume the process. But, we don’t have access to this process. My hack would be to use the requestAnimationFrame() global method for the process. But this time, we won’t be using the timeupdate event for this because it still won’t work. The animation would play forever until the audio is paused, and that’s not what we want. Therefore, we use the play/pause button’s click event.

To use the requestAnimationFrame() method for this feature, we have to accomplish these steps:

  1. Create a function to keep our “update slider value” statement.
  2. Initialize a variable in the function that was previously created to store the request ID returned by the function (that will be used to pause the update process).
  3. Add the statements in the play/pause button click event handler to start and pause the process in the respective blocks.

This is illustrated in the following snippet:

let rAF = null;  const whilePlaying = () => {   seekSlider.value = Math.floor(audio.currentTime);   rAF = requestAnimationFrame(whilePlaying); }  playIconContainer.addEventListener('click', () => {   if(playState === 'play') {     audio.play();     playAnimation.playSegments([14, 27], true);     requestAnimationFrame(whilePlaying);     playState = 'pause';   } else {     audio.pause();     playAnimation.playSegments([0, 14], true);     cancelAnimationFrame(rAF);     playState = 'play';   } });

But this doesn’t exactly solve our problem. The process is only paused when the audio is paused. We also need to pause the process, if it is in execution (i.e. if the audio is playing), when the user wants to interact with the slider. Then, after the slider loses focus, if the process was ongoing before (i.e. if the audio was playing), we start the process again. For this, we would use the slider’s input event handler to pause the process. To start the process again, we would use the change event because it is fired after the user is done sliding the thumb. Here is the implementation:

seekSlider.addEventListener('input', () => {   currentTimeContainer.textContent = calculateTime(seekSlider.value);   if(!audio.paused) {     cancelAnimationFrame(raf);   } });  seekSlider.addEventListener('change', () => {   audio.currentTime = seekSlider.value;   if(!audio.paused) {     requestAnimationFrame(whilePlaying);   } });

I was able to come up with something for the second issue. I added the statements in the seek slider’s input event handlers to the whilePlaying() function. Recall that there are two event listeners for the slider’s input event: one for the presentation, and the other for the functionality. After adding the two statements from the handlers, this is how our whilePlaying() function looks:

const whilePlaying = () => {   seekSlider.value = Math.floor(audio.currentTime);   currentTimeContainer.textContent = calculateTime(seekSlider.value);   audioPlayerContainer.style.setProperty('--seek-before-width', `$ {seekSlider.value / seekSlider.max * 100}%`);   raf = requestAnimationFrame(whilePlaying); }

Note that the statement on the fourth line is the seek slider’s appropriate statement from the showRangeProgress() function we created earlier in the presentation section.

Now we’re left with the volume-control functionality. Whew! But before we begin working on that, here’s a Pen covering all we’ve done so far:

Volume-control

For volume-control, we’re utilizing the second slider, #volume-slider. When the user interacts with the slider, the slider’s value is reflected in the volume of the audio and the <output> element we created earlier.

The slider’s max property has a default value of 100. This makes it easy to display its value in the <output> element when it is updated. We could implement this in the input event handler of the slider. However, to implement this in the volume of the audio, we’re going to have to do some math.

The HTMLMediaElement interface provides a volume property, which returns a value between 0 and 1, where 1 being is the loudest value. What this means is if the user sets the slider’s value to 50, we would have to set the volume property to 0.5. Since 0.5 is a hundredth of 50, we could set the volume to a hundredth of the slider’s value.

const volumeSlider = document.getElementById('volume-slider'); const outputContainer = document.getElementById('volume-output');  volumeSlider.addEventListener('input', (e) => {   const value = e.target.value;    outputContainer.textContent = value;   audio.volume = value / 100; });

Not bad, right?

Muting audio

Next up is the speaker icon, which is clicked to mute and unmute the audio. To mute the audio, we would use its muted property, which is also available via HTMLMediaElement as a boolean type. Its default value is false, which is unmuted. To mute the audio, we set the property to true. If you recall, we added a click event listener to the speaker icon for the presentation (the Lottie animation). To mute and unmute the audio, we should add the statements to the respective conditional blocks in that handler, as in the following:

const muteIconContainer = document.getElementById('mute-icon');  muteIconContainer.addEventListener('click', () => {   if(muteState === 'unmute') {     muteAnimation.playSegments([0, 15], true);     audio.muted = true;     muteState = 'mute';   } else {     muteAnimation.playSegments([15, 25], true);     audio.muted = false;     muteState = 'unmute';   } });

Full demo

Here’s the full demo of our custom audio player in all its glory!

But before we call it quits, I’d like to introduce something — something that will give our user access to the media playback outside of the browser tab where our custom audio player lives.

Permit me to introduce to you, drumroll, please…

The Media Session API

Basically, this API lets the user pause, play, and/or perform other media playback actions, but not with our audio player. Depending on the device or the browser, the user initiates these actions through the notification area, media hubs, or any other interface provided by their browser or OS. I have another article just on that for you to get more context on that.

The following Pen contains the implementation of the Media Session API:

If you view this Pen on your mobile, take a sneak peek at the notification area. If you’re on Chrome on your computer, check the media hub. If your smartwatch is paired, I’d suggest you look at it. You could also tell your voice assistant to perform some of the actions on the audio. Ten bucks says it’ll make you smile. 🤓

One more thing…

If you need an audio player on a webpage, there’s a high chance that the page contains other stuff. That’s why I think it’s smart to group the audio player and all the code needed for it into a web component. This way, the webpage possesses a form of separation of concerns. I transferred everything we’ve done into a web component and came up with the following:

Wrapping up, I’d say the possibilities of creating a media player are endless with the HTMLMediaElement interface. There’s so many various properties and methods for various functions. Then there’s the Media Session API for an enhanced experience.

What’s the saying? With great power comes great responsibility, right? Think of all the various controls, elements, and edge cases we had to consider for what ultimately amounts to a modest custom audio player. Just goes to show that audio players are more than hitting play and pause. Properly speccing out functional requirements will definitely help plan your code in advance and save you lots of time.


The post Let’s Create a Custom Audio Player appeared first on CSS-Tricks.

You can support CSS-Tricks by being an MVP Supporter.

CSS-Tricks

, , , ,

How to Play and Pause CSS Animations with CSS Custom Properties

Let’s have a look CSS @keyframes animations, and specifically about how you can pause and otherwise control them. There is a CSS property specifically for it, that can be controlled with JavaScript, but there is plenty of nuance to get into in the details. We’ll also look at my preferred way of setting this up which gives lots of control. Hint: it involves CSS custom properties.

The importance of pausing animations

Recently, while working on the CSS-powered slideshow you’ll see later in this article, I was inspecting the animations in the Layers panel of DevTools. I noticed something interesting I’d never thought about before: animations not currently in the viewport were still running!

Maybe it’s not that unexpected. We know videos do that. Videos just go on until you pause them. But it made me wonder if these playing animations still use the CPU/GPU? Do they consume unnecessary processing power, slowing down other parts of the page?

Inspecting frames in the Performance panel in DevTools didn’t shed any more light on this since I couldn’t see “offscreen”-frames. But, when I scrolled away from my “CSS Only Slideshow” at the first slide, then waited and scrolled back, it was at slide five. The animation hadn’t paused. Animations just run and run, until you pause them.

So I began to look into how, why and when animations should pause. Performance is an obvious reason, given the findings above. Another reason is control. Users not only love to have control, but they should have control. A couple of years ago, my wife had a really bad concussion. Since then, she has avoided webpages with too many animations, as they make her dizzy. As a result, I consider accessibility perhaps the most important reason for allowing animations to pause.

All together, this is important stuff. We’re talking specifically about CSS keyframe animations, but broadly, that means we’re talking about:

  1. Performance
  2. Control
  3. Accessibility

The basics of pausing an animation

The only way to truly pause an animation in CSS is to use the animation-play-state property with a paused value.

.paused {   animation-play-state: paused; }

In JavaScript, the property is “camelCased” as animationPlayState and set like this:

element.style.animationPlayState = 'paused';

We can create a toggle that plays and pauses the animation by reading the current value of animationPlayState:

const running = element.style.animationPlayState === 'running';

…and then setting it to the opposite value:

element.style.animationPlayState = running ? 'paused' : 'running';

Setting the duration

Another way to pause animations is to set animation-duration to 0s. The animation is actually running, but since it has no duration, you won’t see any action.

But if we change the value to 3s instead:

It works, but has a major caveat: the animations are technically still running. The animation is merely toggling between its initial position, and where it is next in the sequence.

Straight up removing the animation

We can remove the animation entirely and add it back via classes, but like animation-duration, this doesn’t actually pause the animation.

.remove-animation {   animation: none !important; }

Since true pausing is really what we’re after here, let’s stick with animation-play-state and look into other ways of using it.

Using data attributes and CSS custom properties

Let’s use a data-attribute as a selector in our CSS. We can call those whatever we want, so I’m going to use a [data-animation]-attribute on all the elements where I’d like to play/pause animations. That way, it can be distinguished from other animations:

<div data-animation></div>

That attribute is the selector, and the animation shorthand is the property where we’re setting everything. We’ll toss in a bunch of CSS custom properties *(*using Emmet-abbreviations) as values:

[data-animation] {   animation:     var(--animn, none)     var(--animdur, 1s)     var(--animtf, linear)     var(--animdel, 0s)     var(--animic, infinite)     var(--animdir, alternate)     var(--animfm, none)     var(--animps, running); }

With that in place, any animation with this data-attribute will be perfectly ready to accept animations, and we can control individual aspects of the animation with custom properties. Some animations are going to have something in common (like duration, easing-type, etc.), so fallback values are set on the custom properties as well.

Why CSS custom properties? First of all, they can be read and set in both CSS and JavaScript. Secondly, they help significantly reduce the amount of CSS we need to write. And, since we can set them within @keyframes (at least in Chrome at the time of writing), they offer new and exiting ways to work with animations!

For the animations themselves, I’m using class selectors and updating the variables from the [data-animation]-selector:

<div class="circle a-slide" data-animation></div>

Why a class and a data-attribute? At this stage, the data-animation attribute might as well be a regular class, but we’re going to use it in more advanced ways later. Note that the .circle class name actually has nothing to do with the animation — it’s just a class for styling the element.

/* Animation classes */ .a-pulse {   --animn: pulse; } .a-slide {   --animdur: 3s;   --animn: slide; }  /* Keyframes */ @keyframes pulse {   0% { transform: scale(1); }   25% { transform: scale(.9); }   50% { transform: scale(1); }   75% { transform: scale(1.1); }   100% { transform: scale(1); } } @keyframes slide {   from { margin-left: 0%; }   to { margin-left: 150px; } }

We only need to update the values that will change, so if we use some common values in the fallback values for the data-animation selector, we only need to update the name of the animation’s custom property, --animn.

Example: Pausing with the checkbox hack

To pause all the animations using the ol’ checkbox hack, let’s create a checkbox before the animations:

<input type="checkbox" data-animation-pause />

And update the --animps property when checked:

[data-animation-pause]:checked ~ [data-animation] {   --animps: paused; }

That’s it! The animations toggle between played and paused when clicking the checkbox — no JavaScript required.

CSS-only slideshow

Let’s put some of these ideas to work!

I‘ve played with the <details>-tag a lot recently. It’s the obvious candidate for accordions, but it can also be used for tooltips, toggle-tips, drop-downs (styled <select>-look-a-likes), mega-menus… you name it. It is the official HTML disclosure element, after all. Apart from the global attributes and global events that all HTML elements accept, <details> has a single open attribute, and a single toggle event. So, like the checkbox hack, it’s perfect for toggling state — but even simpler:

details[open] {   --state: 1; } details:not([open]) {   --state: 0; }

I decided to do a slideshow, where the slides change automatically via a primary animation called autoplay, and each individual slide has its own unique secondary animation. The animation-play-state is controlled by the --animps-property. Each individual slide can have it’s own, unique animation, defined in a --animn-property:

<figure style="--animn:kenburns-top;--index:0;">   <img src="some-slide-image.jpg" />   <figcaption>Caption</figcaption> </figure>

The animation-play-state of the secondary animations are controlled by the --img-animps-property. I found a bunch of nice Ken Burns-esque animations at Animista and switched between them in the --animn-properties of the slides.

Pausing an animation from another animation

In order to prevent GPU overload, it would be ideal for the primary animation to pause any secondary animations. We noted it briefly earlier, but only Chrome (at the time of writing, and it is a bit shaky) can update a CSS Custom Property from an @keyframe animation — which you can see in the following example where the --bgc-property and --counter-properties are modified at different frames:

The initial state of the secondary animation, the --img-animps -property, needs to be paused, even if the primary animation is running:

details[open] ~ .c-mm__inner .c-mm__frame {   --animps: running;   --img-animps: paused; }

Then, in the main animation @keyframes, the property is updated to running:

@keyframes autoplay {   0.1% {     --img-animps: running; /* START */     opacity: 0;     z-index: calc(var(--z) + var(--slides))   }   5% { opacity: 1 }   50% { opacity: 1 }   51% { --img-animps: paused } /* STOP! */   100% {     opacity: 0;     z-index: var(--z)   } }

To make this work in browsers other than Chrome, the initial value needs to be running, as they cannot update a CSS custom property from a @keyframe.

Here’s the slideshow, with a “details hack” play/pause-button — no JavaScript required:

Enabling prefers-reduced-motion

Some people prefer no animations, or at least reduced motion. It might just be a personal preference, but can also be because of a medical condition. We talked about the importance of accessibility with animations at the very top of this post.

Both macOS and Windows have options that allow users to inform browsers that they prefer reduced motion on websites. This enables us to reach for the prefers-reduced-motion feature query, which Eric Bailey has written all about.

@media (prefers-reduced-motion) { ... }

Let’s use the [data-animation]-selector for reduced motion by giving it different values that are applied when prefers-reduced-motion is enabled*:*

  • alternate = run a different animation
  • once = set the animation-iteration-count to 1
  • slow = change the animation-duration-property
  • stop = set animation-play-state to paused

These are just suggestions and they can be anything you want, really.

<div class="circle a-slide" data-animation="alternate"></div> <div class="circle a-slide" data-animation="once"></div> <div class="circle a-slide" data-animation="slow"></div> <div class="circle a-slide" data-animation="stop"></div>

And the updated media query:

@media (prefers-reduced-motion) {   [data-animation="alternate"] {    /* Change animation duration AND name */     --animdur: 4s;     --animn: opacity;   }   [data-animation="slow"] {     /* Change animation duration */     --animdur: 10s;   }   [data-animation="stop"] {     /* Stop the animation */     --animps: paused;   } }

If this is too generic, and you prefer having unique, alternate animations per animation class, group the selectors like this:

.a-slide[data-animation="alternate"] { /* etc. */ }

Here’s a Pen with a checkbox simulating prefers-reduced-motion. Scroll down within the Pen to see the behavior change for each circle:

Pausing with JavaScript

To re-create the “Pause all animations”-checkbox in JavaScript, iterate all the [data-animation]-elements and toggle the same --animps custom property:

<button id="js-toggle" type="button">Toggle Animations</button>
const animations = document.querySelectorAll('[data-animation'); const jstoggle = document.getElementById('js-toggle');  jstoggle.addEventListener('click', () => {   animations.forEach(animation => {     const running = getComputedStyle(animation).getPropertyValue("--animps") || 'running';     animation.style.setProperty('--animps', running === 'running' ? 'paused' : 'running');   }) });

It’s exactly the same concept as the checkbox hack, using the same custom property: --animps, only set by JavaScript instead of CSS. If we want to support older browsers, we can toggle a class, that will update the animation-play-state.

Using IntersectionObserver

To play and pause all [data-animation]-animations automatically — and thus not unnecessarily overloading the GPU — we can use an IntersectionObserver.

First, we need to make sure that no animations are running at all:

[data-animation] {   /* Change 'running' to 'paused' */   animation: var(--animps, paused);  }

Then, we’ll create the observer and trigger it when an element is 25% or 75% in viewport. If the latter is matched, the animation starts playing; otherwise it pauses.

By default, all elements with a [data-animation]-attribute will be observed, but if prefers-reduced-motion is enabled (set to “reduce”), the elements with [data-animation="stop"] will be ignored.

const IO = new IntersectionObserver((entries) => {   entries.forEach((entry) => {     if (entry.isIntersecting) {       const state = (entry.intersectionRatio >= 0.75) ? 'running' : 'paused';       entry.target.style.setProperty('--animps', state);     }   }); }, {   threshold: [0.25, 0.75] });  const mediaQuery = window.matchMedia("(prefers-reduced-motion: reduce)"); const elements = mediaQuery?.matches ? document.querySelectorAll(`[data-animation]:not([data-animation="stop"]`) : document.querySelectorAll('[data-animation]');  elements.forEach(animation => {   IO.observe(animation); });

You have to play around with the threshold-values, and/or whether you need to unobserve some animations after they’ve triggered, etc. If you load new content or animations dynamically, you might need to re-write parts of the observer as well. It’s impossible to cover all scenarios, but using this as a foundation should get you started with auto-playing and pausing CSS animations!

Bonus: Adding <audio> to the slideshow with minimal JavaScript

Here’s an idea to add music to the slideshow we built. First, add an audio-tag:

<audio src="/asset/audio/slideshow.mp3" hidden loop></audio>

Then, in Javascript:

const audio = document.querySelector('your-audio-selector'); const details = document.querySelector('your-details-selector'); details.addEventListener('toggle', () => {   details.open ? audio.play() : audio.pause(); })

Pretty simple, huh?

I did a “Silent Movie” (with audio)-demo here, where you get to know my geeky past. 🙂


The post How to Play and Pause CSS Animations with CSS Custom Properties appeared first on CSS-Tricks.

You can support CSS-Tricks by being an MVP Supporter.

CSS-Tricks

, , , ,
[Top]

3 Approaches to Integrate React with Custom Elements

In my role as a web developer who sits at the intersection of design and code, I am drawn to Web Components because of their portability. It makes sense: custom elements are fully-functional HTML elements that work in all modern browsers, and the shadow DOM encapsulates the right styles with a decent surface area for customization. It’s a really nice fit, especially for larger organizations looking to create consistent user experiences across multiple frameworks, like Angular, Svelte and Vue.

In my experience, however, there is an outlier where many developers believe that custom elements don’t work, specifically those who work with React, which is, arguably, the most popular front-end library out there right now. And it’s true, React does have some definite opportunities for increased compatibility with the web components specifications; however, the idea that React cannot integrate deeply with Web Components is a myth.

In this article, I am going to walk through how to integrate a React application with Web Components to create a (nearly) seamless developer experience. We will look at React best practices its and limitations, then create generic wrappers and custom JSX pragmas in order to more tightly couple our custom elements and today’s most popular framework.

Coloring in the lines

If React is a coloring book — forgive the metaphor, I have two small children who love to color — there are definitely ways to stay within the lines to work with custom elements. To start, we’ll write a very simple custom element that attaches a text input to the shadow DOM and emits an event when the value changes. For the sake of simplicity, we’ll be using LitElement as a base, but you can certainly write your own custom element from scratch if you’d like.

Our super-cool-input element is basically a wrapper with some styles for a plain ol’ <input> element that emits a custom event. It has a reportValue method for letting users know the current value in the most obnoxious way possible. While this element might not be the most useful, the techniques we will illustrate while plugging it into React will be helpful for working with other custom elements.

Approach 1: Use ref

According to React’s documentation for Web Components, “[t]o access the imperative APIs of a Web Component, you will need to use a ref to interact with the DOM node directly.”

This is necessary because React currently doesn’t have a way to listen to native DOM events (preferring, instead, to use it’s own proprietary SyntheticEvent system), nor does it have a way to declaratively access the current DOM element without using a ref.

We will make use of React’s useRef hook to create a reference to the native DOM element we have defined. We will also use React’s useEffect and useState hooks to gain access to the input’s value and render it to our app. We will also use the ref to call our super-cool-input’s reportValue method if the value is ever a variant of the word “rad.”

One thing to take note of in the example above is our React component’s useEffect block.

useEffect(() => {   coolInput.current.addEventListener('custom-input', eventListener);      return () => {     coolInput.current.removeEventListener('custom-input', eventListener);   } });

The useEffect block creates a side effect (adding an event listener not managed by React), so we have to be careful to remove the event listener when the component needs a change so that we don’t have any unintentional memory leaks.

While the above example simply binds an event listener, this is also a technique that can be employed to bind to DOM properties (defined as entries on the DOM object, rather than React props or DOM attributes).

This isn’t too bad. We have our custom element working in React, and we’re able to bind to our custom event, access the value from it, and call our custom element’s methods as well. While this does work, it is verbose and doesn’t really look like React.

Approach 2: Use a wrapper

Our next attempt at using our custom element in our React application is to create a wrapper for the element. Our wrapper is simply a React component that passes down props to our element and creates an API for interfacing with the parts of our element that aren’t typically available in React.

Here, we have moved the complexity into a wrapper component for our custom element. The new CoolInput React component manages creating a ref while adding and removing event listeners for us so that any consuming component can pass props in like any other React component.

function CoolInput(props) {   const ref = useRef();   const { children, onCustomInput, ...rest } = props;      function invokeCallback(event) {     if (onCustomInput) {       onCustomInput(event, ref.current);     }   }      useEffect(() => {     const { current } = ref;     current.addEventListener('custom-input', invokeCallback);     return () => {       current.removeEventListener('custom-input', invokeCallback);     }   });      return <super-cool-input ref={ref} {...rest}>{children}</super-cool-input>; }

On this component, we have created a prop, onCustomInput, that, when present, triggers an event callback from the parent component. Unlike a normal event callback, we chose to add a second argument that passes along the current value of the CoolInput’s internal ref.

Using these same techniques, it is possible to create a generic wrapper for a custom element, such as this reactifyLitElement component from Mathieu Puech. This particular component takes on defining the React component and managing the entire lifecycle.

Approach 3: Use a JSX pragma

One other option is to use a JSX pragma, which is sort of like hijacking React’s JSX parser and adding our own features to the language. In the example below, we import the package jsx-native-events from Skypack. This pragma adds an additional prop type to React elements, and any prop that is prefixed with onEvent adds an event listener to the host.

To invoke a pragma, we need to import it into the file we are using and call it using the /** @jsx <PRAGMA_NAME> */ comment at the top of the file. Your JSX compiler will generally know what to do with this comment (and Babel can be configured to make this global). You might have seen this in libraries like Emotion.

An <input> element with the onEventInput={callback} prop will run the callback function whenever an event with the name 'input' is dispatched. Let’s see how that looks for our super-cool-input.

The code for the pragma is available on GitHub. If you want to bind to native properties instead of React props, you can use react-bind-properties. Let’s take a quick look at that:

import React from 'react'  /**  * Convert a string from camelCase to kebab-case  * @param {string} string - The base string (ostensibly camelCase)  * @return {string} - A kebab-case string  */ const toKebabCase = string => string.replace(/([a-z0-9]|(?=[A-Z]))([A-Z])/g, '$  1-$  2').toLowerCase()  /** @type {Symbol} - Used to save reference to active listeners */ const listeners = Symbol('jsx-native-events/event-listeners')  const eventPattern = /^onEvent/  export default function jsx (type, props, ...children) {   // Make a copy of the props object   const newProps = { ...props }   if (typeof type === 'string') {     newProps.ref = (element) => {       // Merge existing ref prop       if (props && props.ref) {         if (typeof props.ref === 'function') {           props.ref(element)         } else if (typeof props.ref === 'object') {           props.ref.current = element         }       }        if (element) {         if (props) {           const keys = Object.keys(props)           /** Get all keys that have the `onEvent` prefix */           keys             .filter(key => key.match(eventPattern))             .map(key => ({               key,               eventName: toKebabCase(                 key.replace('onEvent', '')               ).replace('-', '')             })           )           .map(({ eventName, key }) => {             /** Add the listeners Map if not present */             if (!element[listeners]) {               element[listeners] = new Map()             }              /** If the listener hasn't be attached, attach it */             if (!element[listeners].has(eventName)) {               element.addEventListener(eventName, props[key])               /** Save a reference to avoid listening to the same value twice */               element[listeners].set(eventName, props[key])             }           })         }       }     }   }      return React.createElement.apply(null, [type, newProps, ...children]) }

Essentially, this code converts any existing props with the onEvent prefix and transforms them to an event name, taking the value passed to that prop (ostensibly a function with the signature (e: Event) => void) and adding it as an event listener on the element instance.

Looking forward

As of the time of this writing, React recently released version 17. The React team had initially planned to release improvements for compatibility with custom elements; unfortunately, those plans seem to have been pushed back to version 18.

Until then it will take a little extra work to use all the features custom elements offer with React. Hopefully, the React team will continue to improve support to bridge the gap between React and the web platform.


The post 3 Approaches to Integrate React with Custom Elements appeared first on CSS-Tricks.

You can support CSS-Tricks by being an MVP Supporter.

CSS-Tricks

, , , ,
[Top]

Custom Properties as State

Here’s a fun idea from James Stanley: a CSS file (that presumably updates daily) containing CSS custom properties for “seasonal” colors (e.g. spring is greens, fall is oranges). You’d then use the values to theme your site, knowing that those colors change slightly from day to day.

This is what I got while writing this:

:root {   --seasonal-bg: hsl(-68.70967741935485,9.419354838709678%,96%);   --seasonal-bgdark: hsl(-68.70967741935485,9.419354838709678%,90%);   --seasonal-fg: hsl(-68.70967741935485,9.419354838709678%,30%);   --seasonal-hl: hsl(-83.70967741935485,30.000000000000004%,50%);   --seasonal-hldark: hsl(-83.70967741935485,30.000000000000004%,35%); }

I think it would be more fun if the CSS file provided was just the custom properties and not the opinionated other styles (like what sets the body background and such). That way you could implement the colors any way you choose without any side effects.

This makes me think that a CDN-hosted CSS file like this could have other useful stuff, like today’s date for usage in pseudo content, or other special time-sensitive stuff. Maybe the phase of the moon? Sports scores?! Soup of the day?!

/* <div class="soup">The soup of the day is: </div> */ .soup::after {   content: var(--soupOfTheDay); /* lol kinda */ }

It’s almost like a data API that is tremendously easy to use. Pseudo content is even accessible content these days — but you can’t select the text of pseudo-elements, so don’t read this as an actual endorsement of using CSS as a content API.

Will Boyd just blogged about what is possible to put in a custom property. They are tremendously flexible. Just about anything is a valid custom property value and then the usage tends to behave just how you think it will.

body {   /* totally fine */   --rgba: rgba(255, 0, 0, 0.1);   background: var(--rgba);    /* totally fine */   --rgba: 255, 0, 0, 0.1;   background: rgba(var(--rgba));    /* totally fine */   --rgb: 255 0 0;   --a: 0.1;   background: rgb(var(--rgb) / var(--a)); }  body::after {   /* totally fine */   --song: "I need quotes to be pseudo content \A and can't have line breaks without this weird hack \A but still fairly permissive (💧💧💧) ";   content: var(--song);   white-space: pre; }

Bram Van Damme latched onto that flexiblity while covering Will’s article:

That’s why you can use CSS Custom Properties to:

perform conditional calculations

pass data from within your CSS to your JavaScript

inject skin tone / hair color modifiers onto Emoji 

toggle multiple values with one custom property (--foo: ; hack)

Bram points out this “basic” state-flipping quality that a custom property can pull off:

:root {   --is-big: 0; }  .is-big {   --is-big: 1; }  .block {   padding: calc(     25px * var(--is-big) +     10px * (1 - var(--is-big))   );   border-width: calc(     3px * var(--is-big) +     1px * (1 - var(--is-big))   ); }

Add a couple of scoops of complexity and you get The Raven (media queries with custom properties).

Will calls them “CSS variables” which is super common and understandable. You’ll read (and I have written) sentences often that are like “CSS variables (a.k.a CSS Custom Properties)” or “CSS Custom Properties (a.k.a CSS Variables.” Šime Vidas recently noted there is a rather correct way to refer to these things: --this-part is the custom property and var(--this-part) is the variable, which comes right from usage in the spec.

And that reminds me of this Vue proposal. I’m not sure if it went anywhere, but the state of a component would automatically be exposed as CSS custom properties.

<template>   <div class="text">Hello</div> </template>  <script> export default {   data() {     return {       color: 'red'     }   } } </script>  <style vars="{ color }"> .text {   color: var(--color); } </style>

By virtue of having color as part of the state of this component, then --color is available as state to the CSS of this component. I think that’s a great idea.

What if every time you used useState in React, CSS custom properties were put on the :root and were updated automatically. For example, if you did this:

import React, { useState } from 'https://cdn.skypack.dev/react@^16.13.1'; import ReactDOM from 'https://cdn.skypack.dev/react-dom@^16.13.1';  const App = () => {   const [ activeColor, setActiveColor ] = useState("red");   return(     <div className="box">       <h1>Active Color: {activeColor}</h1>       <button onClick={() => {setActiveColor("red")}}>red</button>       <button onClick={() => {setActiveColor("blue")}}>blue</button>     </div>   ); }  ReactDOM.render(<App />, document.getElementById("root"))

And you knew you could do like:

.box {   border-color: 2px solid var(--activeColor); }

Because the state automatically mapped itself to a custom property. Someone should make a useStateWithCustomProperties hook or something to do that. #freeidea

Libraries like React and Vue are for building UI. I think it makes a lot of sense that the state that they manage is automatically exposed to CSS.

And speaking of state that CSS should know about, I’ve seen quite a few demos that do fun stuff by mapping over things, like the current mouse position or scroll position, over to CSS. I don’t think it’s entirely unreasonable to ask for that data to be natively exposed to CSS. We already have the concept of environment variables, like env(safe-area-inset-top), and I could see that being used to expose page state, like env(page-scroll-percentage) or env(mouseY).


The post Custom Properties as State appeared first on CSS-Tricks.

You can support CSS-Tricks by being an MVP Supporter.

CSS-Tricks

, ,
[Top]

Custom Styles in GitHub Readme Files

Even though GitHub Readme files (typically ./readme.md) are Markdown, and although Markdown supports HTML, you can’t put <style> or <script> tags init. (Well, you can, they just get stripped.) So you can’t apply custom styles there. Or can you?

  1. You can use SVG as an <img src="./file.svg" alt="" /> (anywhere).
  2. When used that way, even stuff like animations within them play (wow).
  3. SVG has stuff like <text> for textual content, but also <foreignObject> for regular ol’ HTML content.
  4. SVG support <style> tags.
  5. Your readme.md file does support <img> with SVG sources.

Sindre Sorhus combined all that into an example.

That same SVG source will work here:


The post Custom Styles in GitHub Readme Files appeared first on CSS-Tricks.

You can support CSS-Tricks by being an MVP Supporter.

CSS-Tricks

, , , ,
[Top]

Using CSS Custom Properties to Adjust Variable Font Weights in Dark Mode

Black isn’t always slimming.

When recently testing a dark mode option for one of my sites, I experienced first-hand the issue that Robin Rendle addresses in this article. All of my page text — headings and body copy — appeared to bulk up when I switched to dark mode. And it didn’t matter what fonts I used or which browsers I tried. The same thing happened with all of them.

For example, here’s what happens with Adobe’s Source Sans Pro in Chrome for Windows:

See those blurry edges when we switch to dark mode?

It’s not an illusion. The light characters really are heavier against dark backgrounds. We can zoom in to see better:

The characters really are thicker in dark mode!

And it becomes really obvious when we invert the dark mode portions of those images:

We can really see the difference when putting the characters side-by-side on the same white background.
We can really see the difference when putting the characters side-by-side on the same white background.

One solution

Since variable fonts enjoy wide browser support, we can use them to help us address this issue. The three panels below demonstrate a solution we’ll be working toward:

The top shows us some light text on a dark background. The middle panel shows what happens in dark mode without changing any font weight settings. And the bottom panel demonstrates dark mode text that we’ve thinned out a bit. That third panel is adjusted to match the weight of its light counterpart, which is what we’re trying to accomplish here.

Here’s how we can get this improved effect:

  1. Reduce font-weight properties in dark mode via one of the following methods:
    1. Manually changing each font-weight assignment directly in a dark mode media query.
    2. Creating a single --font-weight-multiplier custom property that changes its value in dark mode, and by which we can then multiply by each element’s default font-weight value.
    3. Same thing, but instead of calculating each element’s font-weight property individually, we take advantage of CSS variable scoping and the universal selector (*) to apply our multiplier calculation everywhere at once.
  2. Adjust a variable font’s grade (“GRAD”) axis. Not all variable fonts support this specific feature, but Roboto Flex, does. Altering this axis value changes the font’s apparent weight, without affecting the width of the letters.
  3. Adjust a variable font’s darkmode ("DRKM") axis. Dalton Maag’s aptly-named Darkmode, with its eponymous darkmode axis, is uniquely suited for this. As with the Roboto Flex’s grade axis, adjusting Darkmode’s darkmode axis changes the font’s apparent weight. But while the grade axis requires some fine-tuning of values, the darkmode axis is simply switched on (thinner) or off (regular).

The techniques in the first group work for most variable fonts. The solution Robin uses in his article is actually the very first item in the group. I’ll expand on the second and third items in the group by introducing custom properties that help us automatically adjust font weights in dark mode.

The second and third groups involve less common font-variation-settings axes. Though these strategies apply to fewer typefaces, they may be preferable when available. The trick is knowing what a variable font supports before choosing it.

I’ve made a demonstration page including all the strategies covered in this article. You can see what some different variable fonts look like in light mode, in dark mode with no adjustment, and in dark mode with our solutions for thinning out characters.

In addition to the strategies listed above, there’s always one more option: don’t do anything! If you think your fonts look good enough in light and dark mode, or you don’t have the bandwidth right now to wrestle with reflow, element resizing, browser/display inconsistencies, and extra CSS to maintain, then you may not have to change a thing. Focus on the rest of your site and leave yourself open to the possibility of revisiting this topic later.

Strategy 1: Reducing the font-weight value

Most variable text fonts have a weight axis, which lets us assign any specific font-weight value within the weight range available to that font (e.g. 0-1000, 300-800, etc.). Each technique in this strategy takes advantage of this fine control over the weight axis to reduce font-weight values in dark mode. (The need for such font-weight precision is also what renders most non-variable fonts unsuitable for this solution.)

If you’re using variable fonts you have locally, you can check their axes and value ranges at Wakamai Fondue:

At Wakamai Fondue, you can view any local font’s variable axes and ranges.

Keep in mind that, if you’re using the @font-face rule to load fonts, you should set a font-weight range for each of them at the same time:

@font-face {   src: url('Highgate.woff2') format('woff2-variations');   font-family: 'Highgate';   font-weight: 100 900; }

If you neglect this step, some variable fonts may not properly reflect specific font-weight values in current Chromium browsers.

Dalton Maag Highgate’s font-weight set to 800 in Chrome without (left) and with (right) a font-weight range specified in the @font-face rule.

The basic solution: Manually entering each weight

Here’s the technique most of us may reach for. We create a dark mode media query in which we enter some font-weight values that are a bit lower than their defaults.

/* Default (light mode) CSS */  body {   font-weight: 400; }  strong, b, th, h1, h2, h3, h4, h5, h6 {   font-weight: 700; }  /* Dark mode CSS */ @media (prefers-color-scheme: dark) {   body {     font-weight: 350;   }    strong, b, th, h1, h2, h3, h4, h5, h6 {     font-weight: 600;   } }

It works, and it’s no problem to maintain — so long as we’re not planning on adding or editing any other weights at our site! But if we do start incorporating more weights, it can get unwieldy fast. Remember to enter each selector/property combo both outside and inside the prefers-color-scheme media query. We’ll have to do some manual calculations (or guesswork) to determine the dark mode property values for each element.

Creating a weight multiplier custom property and using it in a calculation when setting an element’s weight

I generally try to adhere to Mike Riethmuller’s credo that “media queries are only used to change the value of custom properties.” And that’s the improvement we make in this solution. Instead of having to enter font weights for all our elements in and out of dark mode, the only thing we’re putting in our media query is a --font-weight-multiplier custom property:

@media (prefers-color-scheme: dark) {   :root {     --font-weight-multiplier: .85;   } }

Then, for all our font-weight properties throughout the stylesheet, we’ll multiply the variable’s value by our preferred default weight value for each element — thus lowering the font weight by 15% in dark mode. If we’re not in dark mode, we’ll multiply the default weight by 1, meaning it doesn’t change at all.

Here’s what I mean. Normally, we’d use this to set a body font weight of 400:

body {   font-weight: 400; }

For this solution, we use this:

body {   font-weight: calc(400 * var(--font-weight-multiplier, 1)); }

In the var() function, notice that our variable has a fallback value of 1. Because --font-weight-multiplier is only set in dark mode, this fallback value will be used the rest of the time. So, by default, our font weight for body text stays at 400 (400*1). But in dark mode, the weight decreases to 340 (400*.85).

We’ll also do this with bold elements:

strong, b, th, h1, h2, h3, h4, h5, h6 {   font-weight: calc(700 * var(--font-weight-multiplier, 1)); }

These weights will decrease from 700 to 595 (700*.85) in dark mode.

And we can use the same technique for any other elements where we want to set the font-weight to something other than 400 by default.

I’m using a value of .85 for --font-weight-multiplier, because I’ve found that to be a good general value for most fonts (like Adobe Source Sans Pro, the free typeface I use in most of this article’s demos). But feel free to play around with that number.

Here’s how this looks put together:

/* DARK-MODE-SPECIFIC CUSTOM PROPERTIES */ @media (prefers-color-scheme: dark) {   :root {     --font-weight-multiplier: .85;   } }  /* DEFAULT CSS STYLES... */ body {   font-weight: calc(400 * var(--font-weight-multiplier, 1)); }  strong, b, th, h1, h2, h3, h4, h5, h6 {   font-weight: calc(700 * var(--font-weight-multiplier, 1)); }

Creating a weight multiplier variable and automatically calculating and applying it to all elements at once.

When using many CSS custom properties, I think many of us stick to a “set as needed and manually apply everywhere” approach. That’s what the previous solution does. We set our custom property value in the :root (and/or use a fallback value), set it again in a media query, then apply it with calc() and var() functions throughout our stylesheet each time we assign a font-weight value.

The code might look something like this:

h1 {   font-weight: calc(800 * var(--font-weight-multiplier, 1); }  summary {   font-weight: calc(600 * var(--font-weight-multiplier, 1); }

But when we use this technique for various elements, you can see we have to do these three things every time we assign font-weight values:

  • Include the calc() function
  • Include the var() function
  • Remember the --font-weight-multiplier custom property’s name and default value

Instead, I’ve recently started inverting this approach for certain tasks, taking advantage of CSS variable scope with a “set everywhere and apply once” method. For this technique, I replace every font-weight property in the stylesheet with a --font-weight variable, keeping the name the same except for the dashes, for simplicity’s sake. I then set this value to the default weight for that particular selector (e.g. 400 for body text). Neither calc() nor var() is needed — yet. This is how we set everywhere.

Then we apply once, with a lone font-weight property in our stylesheet that sets every text element’s weight via the universal selector. Modifying our snippet above, we’d now have this:

h1 {   --font-weight: 800; }  summary {   --font-weight: 600; }  * {   font-weight: calc(var(--font-weight, 400) * var(--font-weight-multiplier, 1); }

The calc() function multiplies each of our --font-weight custom properties by our multiplier variable, and the font-weight property then applies the value to its appropriate element.

It’s unnecessary to use only a single var() for each custom property in the stylesheet. But I often like doing so when performing calculations and/or using a helper variable, as we do here. That said, while this is certainly the cleverest technique for adjusting font weights, that doesn’t mean it’s the best technique for all projects. There is at least one serious caveat.

The primary advantage of using the universal selector technique — that it applies to everything — also introduces its chief risk. There may be some elements that we don’t want thinned out! For example, if our form elements retain dark text on light backgrounds in dark mode, they may still get steamrolled by the universal selector.

There are ways to mitigate this risk. We could replace * with a long selector string containing a list of only elements to thin out (having them opt-in to the calculation). Or we could hard-code font weights for the elements that we don’t want affected (opt-out):

* {   font-weight: calc(var(--font-weight, 400) * var(--font-weight-multiplier, 1)); }  button, input, select, textarea {   font-weight: 400; }

Such fixes may ultimately make code just as complicated as the previous technique. So, you’ll have to gauge which is appropriate for your project. If you still have concerns over performance, code complexity, or think this technique might introduce undesired (even unpredictable) results, the previous technique might be safest.

The final code:

/* DEFAULT CUSTOM PROPERTIES */ :root {   --font-weight: 400;   --font-weight-multiplier: 1; } strong, b, th, h1, h2, h3, h4, h5, h6 {   --font-weight: 700; }  /* DARK-MODE-SPECIFIC CUSTOM PROPERTIES */ @media (prefers-color-scheme: dark) {   :root {     --font-weight-multiplier: .85;   } }  /* APPLYING THE CUSTOM PROPERTIES... */ * {   font-weight: calc(var(--font-weight, 400) * var(--font-weight-multiplier, 1)); }

We’re not required to set the default --font-weight: 400 and --font-weight-multiplier: 1 custom properties in the above code, because we’ve included the fallback values in the var() functions. But as code gets more complicated, I often like assigning them in a logical place, just in case I want to find and alter them later.

A final note on this strategy: we can also apply weights with the font-variation-settings property and a "wght" axis value, instead of font-weight. If you’re using a typeface with several axes, maybe you find it more manageable to do all your font tweaking that way. I know of at least one font (Type Network’s Roboto Flex, which we’ll be using later in this article) that has 13 axes!

Here’s how to apply our solution via a font-variation-settings property:

* {   --wght: calc(var(--font-weight, 400) * var(--font-weight-multiplier, 1));   font-variation-settings: "wght" var(--wght); }

Strategy 1 Addendum: Handling letter-spacing

One side effect of lowering our type weight is that, for most non-monspaced fonts, it also narrows the characters.

Here again is what happens when we lighten Source Sans Pro with our multiplier. The top two panels below show Source Sans Pro in light and dark mode by default. And the lower panel shows the lighter version.

Adobe’s Source Sans Pro in light mode, dark mode by default, and dark mode thinned out.

With no adjustments, the characters in light mode and dark mode are the same width. But when we lower the font weight, those characters now take up less space. You may not like how this change affects your flow or element sizes (e.g. narrower buttons). And some designers think it’s a good idea to add letter spacing in dark mode, anyway. So, if you want, you can create another custom property to add some space.

Implementing a custom property for letter spacing

Just like we did with our font-weight multiplier variable, we’re going to create a letter spacing variable with a default value that gets overridden in dark mode. In our default (light mode) :root, we set our new --letter-spacing custom property to 0 for now:

:root {   /* ...other custom variables... */   --letter-spacing: 0; }

Then, in our dark mode query, we raise the value to something greater than 0. I’ve entered it as .02ch here (which combines pretty well with a --font-weight-multiplier value of .85). You could even get clever and fine-tune it with some calculations based on your font weights and/or sizes, if you like. But I’ll use this hard-coded value for now:

@media (prefers-color-scheme: dark) {   :root {     /* ...other custom variables... */     --letter-spacing: .02ch;   } }

Finally, we apply it via our universal selector (with a fallback value of 0):

* {   /* ...other property settings... */   letter-spacing: var(--letter-spacing, 0); }

Note: Though I use the ch unit in this example, using em also works, if you prefer. For Source Sans Pro, a value of .009em is about equal to .02ch.

Here’s the full code for a font weight multiplier with letter spacing:

/* DEFAULT CSS CUSTOM PROPERTIES */ :root {   --font-weight: 400;   --font-weight-multiplier: 1;   --letter-spacing: 0; }  strong, b, th, h1, h2, h3, h4, h5, h6 {   --font-weight: 700; }  /* DARK MODE CSS CUSTOM PROPERTIES */ @media (prefers-color-scheme: dark) {   :root {     /* Variables to set the dark mode bg and text colors for our demo. */     --background: #222;     --color: #fff;      /* Variables that affect font appearance in dark mode. */     --font-weight-multiplier: .85;     --letter-spacing: .02ch;   } }  /* APPLYING CSS STYLES... */ * {   font-weight: calc(var(--font-weight, 400) * var(--font-weight-multiplier, 1));   letter-spacing: var(--letter-spacing, 0); }  body {   background: var(--background, #fff);   color: var(--color, #222); } 

Fonts with constant-width characters (aka multi-plexed fonts)

In addition to monospaced fonts, there are some other typefaces specifically designed so that their individual characters take up the same amount of horizontal space, regardless of weight. For example, if an “i” occupies five horizontal pixels of space at a weight of 400, and a “w” occupies thirteen pixels at the same weight, they will still occupy five and thirteen pixels, respectively, when their weights are increased to 700.

Arrow Type’s Recursive Sans is one such typeface. The following image shows how Recursive’s characters maintain the same widths in light mode, default dark mode, and dark mode with our font weight multiplier, respectively:

The characters in Arrow Type’s Recursive maintain their widths regardless of font weight.

Multi-plexed typefaces, like Recursive, are designed so you won’t need to adjust letter spacing when changing their font weights in dark mode. Your element sizes and page flow will remain intact.

Strategy 2: Adjust a variable font’s grade axis

The grade axis ("GRAD") changes a font’s apparent weight without changing its actual font-weight value or the widths of its characters. When using fonts with this axis, you may not need our font weight multiplier variable at all.

For Type Network’s free Roboto Flex font, a grade of -1 is thinnest, 0 (default) is normal, and 1 is thickest. With this font, I start by assigning its grade axis a value of around -.75 for dark mode.

Roboto Flex in light mode, dark mode default, and dark mode with “GRAD” set to -.75
:root {   --GRAD: 0; }  @media (prefers-color-scheme: dark) {   :root {     --GRAD: -.75;   } }  body {   font-variation-settings: "GRAD" var(--GRAD, 0); }

So, adjusting the grade axis seems like the perfect solution if it’s available to you, right? Well, maybe. There are a few things to keep in mind when considering it.

First, the scale for all fonts doesn’t always go from -1 to 1. Some range from 0 to 1. At least one typeface uses percents, making 100 the default. And other fonts align the grade scale with font weights, so the range may be something like 100-900. If you want to use the grade axis in the latter case, you may have to set all your font weights everywhere to a default of 400, and then use the grade axis for all weight changes. For dark mode, you’ll then want to treat grade essentially like we do in our font weight multiplier solution — applying the multiplier to the "GRAD" axis in font-variation settings.

The second caveat is that some typefaces don’t let you grade a font to a value below its default weight. So, grade can’t lighten it at all. Apple’s San Francisco typeface (which can be tested via font-family: system-ui; on Apple devices) has both of these issues. As of macOS Catalina, San Francisco has a grade axis. It’s scaled to line up with font weights, and its minimum value is 400.

San Francisco’s grade and weight axes use the same scale, but have different ranges.

Because we can’t set the grade to a value lower than 400, we can’t lighten fonts from a default of 400 in dark mode. If we want to go lower, we’ll need to lower the weight axis value, instead.

Strategy 3: Adjusting a variable font’s darkmode axis

There’s currently only one typeface with a darkmode ("DRKM") axis at the time of this writing: Dalton Maag’s Darkmode.

The darkmode axis is essentially a grade axis without any fine-tuning. Just turn it on (1) for a thinner appearance in dark mode, and leave it off (0, the default) for normal display.

Darkmode in light mode, in dark mode with “DRKM” unset, and in dark mode with “DRKM” set to 1.
:root {   --DRKM: 0; }  @media (prefers-color-scheme: dark) {   :root {     --DRKM: 1;   } }  body {   font-variation-settings: "DRKM" var(--DRKM, 0); }

I like the Darkmode font a lot. But beware that it is a commercial license that’s required for professional use. Dalton Maag offers a trial version that can be used for “academic, speculative, or pitching purposes only.” I’m hoping this typeface is a pilot for more Dalton Maag families to get a darkmode axis, and that other font foundries will then follow suit!

Other factors to consider

We’ve covered a few big strategies for working with variable fonts in a dark mode context. But, as with most things, there are other things to consider that might sway you toward one solution or another.

Dark mode on high-resolution (“retina”) screens

On screens with higher pixel densities (e.g. most modern phones, MacBooks, iMacs, etc.), the thickening effect of dark mode is often less pronounced. Therefore, you may not want to thin the fonts on these screens out as much — if at all!

If you still want to lighten fonts a bit, you can add another media query to make the effect less severe. Depending which solution you’re using, you can raise the --font-weight-multiplier value closer to 1, raise the --GRAD value closer to 0, or disable --DRKM altogether (since it’s either on or off, with no in-between).

If you add this query, remember to place it below the original prefers-color-scheme query, or it may have no effect. Media queries don’t add CSS specificity, so their order matters!

@media (prefers-color-scheme: dark) and (-webkit-min-device-pixel-ratio: 2),         (prefers-color-scheme: dark) and (min-resolution: 192dpi) {    :root {     --font-weight-multiplier: .92;     /* Or, if you're using grade or darkmode axis instead: */     /* --GRAD: -.3; */     /* --DRKM: 0; */   } }

If you don’t want to lighten fonts at all on high density screens in dark mode, you can update your original dark mode prefers-color-scheme query to the following, to omit these screens:

@media (prefers-color-scheme: dark) and (-webkit-max-device-pixel-ratio: 1.9),         (prefers-color-scheme: dark) and (max-resolution: 191dpi) {     /* Custom properties for dark mode go here. */  }

Mixing fonts with different axes (and mixing variable fonts with non-variable fonts)

If you’re using more than one typeface on your site, you’ll need to consider what effects these adjustments may have on all of them. For example, if you’re using multiple fonts with intersecting axes, you could wind up accidentally combining the effects of multiple strategies (e.g. reducing both grade and weight simultaneously):

If your stylesheet includes solutions for several typefaces/axes, then the effect on fonts that have multiple axes (like this example’s Roboto Flex, which has both grade and weight axes) may be cumulative.

If all the fonts on your site are variable and have a grade axis with a matching scale and range (e.g. if they all range from -1 to 1), that’s the solution I’d recommend. However, you’ll have to revisit this if you plan to add other fonts later that don’t meet those criteria. Same goes for the darkmode axis, too, if it becomes more widespread.

If all your fonts are variable, but they don’t all share the same axes (e.g. grade and darkmode), then using only the --font-weight-multiplier custom property may be your safest bet.

Finally, if you’re mixing variable and non-variable fonts, know that the non-variable fonts will not change appearance with any of these solutions — with some exceptions. For example, if you’re using the font weight multiplier with the font-weight property, it is possible that some — but maybe not all — of your font weights will change enough to move to the next lower weight name.

Say your site includes a font with three weights: regular (400), semi-bold (600), and bold (700). In dark mode, your bold text may lighten up enough to display as semi-bold. But your regular font will still stay regular (as that’s the lowest weight included on the site). If you want to avoid that inconsistency, you could apply your variable font weights via font-variation-settings, and not font-weight, so your non-variable fonts aren’t affected at all. They’ll just always maintain their default weight in dark mode.

In closing

It’s always a happy coincidence when complementary technologies attain common usage near the same time. With the rise in popularity of both dark mode and variable fonts, we have the luxury of using the latter to mitigate one of the challenges of the former. Using CSS custom properties in conjunction with weight, grade, and darkmode axes, we can bring some consistency to the look of our text in both light and dark modes.

You can visit my interactive demo with the fonts and axes from this article.


The post Using CSS Custom Properties to Adjust Variable Font Weights in Dark Mode appeared first on CSS-Tricks.

You can support CSS-Tricks by being an MVP Supporter.

CSS-Tricks

, , , , , , , ,
[Top]

Color Theming with CSS Custom Properties and Tailwind

Custom properties not only enable us to make our code more efficient, but allow us to work some real magic with CSS too. One area where they have huge potential is theming. At Atomic Smash we use Tailwind CSS, a utility class framework, for writing our styles. In this article, we’ll look at how custom properties can be used for theming, and how we can integrate them with Tailwind to maximize the reusability of our code. We won’t cover getting up and running with Tailwind — check out the official documentation for that — but even if you’re new to it you might find some of these tips useful.

Theming overview

Let’s say we have a “Call To Action” (CTA) component with a heading, body copy, and button.

A box with a light red heading that reads join our mailing list above a dark red body that reads be the first to hear about our new offerings right before a red signup button.

Writing regular (non-Tailwind) CSS for this color scheme would look something like this:

.cta {   background-color: #742a2a; // dark red   color: #ffffff; //white }      .cta__heading {   background-color: #e53e3e; // medium red   color: #742a2a; } 
 .cta__button {   background-color: #e53e3e; }

Using Tailwind, we would apply these colors as utility classes in our HTML:

<div class="bg-red-900 text-white">   <h3 class="bg-red-600 text-red-900">Join our mailing list</h3>   <div>     <p>Be the first to hear about our new offerings</p>     <button class="bg-red-600" type="button">Sign up</button>   </div> </div>

I’ve deliberately left out classes relating to anything other than the basic color scheme, but you can see the example in its entirety in this demo:

Now, if we wanted to apply a different color scheme to our component, we would need to override the color values of our original component. Without Tailwind, a common way to do that would be to append a theme class to the component itself, and redefine the color values lower down in the cascade. So for a component with a modifier class of .cta--blue (using the BEM convention) we’ll apply the CSS values for a blue color scheme:

.cta--blue {   background-color: #2a4365; // dark blue } 
 .cta--blue .cta__heading {   background-color: #3182ce; // medium blue   color: #2a4365; } 
 .cta--blue .cta__button {   background-color: #3182ce; }
A box with a light blue heading that reads join our mailing list above a dark bluebody that reads be the first to hear about our new offerings right before a blue signup button.

If we’re using Sass or another preprocessor, it’s likely we’ll make life easier for ourselves by using variables for those color names, and we might nest the .cta__heading and .cta__body selectors. It doesn’t exactly make our code more concise, but it does make it more manageable by having a single place to update those values.

Now, suppose we have 10 different color schemes, as was my experience on a recent project. Our code starts to get longer, as we’re basically duplicating the above example 10 times in order to change those color values. Now imagine every component in our design system needs 10 color schemes, and many of those components are far more complex than our simple CTA. Maybe our themes need different fonts too. Suddenly we have a lot of CSS to write.

Theming with Tailwind

If we’re using Tailwind, on the other hand, we’d need to change multiple classes in the HTML itself. Even if we’re using a JavaScript framework, like React or Vue, this is not exactly a trivial task. In order to ensure unused styles are removed in a production build, Tailwind discourages the use of string concatenation for class names (at the time of writing). So building our themes means potentially piling a lot of logic into our components.

Theming with Custom Properties

By using custom properties for our color themes, we can drastically reduce the amount of code we need to write, and alleviate the maintenance burden. Let’s first take a look at how we can do this in regular CSS.

We define our custom properties as variables on the :root selector, making them global variables. (The body selector would serve us just as well.) Then we can use those variables in a selector, in place of our color property values:

:root {   --primary: #742a2a; // dark red;   --secondary: #e53e3e; // medium red } 
 .cta {   background-color: var(--primary);   color: white; } 
 .cta__heading {   background-color: var(--secondary);   color: var(--primary); } 
 .cta__button {   background-color: var(--secondary); }

This is where the real magic happens: now the code for creating each of our themes becomes a case of only updating those custom property values. The new values will be inherited wherever we apply our theme class:

.th-blue {   --primary: #2a4365; // dark blue   --secondary: #3182ce; // medium blue }

If we want a blue color scheme, we can apply that .th-blue class to the component, or even use it on the <body> tag to apply to apply a page-wide theme, which can be overridden on individual components as desired. Using a utility class potentially saves us writing even more code compared to a component-specific class (such as .cta--blue in the original code), as it could be applied anywhere in our codebase.

Handling older browsers

Like many agencies, plenty of our clients at Atomic Smash still require us to support Internet Explorer 11. While I’m okay with a progressive enhancement approach in most cases (by providing simpler fallback layouts for browsers that don’t support CSS Grid, for instance), I find theming is one area that often doesn’t allow for easy compromise. Clients want their brand colors and fonts seen, even on older browsers. Providing fallbacks using feature queries would entail a lot of extra work that would negate the benefits of using custom properties in the first place. To overcome this, we need a polyfill.

There are a couple of options for polyfilling custom properties in IE 11.

postcss-custom-properties

The first is using a PostCSS plugin called postcss-custom-properties. If you’re already using PostCSS in your workflow, this is fairly simple to add. It works by processing your CSS and outputting the result of the variable as the property value. So if you have the following CSS:

:root {   --color: red; } 
 h1 {   color: var(--color); }

The processed result will be:

h1 {   color: red;   color: var(--color); }

Browsers that don’t support custom properties will ignore the second rule and fall back to the regular property value. There is also an option to remove the rules with the custom properties in the output, so the file size will be smaller. This means that no browsers will get the custom property — which is an issue if you’re updating variables dynamically — but you’ll be able to use them for static values in your code with no ill effects.

Unfortunately this polyfill has some limitations:

  1. You need to specify the file (or files) in your config where you’re defining the custom properties.
  2. Custom properties can only be defined on the :root selector.

The first limitation is relatively trivial, but the second unfortunately renders this polyfill entirely useless for our theming use case. It means we can’t redefine variables on a selector to create our themes.

ie11CustomProperties

This polyfill option involves serving a client-side script, rather than preprocessing the CSS. We can add the following script to our head to ensure the polyfill will only be loaded in IE 11:

<script>window.MSInputMethodContext && document.documentMode && document.write('<script src="https://cdn.jsdelivr.net/gh/nuxodin/ie11CustomProperties@4.1.0/ie11CustomProperties.min.js"></script>');</script>

This permits us to enjoy the full benefits of custom properties as in the examples here, so it’s the solution I decided to go with. It has a limitation where custom properties set in style attributes aren’t polyfilled. But I’ve tested it for the theming example above and it works just fine.

But what does this have to do with Tailwind? 

As we’ve already seen, utility classes — single-purpose classes that can be applied anywhere in our HTML — can make our code more reusable. That’s the main selling point of Tailwind and other utility class frameworks — the size of the CSS file you ship should end up smaller as a result. Tailwind makes multiple color classes available: .bg-red-medium would give us a red background-color property value, .text-red-medium for color and so on for border, box-shadow, or any place you can think of that you might need a color value. 

Colors can be defined in a config file:

module.exports = {   theme: {     colors: {       red: {         medium: '#e53e3e',         dark: '#742a2a'       },       blue: {         medium: '#3182ce',         dark: '#2a4365'       }     }   } }

If we want to use custom property values for our Tailwind classes, we can specify them in the config:

module.exports = {   theme: {     colors: {       'th-primary': 'var(--primary)',       'th-secondary': 'var(--secondary)'     }   } }

I’m prefixing my colors and theme-related class names with th- so that it’s obvious they’re specifically related to theming, but feel free to use whatever convention suits you.

Now those classes will be available to us through Tailwind. Using .bg-th-primary gives us the equivalent of writing:

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

In our CSS we can define our custom properties for our themes as before:

:root {   --primary: #742a2a;   --secondary: #742a2a; } 
 .th-blue {   --primary: #2a4365;   --secondary: #3182ce; }

Let’s apply those classes to our HTML. The first example gives us a component with our default theme (the variables defined on the :root). The second has our blue theme. The only difference is the addition of the .th-blue class on the component. (Once again, I’ve omitted the classes unrelated to the theme, for brevity and clarity.)

<!--Component with default (red) theme--> <div class="bg-th-primary">   <h3 class="bg-th-secondary text-th-primary">Join our mailing list</h3>   <div>     <p>Be the first to hear about our new offerings</p>     <button class="bg-th-secondary" type="button">Sign up</button>   </div> </div> 
 <!--Component with blue theme--> <div class="th-blue bg-th-primary">   <h3 class="bg-th-secondary text-th-primary">Join our mailing list</h3>   <div>     <p>Be the first to hear about our new offerings</p>     <button class="bg-th-secondary" type="button">Sign up</button>   </div> </div>

Using the config as a style guide

Tailwind encourages you to define all variables in the config, and personally I agree that it’s a better approach. It means that the config file can be a single source of truth rather than (potentially) ending up with multiple places to define your colors and other theme values. Luckily, we can also use values from the Tailwind config file for our custom properties. We’ll need to first define all of our colors in the config (assuming we’re not using the default color palette included with Tailwind):

module.exports = {   theme: {     colors: {       red: {         medium: '#e53e3e',         dark: '#742a2a'       },       blue: {         medium: '#3182ce',         dark: '#2a4365'       },       'th-primary': 'var(--primary)',       'th-secondary': 'var(--secondary)'     }   } }

Then we can access the theme object in the CSS:

:root {   --th-primary: theme('colors.red.dark');   --th-secondary: theme('colors.red.medium'); } 
 .th-blue {   --th-primary: theme('colors.blue.dark');   --th-secondary: theme('colors.blue.medium'); }

Wrapping up

I’m really excited about the benefits of being able to use custom properties without having to worry about browser support, even more so to be able to integrate them smoothly with our existing workflow. It’s hard to overstate the amount of time they will save us for theming. I hope that even if you’re not a Tailwind user, this article might encourage you to give custom properties a go for this use case.


The post Color Theming with CSS Custom Properties and Tailwind appeared first on CSS-Tricks.

You can support CSS-Tricks by being an MVP Supporter.

CSS-Tricks

, , , ,
[Top]

The CSS Custom Property Toggle Trick

Back in July 2020, I got an email from James0x57 (I always try to refer to people by their name, but I think I get the sense they prefer to go by screen name) that says:

The entire world of branching conditional logic and bulk feature toggling for custom CSS properties is possible and only exists because of a tiny footnote in the CSS spec that has gone unnoticed.

That line is:

Note: While <declaration-value> must represent at least one token, that one token may be whitespace.

In other words, --foo: ; is valid.

If you’re like me, this doesn’t read as some massive revelation that unlocks huge doors, but to smarter people, like James0x57, it does! We started working on a draft blog post, but for various reasons it didn’t make it all the way here. One of those reasons is that I just wasn’t getting it. Call me dense, sorry James0x57. One demo they sent me when I asked super dumbed-down example was helpful though, and I think it’s kind of clicked for me. Here’s my interpretation:

Let me attempt to explain:

  • The breakpoint we’ve set up here is a 900px max-width media query. You can see that’s where the variable --mq-sm flops from initial to an empty space value.
  • When the browser window is wider than 900px, that the value of --mq-sm is initial.
    • That makes the variable --padding-when-small contain two values — initial and 2rem —which, I guess is invalid.
    • So when we actually set the padding and call that variable like padding: var(--padding-when-small, var(--padding-when-large)), the second value (the “fallback”) is used because the first value is invalid.
  • When the browser window is narrower than 900px, the --mq-sm value is a space.
    • That makes the variable --padding-when-small value "(space)2rem" which, I guess is valid.
    • That means when we actually set the padding and call that variable like padding: var(--padding-when-small, var(--padding-when-large)), the first value is used.

So, now we can flip the padding between two values by changing a placeholder variable.

That clicks for me.

When see this as simply changing a single value, it’s almost like uh, ok, you’ve found a really complex way to change some padding, but you could have just changed the padding in the media query. But the trick is that now we have this placeholder variable that has changed and we can key into that to change unlimited other values.

We could have a single media query (or set of media queries) in our CSS that only toggles these placeholder variables and we use elsewhere to toggle values. That could be nice and clean compared to sprinkling media queries all over the CSS. It’s a proper toggle in CSS, like a form of IF/THEN logic that we haven’t quite had before.

James0x57 extended that thinking to all the logical possibilities, like AND, OR, XOR, NAND, NOR, and XNOR, but that lost me again. Not really a computer scientist over here. But you can follow their work if you want to see real world usage of this stuff.

This variable stuff is wild and gets very confusing. I noted in a possibly recent (but the byline says 2015?) article from Patrick Brosset that covers some tricky CSS custom properties stuff. For example, fallbacks can be infinitely nested, like:

color: var(--foo, var(--bar, var(--baz, var(--are, var(--you, var(--crazy)))));

Also, valid values for CSS custom properties can have commas in them like this:

content: var(--foo, one, two, three);

Is that really just one fallback with a single one, two, three value? This is rather mind-bending.

Anyway, fast-forward a bunch of months now, and CSS trickery master Lea Verou has set her sights on this whitespace-in-custom-properties stuff:

What if I told you you could use a single property value to turn multiple different values on and off across multiple different properties and even across multiple CSS rules?

It’s the same trick! In Lea’s example, though, she uses this ability to:

  • set variations on a button, and
  • set four different properties rather than one.

This really hones in on why this is the concept is so cool.

Lea points to some downsides:

There is no way to say “the background should be red if --foo is set and white otherwise”. Some such conditionals can be emulated with clever use of appending, but not most.

And of course there’s a certain readability issue: --foo: ; looks like a mistake and --foo: initial looks pretty weird, unless you’re aware of this technique.

We’re certainly entering the next era of how custom properties are used. First, we used them like preprocessor variables. Then we started seeing more cascade and fallback usage. Next, we used it alongside JavaScript more frequently. Now this.

There is even more writing about keeping CSS preprocessor variables around, not so much for the times when you only need what they can do, but for the things that only they can do, like having their color values manipulated.


The post The CSS Custom Property Toggle Trick appeared first on CSS-Tricks.

You can support CSS-Tricks by being an MVP Supporter.

CSS-Tricks

, , ,
[Top]

Using @property for CSS Custom Properties

Una Kravetz digs into how Chrome now allows you to declare CSS custom properties directly from CSS with more information than just a string.

So rather than something like this:

html {   --stop: 50%; }

…can be declared with more details like this:

@property --stop {   syntax: "<percentage>";   initial-value: 50%;   inherits: false; }

The browser then knows this specific custom property is a percentage rather than a string. It can be other useful stuff like <integer> and <color>. Now that we have a way to communicate this sort of information to the browser, we get some new abilities, like being able to transition between two values.

While playing around, I noticed you have to very specifically call out the property to be transitioned (because a catch-all transition won’t do it). Try hovering on this demo, which is a re-creation of what Una did in the post:

Note that I’m animating the color stop’s position (which is a percentage), but I’m also trying to animate the color, which still does not work. I assumed it would with this new feature. I know people have been confused about the lack of being able to animate gradients for a long time. (See Ana Tudor’s article.)

You can always re-declare the properties somewhere at a high-level to “support” browsers that can’t read custom properties. Feels like a funny time to be talking about that. Safari seems to signal strong interest in this Houdini-based stuff, but hasn’t yet. Firefox? Eeesh, I dunno. Best we know is they labeled it as “Worth Prototyping” before all the layoffs.

This will also help with a the weird fallback issue with CSS custom properties that we mentioned in the newsletter:

As with any other custom property, you can get (using var) or set (write/rewrite) values, but with Houdini custom properties, if you set a falsey value when overriding it, the CSS rendering engine will send the initial value (its fallback value) instead of ignoring the line.


The post Using @property for CSS Custom Properties appeared first on CSS-Tricks.

You can support CSS-Tricks by being an MVP Supporter.

CSS-Tricks

, , ,
[Top]

Timer Bars in CSS with Custom Properties

I was working on a thing the other day that needed a visible timer. There was UI precedent for this type of timer on the project. People didn’t want to see numbers ticking downward; it was more ideal to see a “bar” drain away from full to empty. I mention that because there are tons and tons of ways you could approach a “timer” UI. This isn’t an exploration of all of those (a search on CodePen would be more helpful there), but an exploration of the one way that was useful to me.

The kind of timer I needed was what the project called a “round time” bar. An action is performed. It may cause a round time, and most further actions are blocked until the round time is over. So, a very clear red bar that ticks away was the right UI. It gives a sense of rhythm and flow where you can kinda feel the end of the timer and time your next action.

a linear animation that shrinks the bar to zero.

Setting this up is fairly easy…

Let’s give ourselves a parent/child thing, just in case we want to style the empty part of the container at some point.

<div class="round-time-bar">   <div></div> </div>

For now, let’s just style the bar inside.

.round-time-bar div {   height: 5px;   background: linear-gradient(to bottom, red, #900); }

That gives us a nice little red bar we can use for the time indicator.

Next we need to make it tick down, but here’s where we need to think about functionality. A timer like this needs to know how long it’s timing! We can give it that information right in the HTML. This doesn’t mean we’re avoiding JavaScript — we’re embracing it. We’re saying, “hey JavaScript, please give us the duration as a variable and we’ll take it from there.”

<div class="round-time-bar" style="--duration: 5;">   <div></div> </div>

In fact, this way is very friendly to modern DOM-handling JavaScript. As long as that --variable is correct, it is free to re-render that DOM element at any time and we can make sure the design handles that just fine. We’ll make a variation that does that.

For now, let’s make the animation happen. Good news, it’s easy. Here’s a one-liner keyframe:

@keyframes roundtime {   to {     /* More performant than animating `width` */     transform: scaleX(0);   } }

We can “squish” the bar because the design of the bar doesn’t have anything that will look squished when we scale it horizontally. If we did, we could animate the width. It’s not that big of a deal, especially since it doesn’t reflow anything else.

Now we apply it to the bar:

.round-time-bar div {   /* ... */   animation: roundtime calc(var(--duration) * 1s) steps(var(--duration)) forwards;   transform-origin: left center; }

See how we’re yanking that --duration variable to set the duration of the animation? That does the heavy lifting. I’m also using it to set the same number of steps() so it “ticks” down. That “ticking” might be a visual UI thing that you like (I do), but it also accommodates the idea that JavaScript might re-render this bar at any time, and the ticks make it so you are less likely to notice. I used an integer for the duration value so that it could do double-duty like this.

If you want a smooth animation though, we could do that as a variation, like:

<div class="round-time-bar" data-style="smooth" ... />

Then not do the steps:

.round-time-bar[data-style="smooth"] div {   animation: roundtime calc(var(--duration) * 1s) linear forwards; }

Note we’re also using a linear animation, which seems to make sense for a timer. Time, as it were, doesn’t ease. Or does it? Whatever, it’s your call. If you want a timer that appears to speed up or slow down at certain points, go for it.

We can use the same variation data-attribute-driven API for things like color variations:

.round-time-bar[data-color="blue"] div {   background: linear-gradient(to bottom, #64b5f6, #1565c0); }

And one final variation is making each “second” a fixed width. That way, a 10 second timer will literally look longer than a 5 second timer:

.round-time-bar[data-style="fixed"] div {   width: calc(var(--duration) * 5%); }

Here’s the demo:

Notice the little trick in there for restarting CSS animation.

Oh, and hey, I know there is a <meter> element which is maybe a bit more semantic, but it brings it’s own UI which isn’t animatable like I wanted things to be here — at least not without fighting it. But I wonder if it’s more accessible? Does it announce its current value in a useful way? Would it be a more accessible timer if we were updating a <meter> in real-time with JavaScript? If anyone knows, I can link up a solution here.


The post Timer Bars in CSS with Custom Properties appeared first on CSS-Tricks.

You can support CSS-Tricks by being an MVP Supporter.

CSS-Tricks

, , ,
[Top]