Rendering Lists Using React Virtualized

Working with data in React is relatively easy because React is designed to handle data as state. The hassle begins when the amount of data you need to consume becomes massive. For example, say you have to handle a dataset which is between 500-1,000 records. This can result in massive loads and lead performance problems. Well, we’re going to look at how we can make use of virtualized lists in React to seamlessly render a long list of data in your application.

We’re going to use the React Virtualized component to get what we need. It will allow us to take large sets of data, process them on the fly, and render them with little-to-no jank.

The setup

React Virtualized already has a detailed set of instructions to get it up and running, so please check out the repo to get started.

We’re going to want data to work with, so we will set up a function which uses faker to create a large data set.

function createRecord(count) { let records = []; for (let i = 0; i < count; i++) { records.push({ username: faker.internet.userName(), email: faker.internet.email() }); } return records;
}

Next, we will pass it the number of data records we want to create, like so:

const records = createRecord(1000);

Alright, now we have what we need to work on rendering a list of those records!

Creating a virtualized list

Here’s the list we want to create, sans styling. We could make use of the few presentational styles that the library includes by importing the included CSS file, but we’re going to leave that out in this post.

See the Pen React Virtualized 1 by Kingsley Silas Chijioke (@kinsomicrote) on CodePen.

Go ahead and re-run that demo. Crazy fast, right?

You might wonder what the heck React Virtualized is doing behind the scenes to make that happen. Turns out it’s a bunch of crazy and cool sizing, positioning, transforms and transitions that allow the records to scroll in and out of view. The data is already there and rendered. React Virtualized creates a window frame that allows records to slide in and out of view as the user scrolls through it.

To render a virtualized list in React Virtualized, we make use of its List component, which uses a Grid component internally to render the list.

First, we start by setting up rowRenderer, which is responsible for displaying a single row and sets up an index that assigns an ID to each record.

rowRenderer = ({ index, isScrolling, key, style }) => { return ( <div key={key} style={style}> <div>{this.props.data[index].username}</div> <div>{this.props.data[index].email}</div> </div> ); };

As you can see, this returns a single div node that contains two additional divs: one for the username and another for the email. You know, a common list pattern to display users.

rowRenderer accepts several parameters. Here’s what they are and what each one does:

  • index: The numeric ID of a record.
  • isScrolling: Indicates if the scrolling is occurring in the List component.
  • isVisible: Determines if a row is visible or out of view.
  • key: The records position in the array.
  • parent: Defines whether the list is a parent or a child of another list.
  • style: A style object to position the row.

Now that we know more about the rowRenderer function, let’s make put it to use in the List component:

<List rowCount={this.props.data.length} width={width} height={height} rowHeight={rowHeight} rowRenderer={this.rowRenderer} overscanRowCount={3}
/>

You may have noticed a few new parameters. Here’s what they are:

  • rowCount: This takes the numbers of a row in a list that we pass to calculate the length of our list.
  • width: The width of the list.
  • height: The height of the list.
  • rowHeight: This can be a number or a function that returns a row height given its index.
  • rowRenderer: This is responsible for rendering the row. the list is not supposed to be passed directly, so we pass the rowRenderer function that we created in this tutorial.
  • overscanRowCount: This is used to render additional rows in the direction the user scrolls. It reduces the chances of the user scrolling faster than the virtualized content is rendered.

At the end, your code should look something like this;

const { List } = ReactVirtualized ... const height = 700;
const rowHeight = 40;
const width = 800; class App extends React.Component { rowRenderer = ({ index, isScrolling, key, style }) => { return ( <div key={key} style={style}> <div>{this.props.data[index].username}</div> <div>{this.props.data[index].email}</div> </div> ); }; render() { return ( <div> <h2>Details</h2> <List rowCount={this.props.data.length} width={width} height={height} rowHeight={rowHeight} rowRenderer={this.rowRenderer} overscanRowCount={3} /> </div> ); }
}

Cell measurer

According to the documentation, a cell measurer is a higher-order component that is used to temporarily render a list. It’s not yet visible to the user at this point, but the data is held and ready to display.

Why should you care about this? The popular use case is a situation where the value of your rowHeight is dynamic. React Virtualized can render the height of the row on render then cache that height so it no longer needs to calculate as data scrolls out of view — it’s always the right height, no matter the content it contains!

First, we create our cache, which can be done in our component’s constructor using CellMeasurerCache:

constructor() { super() this.cache = new CellMeasurerCache({ fixedWidth: true, defaultHeight: 100 })
}

We make use of the cache when we set up the List component;

<List rowCount={this.props.data.length} width={rowWidth} height={listHeight} deferredMeasurementCache={this.cache} rowHeight={this.cache.rowHeight} rowRenderer={this.renderRow} overscanRowCount={3}
/>

The value passed to deferredMeasurementCache will be used to temporarily rendering the data, then — as the calculated value for rowHeight comes in — additional rows will flow in like they were always there.

Next, though, we will make use of React Virtualized’s CellMeasurer component inside our rowRenderer function instead of the div we initially set up as a placeholder:

rowRenderer = ({ index, parent, key, style }) => { return ( <CellMeasurer key={key} cache={this.cache} parent={parent} columnIndex={0} rowIndex={index} > <div style={style}> <div>{this.props.data[index].username}</div> <div>{this.props.data[index].email}</div> </div> </CellMeasurer> ); };

Now the data is fetched, cached and ready to display in the virtual window at will!

Virtualized table

Yeah, so the main point of this post is to cover lists, but what if we actually want to render data to a table instead? React Virtualized has you covered on that front, too. In this case, we will make use of Table and Column components that come baked into React Virtualized.

Here’s how we would put those components to use in our primary App component:

class App extends React.Component { render() { return ( <div> <h2>Details</h2> <Table width={500} height={300} headerHeight={20} rowHeight={40} rowCount={this.props.data.length} rowGetter={({ index }) => this.props.data[index]} > <Column label='Username' dataKey='username' width={100} /> <Column width={200} label='Email' dataKey='email' /> </Table> </div> ); }
}

The Table component accepts the following parameters:

  • width: The width of the table.
  • height: The height of the table.
  • headerHeight: The table header height.
  • rowHeight: The height of a row given its index.
  • rowCount: This is the initial number of rows we want in the table. It’s the same as the way we defined the number of records we wanted to start with in the List component example.
  • rowGetter: This returns the data of a specific row by its index.

If you take a look at the Column component, you will notice that we put a dataKey parameter to use. That passes the data for each column we called in the dataKey, which receives a unique identifier for that data. Remember that in the function where we create our random data, we make use of two keys; username and email. This is why we have the dataKey of one column set as username and the other set as email.

In conclusion

Hopefully, this walkthrough gives you a good idea of what React Virtualized is capable of doing, how it can make rendering large data sets into lists and tables super fast, and how to put it to use in a project.

We’ve only scratched the surface here. The library is capable of handling a lot of other use cases, like generating placeholders for the data records on scroll, an infinite loading component to fetch and cache data in real-time, a method for allowing arrow keys to navigate through the data, and a slick grid and masonry layouts that we didn’t even cover here.

That should give you a lot to play around with!

Plus, the package is highly maintained. In fact, you can join the Slack group to keep up with the project, contribute to it, and generally get to connect with other folks.

It’s also worth noting that React Virtualized has it own tag in StackOverflow and that can be a good resource to find questions other people have asked about it, or even post your own questions.

Oh, and if you’ve put React Virtualized to use on a project, we’d love to know it! Share it with us in the comments with some notes on how you approached it or what you learned from it.

The post Rendering Lists Using React Virtualized appeared first on CSS-Tricks.

Keep Math in the CSS

There is a sentiment that leaving math calculations in your CSS is a good idea that I agree with. This is for math that you could calculate at authoring time, but specifically chose not to. For instance, if you needed a 7-column float-based grid (don’t ask), it’s cleaner and more intuitive:

.col { /* groan */ width: 14.2857142857%; /* oh, I get it */ width: calc(100% / 7);
}

You could probably prove that the calc() takes the computer 0.0000001% longer, so explicitly defining the width is technically faster for performance reason — but that is about the equivalent of not using punctuation in sentences because it saves HTML weight.

That math can be a little more complicated as you continue. For example, like in our use cases for calc() article, what about columns in that 7-column grid that span?

.column-1-7 { width: calc(100% / 7);
} .column-2-7 { width: calc(100% / 7 * 2);
} .column-3-7 { width: calc(100% / 7 * 3);
}

I’d say that’s rather clean to read and manage.

The readability of the math can be enhanced by comments if it gets too complicated. Say you are trying to account for a margin-based gutter with padding inside of an element:

.parent { width: 600px; padding: 18px;
} .left { /* base width - 1/2 horizontal padding */ width: calc(400px - 18px); margin-right: 1rem; /* gutter */
} .right { /* base width - 1/2 horizontal padding - gutter */ width: calc(200px - 1rem - 18px);
}

Again, I’d say that’s pretty readable, but it’s also a good amount of repetition. This might call for using variables. We’ll do it with CSS custom properties for fun. You have to pick what is worthy of a variable and what isn’t. You might need fewer comments as the code becomes somewhat self-documenting:

.parent { --padding: 18px; --gutter: 1rem; width: 600px; padding: var(--padding);
} .left { width: calc(400px - var(--padding)); margin-right: var(--gutter);
} .right { width: calc(200px - var(--gutter) - var(--padding));
}

That is a decent balance to me. Here’s a step further:

.parent { --padding: 18px; --gutter: 1rem; --parentWidth: 600px; --leftSize: 2/3; --rightSize: 1/3; width: var(--parentWidth); padding: var(--padding);
} .left { width: calc(calc(var(--parentWidth) * var(--leftSize)) - var(--padding)); margin-right: var(--gutter);
} .right { width: calc(calc(var(--parentWidth) * var(--rightSize)) - var(--gutter) - var(--padding));
}

Every single number has been given a variable in there. Too far? Maybe. It certainly makes those width declarations pretty hard to wrap your head around quickly. Ana Tudor does some serious stuff with calc(), as proof that everyone’s comfort level with this stuff is different.

One of the things that made me think of all this is a recent article from James Nash — “Hardcore CSS calc()” — where he builds this:

While the solution took a heavily math-y road to get there, it ends up being only sort of medium-level calculation on the ol’ complexity meter. And note that not everything gets a variable’ only the most re-used bits:

See the Pen Fluid 1 + 2 thumbnail block by James Nash (@cirrus) on CodePen.

The post Keep Math in the CSS appeared first on CSS-Tricks.

Web Designs That Feel Like Ancient History, but Are More Recent Than You Think

Flickr announced not long ago that they are limiting free accounts to 1,000 photos. I don’t particularly mind that (because it seems like sound business sense), although it is a bit sad that a ton of photos will be nuked from the internet. I imagine the Internet Archive will swoop in and get most of it. And oh hey, the Twitter account @FlickrJubilee is showcasing Flickr users that could really use a gifted pro account so their amazing photos are not lost, if you’re feeling generous and want to contribute.

This change doesn’t affect pro accounts. I’ve been pro forever on Flickr, so my photos were never at risk, but the big change has me thinking it’s about time to spin down Flickr for myself. I’ve been keeping all my photos on iCloud/Photos for years now anyway so it seems kind redundant to keep Flickr around.

I went into the Flickr settings and exported all my photos, got a bunch of gigabytes of exported photos, and loaded them into Photos. Sadly, the exported photos have zero metadata, so there will forever be this obnoxious chunk of thousands upon thousands of photos in my Photos collection that all look like they were taken on the same day and with no location.

Anyway, that was way too long of an intro to say: I found a bunch of old website screenshots! Not a ton, but it looks like I used Flickr to store a handful of web designs I found interesting in some way a number of years back. What’s interesting today is how dated they look when they were created not that long ago. Shows how fast things change.

Here they are.


It’s not terribly surprising to me to hear people push back on the same-ness of web design these days, and to blame things like frameworks, component-driven architecture, and design systems for it. It wasn’t long ago when it seemed like we were trying harder to be fancy and unique with our designs — things like shadow treatments, reflective images and skeuomorphic enhancements. I don’t mean to make sweeping generalizations here… merely a difference between what we considered to be boring and fancy work back than compared to now, of course.

The post Web Designs That Feel Like Ancient History, but Are More Recent Than You Think appeared first on CSS-Tricks.

Creating an Animated Login Form for TouchID

I came across this amazing Dribbble shot by Jakub Reis a while back. It caught my eye and I knew that I just had to try recreating it in code. At that moment, I didn’t know how. I tried out a bunch of different things, and about a year later, I finally managed to make this demo.

I learned a couple of things along the way, so let me take you on a little journey of what I did to make this because you may learn a thing or two as well.

See the Pen Opening screen for a banking app by Kirill Kiyutin (@kiyutink) on CodePen.

Step 1: Split the work into parts

I watched the original GIF many times. My goal was to split the animation into small, digestible chunks and I was able to break it down like this:

I know, it looks a lot — but we can do this!

Step 2: Take the original demo apart frame-by-frame

I needed to extract as much info as I could out of the original GIF to have a good understanding of the animation, so I split it up into single frames. There actually are a lot of services that can do this for us. I used one at ezgif.com but it could have just as easily been something else. Either way, this enables us to get details such as the colors, sizes, and proportions of all the different elements we need to create.

Oh, and we still need to turn the fingerprint into an SVG. Again, there are plenty of apps that will help us here. I used Adobe Illustrator to trace the fingerprint with the pen tool to get this set of paths:

See the Pen css-t. paths by Kirill Kiyutin (@kiyutink) on CodePen.

We’ll go through the same process with the line chart that appears towards the end of the animation, so might as well keep that vector editor open. 🙂

Step 3: Implement the animations

I’ll explain how the animations work in the final pen, but you can also find some of the unsuccessful approaches I took along the way in the end of the article.

I’ll focus on the important parts here and you can refer to the demos for the full code.

Filling the fingerprint

Let’s create the HTML structure of the phone screen and the fingerprint.

<div class="demo"> <div class="demo__screen demo__screen--clickable"> <svg class="demo__fprint" viewBox="0 0 180 320"> <!-- removes-forwards and removes-backwards classes will be helpful later on --> <path class="demo__fprint-path demo__fprint-path--removes-backwards demo__fprint-path--pinkish" d="M46.1,214.3c0,0-4.7-15.6,4.1-33.3"/> <path class="demo__fprint-path demo__fprint-path--removes-backwards demo__fprint-path--purplish" d="M53.5,176.8c0,0,18.2-30.3,57.5-13.7"/> <path class="demo__fprint-path demo__fprint-path--removes-forwards demo__fprint-path--pinkish" d="M115.8,166.5c0,0,19.1,8.7,19.6,38.4"/> <!-- ... and about 20 more paths like this --> </svg> 

The styles are quite simple so far. Note that I am using Sass throughout the demo — I find that it helps keep the work clean and helps with some of the heavier lifting we need to do.

// I use a $scale variable to quickly change the scaling of the whole pen, so I can focus on the animation and decide on the size later on.
$scale: 1.65;
$purplish-color: #8742cc;
$pinkish-color: #a94a8c;
$bg-color: #372546; // The main container
.demo { background: linear-gradient(45deg, lighten($pinkish-color, 10%), lighten($purplish-color, 10%)); min-height: 100vh; display: flex; justify-content: center; align-items: center; font-size: 0; user-select: none; overflow: hidden; position: relative; // The screen that holds the login component &__screen { position: relative; background-color: $bg-color; overflow: hidden; flex-shrink: 0; &--clickable { cursor: pointer; -webkit-tap-highlight-color: transparent; } } // Styles the fingerprint SVG paths &__fprint-path { stroke-width: 2.5px; stroke-linecap: round; fill: none; stroke: white; visibility: hidden; transition: opacity 0.5s ease; &--pinkish { stroke: $pinkish-color; } &--purplish { stroke: $purplish-color; } } // Sizes positions the fingerprint SVG &__fprint { width: 180px * $scale; height: 320px * $scale; position: relative; top: 20px * $scale; overflow: visible; // This is going to serve as background to show "unfilled" paths. we're gonna remove it at the moment where the filling animation is over background-image: url('https://kiyutink.github.io/svg/fprintBackground.svg'); background-size: cover; &--no-bg { background-image: none; } }
}

Now the hard part: making the fingerprint interactive. You can read about the animation of SVG lines here. That’s the method we’ll use to fill in each individual path.

Let’s create a class that describes a path element so that it’s easier to manipulate the paths later on.

class Path { constructor(selector, index) { this.index = index; this.querySelection = document.querySelectorAll(selector)[index]; this.length = this.querySelection.getTotalLength(); this.$ = $(selector).eq(index); this.setDasharray(); this.removesForwards = this.$.hasClass('demo__fprint-path--removes-forwards'); } setDasharray() { this.$.css('stroke-dasharray', `${this.length} ${this.length + 2}`); return this; } offset(ratio) { this.$.css('stroke-dashoffset', -this.length * ratio + 1); return this; } makeVisible() { this.$.css('visibility', 'visible'); return this; }
}

The general idea is this: Create an instance of this class for each path that we have in the fingerprint, and modify them in every frame. The paths will start with an offset ratio of -1 (fully invisible) and then will increase the offset ratio (which we’ll refer to as “offset” from here on) by a constant value each frame until they get to 0 (fully visible). The filling animation will be over at this point.

If you’ve never animated anything with this frame-by-frame approach, here’s a very simple demo to help understand how this works:

See the Pen 60fps raf animation proof of concept by Kirill Kiyutin (@kiyutink) on CodePen.

We should also handle the case where the user stops tapping or pressing the mouse button. In this case, we will animate in the opposite direction (subtracting a constant value from the offset each frame until it gets to -1 again).

Let’s create the function that calculates the offset increment for every frame — this’ll be useful later on.

function getPropertyIncrement(startValue, endValue, transitionDuration) { // We animate at 60 fps const TICK_TIME = 1000 / 60; const ticksToComplete = transitionDuration / TICK_TIME; return (endValue - startValue) / ticksToComplete;
}

Now it’s time to animate! We will keep the fingerprint paths in a single array:

let fprintPaths = []; // We create an instance of Path for every existing path. // We don't want the paths to be visible at first and then // disappear after the JavaScript runs, so we set them to // be invisible in CSS. That way we can offset them first // and then make them visible.
for (let i = 0; i < $(fprintPathSelector).length; i++) { fprintPaths.push(new Path(fprintPathSelector, i)); fprintPaths[i].offset(-1).makeVisible();
}

We will go through that array for each frame in the animation, animating the paths one by one:

let fprintTick = getPropertyIncrement(0, 1, TIME_TO_FILL_FPRINT); function fprintFrame(timestamp) { // We don't want to paint if less than 1000 / 65 ms elapsed // since the last frame (because there are faster screens // out there and we want the animation to look the same on // all devices). We use 65 instead of 60 because, even on // 60 Hz screens, `requestAnimationFrame` can sometimes be called // a little sooner, which can result in a skipped frame. if (timestamp - lastRafCallTimestamp >= 1000 / 65) { lastRafCallTimestamp = timestamp; curFprintPathsOffset += fprintTick * fprintProgressionDirection; offsetAllFprintPaths(curFprintPathsOffset); } // Schedule the next frame if the animation isn't over if (curFprintPathsOffset >= -1 && curFprintPathsOffset <= 0) { isFprintAnimationInProgress = true; window.requestAnimationFrame(fprintFrame); } // The animation is over. We can schedule next animation steps else if (curFprintPathsOffset > 0) { curFprintPathsOffset = 0; offsetAllFprintPaths(curFprintPathsOffset); isFprintAnimationInProgress = false; isFprintAnimationOver = true; // Remove the background with grey paths $fprint.addClass('demo__fprint--no-bg'); // Schedule the next animation step - transforming one of the paths into a string // (this function is not implemented at this step yet, but we'll do that soon) startElasticAnimation(); // Schedule the fingerprint removal (removeFprint function will be implemented in the next section) window.requestAnimationFrame(removeFprint); } // The fingerprint is back to the original state (the user has stopped holding the mouse down) else if (curFprintPathsOffset < -1) { curFprintPathsOffset = -1; offsetAllFprintPaths(curFprintPathsOffset); isFprintAnimationInProgress = false; }
}

And we’ll attach some event listeners to the demo:

$screen.on('mousedown touchstart', function() { fprintProgressionDirection = 1; // If the animation is already in progress, // we don't schedule the next frame since it's // already scheduled in the `fprintFrame`. Also, // we obviously don't schedule it if the animation // is already over. That's why we have two separate // flags for these conditions. if (!isFprintAnimationInProgress && !isFprintAnimationOver) window.requestAnimationFrame(fprintFrame);
}) // On `mouseup` / `touchend` we flip the animation direction
$(document).on('mouseup touchend', function() { fprintProgressionDirection = -1; if (!isFprintAnimationInProgress && !isFprintAnimationOver) window.requestAnimationFrame(fprintFrame);
})

…and now we should be done with the first step! Here’s how our work looks at this step:

See the Pen css-t. step 1 by Kirill Kiyutin (@kiyutink) on CodePen.

Removing the fingerprint

This part is pretty similar to the first one, only now we have to account for the fact that some of the paths remove in one direction and the rest of them in the other. That’s why we added the --removes-forwards modifier earlier.

First, we’ll have two additional arrays: one for the paths that are removed forwards and another one for the ones that are removed backwards:

const fprintPathsFirstHalf = [];
const fprintPathsSecondHalf = []; for (let i = 0; i < $(fprintPathSelector).length; i++) { // ... if (fprintPaths[i].removesForwards) fprintPathsSecondHalf.push(fprintPaths[i]); else fprintPathsFirstHalf.push(fprintPaths[i]);
}

…and we’ll write a function that offsets them in the right direction:

function offsetFprintPathsByHalves(ratio) { fprintPathsFirstHalf.forEach(path => path.offset(ratio)); fprintPathsSecondHalf.forEach(path => path.offset(-ratio));
}

We’re also going to need a function that draws the frames:

function removeFprintFrame(timestamp) { // Drop the frame if we're faster than 65 fps if (timestamp - lastRafCallTimestamp >= 1000 / 65) { curFprintPathsOffset += fprintTick * fprintProgressionDirection; offsetFprintPathsByHalves(curFprintPathsOffset); lastRafCallTimestamp = timestamp; } // Schedule the next frame if the animation isn't over if (curFprintPathsOffset >= -1) window.requestAnimationFrame(removeFprintFrame); else { // Due to the floating point errors, the final offset might be // slightly less than -1, so if it exceeds that, we'll just // assign -1 to it and animate one more frame curFprintPathsOffset = -1; offsetAllFprintPaths(curFprintPathsOffset); }
} function removeFprint() { fprintProgressionDirection = -1; window.requestAnimationFrame(removeFprintFrame);
}

Now all that’s left is to call removeFprint when we’re done filling the fingerprint:

function fprintFrame(timestamp) { // ... else if (curFprintPathsOffset > 0) { // ... window.requestAnimationFrame(removeFprint); } // ...
}

Let’s check our work now:

See the Pen css-t. part 2 by Kirill Kiyutin (@kiyutink) on CodePen.

Animating the path ends

You can see that, as the fingerprint is almost removed, some of its paths are longer than they were in the beginning. I moved them into separate paths that start animating at the right moment. I could incorporate them into the existing paths, but it would be much harder and at 60fps would make next-to-no difference.

Let’s create them:

<path class="demo__ending-path demo__ending-path--pinkish" d="M48.4,220c-5.8,4.2-6.9,11.5-7.6,18.1c-0.8,6.7-0.9,14.9-9.9,12.4c-9.1-2.5-14.7-5.4-19.9-13.4c-3.4-5.2-0.4-12.3,2.3-17.2c3.2-5.9,6.8-13,14.5-11.6c3.5,0.6,7.7,3.4,4.5,7.1"/>
<!-- and 5 more paths like this -->

…and apply some basic styles:

&__ending-path { fill: none; stroke-width: 2.5px; stroke-dasharray: 60 1000; stroke-dashoffset: 61; stroke-linecap: round; will-change: stroke-dashoffset, stroke-dasharray, opacity; transform: translateZ(0); transition: stroke-dashoffset 1s ease, stroke-dasharray 0.5s linear, opacity 0.75s ease; &--removed { stroke-dashoffset: -130; stroke-dasharray: 5 1000; } &--transparent { opacity: 0; } &--pinkish { stroke: $pinkish-color; } &--purplish { stroke: $purplish-color; }
}

Now, we have to add the --removed modifier to flow these paths in at the right moment:

function removeFprint() { $endingPaths.addClass('demo__ending-path--removed'); setTimeout(() => { $endingPaths.addClass('demo__ending-path--transparent'); }, TIME_TO_REMOVE_FPRINT * 0.9); // ...
}

Now our work is really starting to take shape:

See the Pen css-t. part 3 by Kirill Kiyutin (@kiyutink) on CodePen.

Morphing the fingerprint

OK, I found this part to be really hard to do on my own, but it’s really easy to implement with GSAP’s morphSVG plugin.

Let’s create the invisible paths (well, a path and a line to be exact 🙂) that will be the keyframes for our string:

<line id='demo__straight-path' x1="0" y1="151.3" x2="180" y2="151.3"/>
<path class="demo__hidden-path" id='demo__arc-to-top' d="M0,148.4c62.3-13.5,122.3-13.5,180,0"/>

Then we’ll use morphSVG to transition the path in between the keyframes:

const $elasticPath = $('#demo__elastic-path'); const ELASTIC_TRANSITION_TIME_TO_STRAIGHT = 250;
const WOBBLE_TIME = 1000; function startElasticAnimation() { $elasticPath.css('stroke-dasharray', 'none'); const elasticAnimationTimeline = new TimelineLite(); elasticAnimationTimeline .to('#demo__elastic-path', ELASTIC_TRANSITION_TIME_TO_STRAIGHT / 1000, { delay: TIME_TO_REMOVE_FPRINT / 1000 * 0.7, morphSVG: '#demo__arc-to-top' }) .to('#demo__elastic-path', WOBBLE_TIME / 1000, { morphSVG: '#demo__straight-path', // I played with the easing a bit to get that "vibration" effect ease: Elastic.easeOut.config(1, 0.3) })
}

We’ll call this function inside the fprintFrame once the fingerprint is filled:

function fprintFrame(timestamp) { // ... else if (curFprintPathsOffset > 0) { // ... startElasticAnimation(); // ... } // ...
}

The outcome is this:

See the Pen css-t. part 4 by Kirill Kiyutin (@kiyutink) on CodePen.

Animating the floating bullet

For this, I used some simple straightforward CSS animations. I chose the timing functions to emulate the gravity. You can play around with the timing functions here or here.

Let’s create a div:

<div class="demo__bullet"></div>

…and apply some styles to it:

&__bullet { position: absolute; width: 4px * $scale; height: 4px * $scale; background-color: white; border-radius: 50%; top: 210px * $scale; left: 88px * $scale; opacity: 0; transition: all 0.7s cubic-bezier(0.455, 0.030, 0.515, 0.955); will-change: transform, opacity; // This will be applied after the bullet has descended, to create a transparent "aura" around it &--with-aura { box-shadow: 0 0 0 3px * $scale rgba(255, 255, 255, 0.3); } // This will be applied to make the bullet go up &--elevated { transform: translate3d(0, -250px * $scale, 0); opacity: 1; } // This will be applied to make the bullet go down &--descended { transform: translate3d(0, 30px * $scale, 0); opacity: 1; transition: all 0.6s cubic-bezier(0.285, 0.210, 0.605, 0.910); }
}

Then we tie it together by adding and removing classes based on a user’s interactions:

const DELAY_TO_BULLET_AURA = 300;
const ELEVATION_TIME = 700;
const DELAY_AFTER_ELEVATION = 700; const $bullet = $('.demo__bullet'); function elevateBullet() { $bullet.addClass('demo__bullet--elevated');
} function descendBullet() { $bullet.addClass('demo__bullet--descended').removeClass('demo__bullet--elevated'); animateBulletAura();
} function animateBulletAura() { setTimeout(() => $bullet.addClass('demo__bullet--with-aura'), DELAY_TO_BULLET_AURA);
} function animateBullet() { elevateBullet(); $screen.removeClass('demo__screen--clickable'); setTimeout(descendBullet, ELEVATION_TIME + DELAY_AFTER_ELEVATION);
}

Now, we need to call the animateBullet function:

function startElasticAnimation() { // ... animateBullet();
}

Here’s where we are at this point:

See the Pen css-t. part 5 by Kirill Kiyutin (@kiyutink) on CodePen.

Morphing the string into a graph

Now, let’s turn that string into a graph where the bullet can land. We’ll add another keyframe to the morphSVG animation.

<path class="demo__hidden-path" id='demo__curve' d="M0,140.2c13.1-10.5,34.7-17,48.5-4.1c5.5,5.2,7.6,12.1,9.2,19.2c2.4,10.5,4.3,21,7.2,31.4c2.4,8.6,4.3,19.6,10.4,26.7c4.3,5,17.7,13.4,23.1,4.8c5.9-9.4,6.8-22.5,9.7-33c4.9-17.8,13-14.6,15.7-14.6c1.8,0,9,2.3,15.4,5.4c6.2,3,11.9,7.7,17.9,11.2c7,4.1,16.5,9.2,22.8,6.6"/>

We add this keyframe into our timeline like this:

const DELAY_TO_CURVE = 350;
const ELASTIC_TRANSITION_TIME_TO_CURVED = 300; function startElasticAnimation() { // ... elasticAnimationTimeline // ... .to('#demo__elastic-path', ELASTIC_TRANSITION_TIME_TO_CURVED / 1000, { delay: DELAY_TO_CURVE / 1000, morphSVG: '#demo__curve' }) // ...
}

Here’s what we get:

See the Pen css-t. part 6 by Kirill Kiyutin (@kiyutink) on CodePen.

Exploding the particles

This is a fun animation. First, we’ll create a couple of new divs that contain the particles that explode:

<div class="demo__logo-particles"> <div class="demo__logo-particle"></div> <!-- and several more of these -->
</div>
<div class="demo__money-particles"> <div class="demo__money-particle"></div> <!-- and several more of these -->
</div>

The two explosions are practically the same with the exception of a few parameters. That’s where SCSS mixins will come in handy. We can write the function once and use it on our divs.

@mixin particlesContainer($top) { position: absolute; width: 2px * $scale; height: 2px * $scale; left: 89px * $scale; top: $top * $scale; // We'll hide the whole container to not show the particles initially opacity: 0; &--visible { opacity: 1; }
} // The $sweep parameter shows how far from the center (horizontally) the initial positions of the particles can be
@mixin particle($sweep, $time) { width: 1.5px * $scale; height: 1.5px * $scale; border-radius: 50%; background-color: white; opacity: 1; transition: all $time ease; position: absolute; will-change: transform; // Phones can't handle the particles very well 🙁 @media (max-width: 400px) { display: none; } @for $i from 1 through 30 { &:nth-child(#{$i}) { left: (random($sweep) - $sweep / 2) * $scale + px; @if random(100) > 50 { background-color: $purplish-color; } @else { background-color: $pinkish-color; } } &--exploded:nth-child(#{$i}) { transform: translate3d((random(110) - 55) * $scale + px, random(35) * $scale + px, 0); opacity: 0; } }
}

Note the comment in the code that the particles don’t perform particularly well on less powerful devices such as phones. Perhaps there’s another approach here that would solve this if anyone has ideas and wants to chime in.

Alright, let’s put the mixins to use on the elements:

&__logo-particles { @include particlesContainer(15px);
} &__logo-particle { @include particle(50, 1.7s);
} &__money-particles { @include particlesContainer(100px);
} &__money-particle { @include particle(100, 1.5s);
}

Now we add the classes to the divs at the right time in JavaScript:

const DELAY_TO_ANIMATE_MONEY_PARTICLES = 300;
const DELAY_TO_ANIMATE_LOGO_PARTICLES = 500; const $moneyParticles = $('.demo__money-particle');
const $moneyParticlesContainer = $('.demo__money-particles');
const $logoParticlesContainer = $('.demo__logo-particles');
const $logoParticles = $('.demo__logo-particle'); function animateMoneyParticles() { setTimeout(() => { $moneyParticlesContainer.addClass('demo__money-particles--visible') $moneyParticles.addClass('demo__money-particle--exploded'); }, DELAY_TO_ANIMATE_MONEY_PARTICLES); } function animateLogoParticles() { setTimeout(() => { $logoParticlesContainer.addClass('demo__logo-particles--visible') $logoParticles.addClass('demo__logo-particle--exploded'); }, DELAY_TO_ANIMATE_LOGO_PARTICLES); } function elevateBullet() { // ... animateMoneyParticles(); animateLogoParticles();
}

Here’s where we’re at:

See the Pen css-t. part 7 by Kirill Kiyutin (@kiyutink) on CodePen.

Animating the account balance

Every digit will have a few random numbers that we’ll scroll through:

<div class="demo__money"> <div class="demo__money-currency">$</div> <!-- every digit will be a div like this one --> <div class="demo__money-digit"> 1 2 3 4 5 6 7 8 1 </div> // ...
</div>

We will put different transition times on all of the digits so that the animations are staggered. We can use a SCSS loop for that:

&__money-digit { // ... // we start from 2 because the first child is the currency sign 🙂 @for $i from 2 through 6 { &:nth-child(#{$i}) { transition: transform 0.1s * $i + 0.2s ease; transition-delay: 0.3s; transform: translate3d(0, -26px * $scale * 8, 0); } &--visible:nth-child(#{$i}) { transform: none; } }
}

All that’s left is to add the CSS classes at the right time:

const $money = $('.demo__money');
const $moneyDigits = $('.demo__money-digit'); function animateMoney() { $money.addClass('demo__money--visible'); $moneyDigits.addClass('demo__money-digit--visible');
} function descendBullet() { // ... animateMoney(); // ...
}

Now sit back and marvel at our work:

See the Pen css-t. part 8 by Kirill Kiyutin (@kiyutink) on CodePen.

The rest of the animations are fairly simple and involve light CSS transitions, so I won’t get into them to keep things brief. You can see all of the final code in the completed demo.

View Demo

Some final words

  • In my early attempts I tried using CSS transitions for all of the animation work. I found it virtually impossible to control the progress and direction of the animation, so shortly I abandoned that idea and waited a month or so before starting again. In reality, if I knew back then that the Web Animations API was a thing, I would have tried to make use of it.
  • I tried making the explosion with Canvas for better performance (using this article as a reference), but I found it difficult to control the frame rate with two separate requestAnimationFrame chains. If you know how to do that, then maybe you can tell me in the comments (or write an article for CSS-Tricks 🙂).
  • After I got a first working prototype, I was really unhappy with its performance. I was hitting around 40-50fps on a PC, not to mention phones at all. I spent a lot of time optimizing the code and this article was a lot of help.
  • You can see that the graph has a gradient. I did that by declaring a gradient directly in the SVG defs block:
<defs> <linearGradient id="linear" x1="0%" y1="0%" x2="100%" y2="0%"> <stop offset="0%" stop-color="#8742cc"/> <stop offset="100%" stop-color="#a94a8c"/> </linearGradient>
</defs>

…and then applied it in the CSS properties:

fill: url(#linear);
stroke: url(#linear);

The whole process from start to finish — discovering the Dribbble shot and finishing the work — took me about a year. I was taking month-long breaks here and there either because I didn’t know how to approach a particular aspect or I simply didn’t have enough free time to work on it. The entire process was a really valuable experience and I learned a lot of new things along the way.

That being said, the biggest lesson to take away from this is that there’s no need to shy away from taking on an ambitious task, or feel discouraged if you don’t know how to approach it at first. The web is a big place and there is plenty of space to figure things out as you go along.

The post Creating an Animated Login Form for TouchID appeared first on CSS-Tricks.

Browser Diversity Commentary, Regarding the Edge News

Still no word from the horse’s mouth about the reported EdgeHTML demise, but I hear that’s coming later today. The blog posts are starting to roll in about the possible impact of this though.

Andre Garzia: While we Blink, we loose the Web:

Even though Opera, Beaker and Brave are all doing very good work, it is still Chrome engine behind them and that limits the amount of stuff they can build and innovate. It is like as if they were building cars, there is a lot they can do without actually changing the engine itself, and thats what the Web Browsers are becoming, everyone is working on parts of the car but all the engines are now Chrome and believe me, you don’t want all the engines to be the same, not even if they are all Gecko or if somehow we resurrect Presto, we want diversity of engines and not monoculture.

Tim Kadlec, Risking a Homogeneous Web:

I can understand the logic. Microsoft can’t put as many folks on Edge (including EdgeHTML for rendering and Chakra for JavaScript) as Google has done with Chromium (using Blink for rendering and V8 for JavaScript), so keeping up was always going to be a challenge. Now they can contribute to the same codebase and try to focus on the user-focused features. Whether this gets people to pay more attention to their next browser or not remains to be seen, but I get the thinking behind the move.

The big concern here is we’ve lost another voice from an engine perspective.

Ferdy Christant, The State of Web Browsers:

Edge is doomed. It was doomed and its next version will be equally doomed from the start. For the simple reason that Microsoft has close to no say in how browsers get installed: on mobile as a default app, and on desktop via web services under the control of Google. Switching to Chromium makes no difference in market share, as the only way to compete now is through the browser’s UI, not via the engine. Which isn’t a competition at all, since browser UI is a commodity.

I’ll link up the official statements as they come out.

The post Browser Diversity Commentary, Regarding the Edge News appeared first on CSS-Tricks.

DRY State Switching With CSS Variables: Fallbacks and Invalid Values

This is the second post in a two-part series that looks into the way CSS variables can be used to make the code for complex layouts and interactions less difficult to write and a lot easier to maintain. The first installment walks through various use cases where this technique applies. This post covers the use of fallbacks and invalid values to extend the technique to non-numeric values.

The strategy of using CSS Variables to drive the switching of layouts and interactions that we covered in the first post in this series comes with one major caveat: it only works with numeric values — lengths, percentages, angles, durations, frequencies, unit-less number values and so on. As a result, it can be really frustrating to know that you’re able to switch the computed values of more than ten properties with a single CSS variable, but then you need to explicitly switch the non-numeric values of properties like flex-direction or text-align from row to column or from left to right or the other way around.

One example would be the one below, where the text-align property depends on parity and the flex-direction depends on whether we are viewing the front end in the wide screen scenario or not.

Screenshot collage. On the left, we have the wide screen scenario, with four paragraphs as the four horizontal, offset based on parity slices of a disc. The slice numbering position is either to the right or left of the actual text content, depending on parity. The text alignment also depends on parity. In the middle, we have the normal screen case. The paragraphs are now full width rectangular elements. On the right, we have the narrow screen case. The paragraph numbering is always above the actual text content in this case.
Screenshot collage.

I complained about this and got a very interesting suggestion in return that makes use of CSS variable fallbacks and invalid values. It was interesting and gives us something new to work with, so let’s start with a short recap of what these are and go from there!

Fallback values

The fallback value of a CSS variable is the second and optional argument of the var() function. For example, let’s consider we have some .box elements whose background is set to a variable of --c:

.box { background: var(--c, #ccc) }

If we haven’t explicitly specified a value for the --c variable elsewhere, then the fallback value #ccc is used.

Now let’s say some of these boxes have a class of .special. Here, we can specify --c as being some kind of orange:

.special { --c: #f90 }

This way, the boxes with this .special class have an orange background, while the others use the light grey fallback.

See the Pen by thebabydino (@thebabydino) on CodePen.

There are a few things to note here.

First off, the fallback can be another CSS variable, which can have a CSS variable fallback itself and… we can fall down a really deep rabbit hole this way!

background: var(--c, var(--c0, var(--c1, var(--c2, var(--c3, var(--c4, #ccc))))))

Secondly, a comma separated list is a perfectly valid fallback value. In fact, everything specified after the first comma inside the var() function constitutes the fallback value, as seen in the example below:

background: linear-gradient(90deg, var(--stop-list, #ccc, #f90))

See the Pen by thebabydino (@thebabydino) on CodePen.

And last, but certainly not least, we can have different fallback values for the same variable used in different places, as illustrated by this example:

$highlight: #f90; a { border: solid 2px var(--c, #{rgba($highlight, 0)}) color: var(--c, #ccc); &:hover, &:focus { --c: #{$highlight} }
}

See the Pen by thebabydino (@thebabydino) on CodePen.

Invalid values

First off, I want to clarify what I mean by this. “Invalid values” is shorter and easier to remember, but what it really refers to any value that makes a declaration invalid at computed value time.

For example, consider the following piece of code:

--c: 1em;
background: var(--c)

1em is a valid length value, but this is not a valid value for the background-color property, so here this property will take its initial value (which is transparent) instead.

Putting it all together

Let’s say we have a bunch of paragraphs where we change the lightness of the color value to switch between black and white based on parity (as explained in the previous post in this series):

p { --i: 0; /* for --i: 0 (odd), the lightness is 0*100% = 0% (black) * for --i: 1 (even), the lightness is 1*100% = 100% (white)* / color: hsl(0, 0%, calc(var(--i)*100%)); &:nth-child(2n) { --i: 1 }
}

We also want the odd paragraphs to be right-aligned, while keeping the even ones left-aligned. In order to achieve this, we introduce a --parity variable which we don’t set explicitly in the general case — only for even items. What we do set in the general case is our previous variable, --i. We set it to the value of --parity with a fallback of 0:

p { --i: var(--parity, 0); color: hsl(0, 0%, calc(var(--i)*100%)); &:nth-child(2n) { --parity: 1 }
}

So far, this achieves exactly the same as the previous version of our code. However, if we take advantage of the fact that, we can use different fallback values in different places for the same variable, then we can also set text-align to the value of --parity using a fallback of… right!

text-align: var(--parity, right)

In the general case, where we’re not setting --parity explicitly; text-align uses the fallback right, which is a valid value, so we have right alignment. For the even items however, we’re setting --parity explicitly to 1, which is not a valid value for text-align. That means text-align reverts to its initial value, which is left.

See the Pen by thebabydino (@thebabydino) on CodePen.

Now we have right alignment for the odd items and left alignment for the even items while still putting a single CSS variable to use!

Dissecting a more complex example

Let’s consider we want to get the result below:

Screenshot. Shows a bunch of numbered cards. Odd ones have the numbering on the left, while even ones have it on the right. Odd ones are right-aligned, while even ones are left-aligned. Odd ones are shifted a bit to the right and have a bit of a clockwise rotation, while even ones are shifted and rotated by the same amounts, but in the opposite directions. All have a grey to orange gradient background, but for the odd ones, this gradient goes from left to right, while for the even ones it goes from right to left.
Numbered cards where even cards have symmetrical styles with respect to odd cards.

We create these cards with a paragraph element <p> for each one. We switch their box-sizing to border-box, then give them a width, a max-width, a padding and a margin. We also change the default font.

See the Pen by thebabydino (@thebabydino) on CodePen.

We’ve also added a dummy outline just to see the boundaries of these elements.

Next, let’s add the numbering using CSS counters and a :before pseudo-element:

p { /* same code as before */ counter-increment: c; &:before { content: counter(c, decimal-leading-zero) }
}

See the Pen by thebabydino (@thebabydino) on CodePen.

Now, we’ll give our paragraphs a flex layout and increase the size of the numbering:

p { /* same code as before */ display: flex; align-items: center; &:before { font-size: 2em; content: counter(c, decimal-leading-zero); }
}

See the Pen by thebabydino (@thebabydino) on CodePen.

Now comes the interesting part!

We set a switch --i that changes value with the parity — it’s 0 for the odd items and 1 for the even ones.

p { /* same code as before */ --i: 0; &:nth-child(2n) { --i: 1 }
}

Next, we want the numbering to be on the left for the odd items and on the right for the even ones. We achieve this via the order property. The initial value for this property is 0, for both the :before pseudo-element and the paragraph’s text content. If we set this order property to 1 for the numbering (the :before pseudo-element) of the even elements, then this moves the numbering after the content.

p { /* same code as before */ --i: 0; &:before { /* same code as before */ /* we don't really need to set order explicitly as 0 is the initial value */ order: 0; } &:nth-child(2n) { --i: 1; &:before { order: 1 } }
}

You may notice that, in this case, the order value is the same as the switch --i value, so in order to simplify things, we set the order to the switch value.

p { /* same code as before */ --i: 0; &:before { /* same code as before */ order: var(--i) } &:nth-child(2n) { --i: 1 }
}

See the Pen by thebabydino (@thebabydino) on CodePen.

Now we want a bit of spacing (let’s say $gap) in between the numbers and the paragraph text. This can be achieved with a lateral margin on the :before.

For the odd items, the item numbers are on the left, so we need a non-zero margin-right. For the even items, the item numbers are on the right, so we need a non-zero margin-left.

When the parity switch value is 0 for the odd items, the left margin is 0 = 0*$gap, while the right margin is $gap = 1*$gap = (1 - 0)*$gap.

Similarly for the even items, when the parity switch value is 1, the left margin is $gap = 1*$gap, while the right margin is 0 = 0*$gap = (1 - 1)*$gap.

The result in both cases is that margin-left is the parity switch value times the margin value ($gap), while margin-right is 1 minus the parity switch value, all multiplied with the margin value.

$gap: .75em; p { /* same code as before */ --i: 0; &:before { /* same code as before */ margin: 0 /* top */ calc((1 - var(--i))*#{$gap}) /* right */ 0 /* bottom */ calc(var(--i)*#{$gap}) /* left */; } &:nth-child(2n) { --i: 1 }
}

If we use the complementary value (1 - var(--i)) in more than one place, then it’s probably best to set it to another CSS variable --j.

$gap: .75em; p { /* same code as before */ --i: 0; --j: calc(1 - var(--i)); &:before { /* same code as before */ margin: 0 /* top */ calc(var(--j)*#{$gap}) /* right */ 0 /* bottom */ calc(var(--i)*#{$gap}) /* left */; } &:nth-child(2n) { --i: 1 }
}

See the Pen by thebabydino (@thebabydino) on CodePen.

Next, we want to give these items a proper background. This is a grey to orange gradient, going from left to right (or along a 90deg angle) in the case of odd items (parity switch --i: 0) and from right to left (at a -90deg angle) in the case of even items (parity switch --i: 1).

This means the absolute value of the gradient angle is the same (90deg), only the sign is different — it’s +1 for the odd items (--i: 0) and -1 for the even items (--i: 1).

In order to switch the sign, we use the approach we covered in the first post:

/* * for --i: 0, we have 1 - 2*0 = 1 - 0 = +1 * for --i: 1, we have 1 - 2*1 = 1 - 2 = -1 */
--s: calc(1 - 2*var(--i))

This way, our code becomes:

p { /* same code as before */ --i: 0; --s: calc(1 - 2*var(--i)); background: linear-gradient(calc(var(--s)*90deg), #ccc, #f90); &:nth-child(2n) { --i: 1 }
}

We can also remove the dummy outline since we don’t need it at this point:

See the Pen by thebabydino (@thebabydino) on CodePen.

Next, we do something similar for the transform property.

The odd items are translated a bit to the right (in the positive direction of the x axis) and rotated a bit in the clockwise (positive) direction, while the even items are translated a bit to the left (in the negative direction of the x axis) and rotated a bit in the other (negative) direction.

The translation and rotation amounts are the same; only the signs differ.

For the odd items, the transform chain is:

translate(10%) rotate(5deg)

While for the even items, we have:

translate(-10%) rotate(-5deg)

Using our sign --s variable, the unified code is:

p { /* same code as before */ --i: 0; --s: calc(1 - 2*var(--i)); transform: translate(calc(var(--s)*10%)) rotate(calc(var(--s)*5deg)); &:nth-child(2n) { --i: 1 }
}

This is now starting to look like something!

See the Pen by thebabydino (@thebabydino) on CodePen.

The next step is to round the card corners. For the odd cards, we want the corners on the left side to be rounded to a radius of half the height. For the even items, we want the corners on the right side to be rounded to the same radius.

Given we don’t know the heights of our cards, we just use a ridiculously large value, say something like 50vh, which gets scaled down to fit due to the way border-radius works. In our case, this means scaled down to whichever is smaller between half the item height (since going vertically has both a top and bottom rounded corner on the same side) and the full item width (since going horizontally has one rounded corner; either on the left or on the right, but not on both the right and the left).

This means we want the corners on the left to have this radius ($r: 50vh) for odd items (--i: 0) and the ones on the right to have the same radius for even items (--i: 1). As a result, we do something pretty similar to the numbering margin case:

$r: 50vh; p { /* same code as before */ --i: 0; --j: calc(1 - var(--i)); --r0: calc(var(--j)*#{$r}); --r1: calc(var(--i)*#{$r}); /* clockwise from the top left */ border-radius: var(--r0) /* top left */ var(--r1) /* top right */ var(--r1) /* bottom right */ var(--r0) /* bottom left */; &:nth-child(2n) { --i: 1 }
}

See the Pen by thebabydino (@thebabydino) on CodePen.

Now comes the truly interesting part — text alignment! We want the text in the odd items to be aligned right, while the text in the even items is aligned left. The only problem is that text-align doesn’t take a number value so, no addition or multiplication tricks can help us here.

What can help is combining the use of fallback and invalid values for CSS variables. To do this, we introduce another parity variable --p and it’s this variable that we actually set to 1 for even items. Unlike --i before, we never set --p explicitly for the general case as we want different fallback values of this variable to be used for different properties.

As for --i, we set it to --p with a fallback value of 0. This fallback value of 0 is the value that actually gets used in the general case, since we never explicitly set --p there. For the even case, where we explicitly set --p to 1, --i becomes 1 as well.

At the same time, we set the text-align property to --p with a fallback value of right in the general case. In the even case, where we have --p explicitly set to 1, the text-align value becomes invalid (because we have set text-align to the value of --p and --p is now 1, which is not a valid value for text-align), so the text reverts to being aligned to the left.

p { /* same code as before */ --i: var(--p, 0); text-align: var(--p, right); &:nth-child(2n) { --p: 1 }
}

This gives us the result we’ve been after:

See the Pen by thebabydino (@thebabydino) on CodePen.

Handling responsiveness

While our cards example looks great on wider screens, the same can’t be said when shrink things down.

Screenshot collage. Since the width of the cards depends on the viewport width, the viewport may get too narrow to allow for displaying the numbering and the paragraph text side by side and the right one of the two overflows in this case.
The wide screen result (left) vs. the narrow screen result (right)

In order to fix this, we introduce two more custom properties, --wide and --k to switch between the wide and narrow cases. We set --k to --wide with a fallback value of 0 in the general case and then set --wide to 1 if the viewport width is anything 340px and up.

p { /* same code as before */ --k: var(--wide, 0); @media (min-width: 340px) { --wide: 1 }
}

Since we only want our items to be transformed and have rounded corners in the wide case, we multiply the translation, rotation and radius values by --k (which is 0, unless the viewport is wide, which switches its value to 1).

p { /* same code as before */ --k: var(--wide, 0); --r0: calc(var(--k)*var(--j)*#{$r}); --r1: calc(var(--k)*var(--i)*#{$r}); border-radius: var(--r0) /* top left */ var(--r1) /* top right */ var(--r1) /* bottom right */ var(--r0) /* bottom left */; transform: translate(calc(var(--k)*var(--s)*10%)) rotate(calc(var(--k)*var(--s)*5deg)); @media (min-width: 340px) { --wide: 1 }
}

This is slightly better, but our content still overflows in narrow viewports. We can fix this by only placing the numbering (the :before pseudo-element) on the left or right side only in the wide case then moving it above the card in the narrow case.

In order to do this, we multiply both its order and its lateral margin values by --k (which is 1 in the wide case and 0 otherwise).

We also set flex-direction to --wide with a fallback value of column.

This means the flex-direction value is column in the general case (since we haven’t set --wide explicitly elsewhere). However, if the viewport is wide (min-width: 340px), then our --wide variable gets set to 1. But 1 is an invalid value for flex-direction, so this property reverts back to its initial value of row.

p { /* same code as before */ --k: var(--wide, 0); flex-direction: var(--wide, column); &:before { /* same code as before */ order: calc(var(--k)*var(--i)); margin: 0 /* top */ calc(var(--k)*var(--j)*#{$gap}) /* right */ 0 /* bottom */ calc(var(--k)*var(--i)*#{$gap}) /* left */; } @media (min-width: 340px) { --wide: 1 }
}

Coupled with setting a min-width of 160px on the body, we’ve now eliminated the overflow issue:

Responsive cards, no overflow (live demo).

One more thing we can do is tweak the font-size so that it also depends on --k:

p { /* same code as before */ --k: var(--wide, 0); font: 900 calc(var(--k)*.5em + .75em) cursive; @media (min-width: 340px) { --wide: 1 }
}

And that’s it, our demo is now nicely responsive!

Responsive cards, font smaller for narrow screens and with no overflow (live demo).

A few more quick examples!

Let’s look at a few more demos that use the same technique, but quickly without building them from scratch. We’ll merely go through the basic ideas behind them.

Disc slices

Sliced disc (live demo).

Just like the cards example we completed together, we can use a :before pseudo-element for the numbering and a flex layout on the paragraphs. The sliced disc effect is achieved using clip-path.

The paragraph elements themselves — the horizontal offsets, the position and intensity of the radial-gradient() creating the shadow effect, the direction of the linear-gradient() and the saturation of its stops, the color and the text alignment — all depend on the --parity variable.

p { /* other styles not relevant here */ --p: var(--parity, 1); --q: calc(1 - var(--p)); --s: calc(1 - 2*var(--p)); /* sign depending on parity */ transform: translate((calc(var(--i)*var(--s)*#{-$x}))); background: radial-gradient(at calc(var(--q)*100%) 0, rgba(0, 0, 0, calc(.5 + var(--p)*.5)), transparent 63%) calc(var(--q)*100%) 0/ 65% 65% no-repeat, linear-gradient(calc(var(--s)*-90deg), hsl(23, calc(var(--q)*98%), calc(27% + var(--q)*20%)), hsl(44, calc(var(--q)*92%), 52%)); color: HSL(0, 0%, calc(var(--p)*100%)); text-align: var(--parity, right); &:nth-child(odd) { --parity: 0 }
}

For the numbering (the :before pseudo-elements of the paragraphs), we have that both the margin and the order depend on the --parity in the exact same way as the cards example.

If the viewport width is smaller than the disc diameter $d plus twice the horizontal slice offset in absolute value $x, then we’re not in the --wide case anymore. This affects the width, padding and margin of our paragraphs, as well as their horizontal offset and their shape (because we don’t clip them to get the sliced disc effect at that point).

body { /* other styles not relevant here */ --i: var(--wide, 1); --j: calc(1 - var(--i)); @media (max-width: $d + 2*$x) { --wide: 0 }
} p { /* other styles not relevant here */ margin: calc(var(--j)*.25em) 0; padding: calc(var(--i)*#{.5*$r}/var(--n) + var(--j)*5vw) /* vertical */ calc(var(--i)*#{.5*$r} + var(--j)*2vw) /* horizontal */; width: calc(var(--i)*#{$d} /* wide */ + var(--j)*100% /* not wide */); transform: translate((calc(var(--i)*var(--s)*#{-$x}))); clip-path: var(--wide, /* fallback, used in the wide case only */ circle($r at 50% calc((.5*var(--n) - var(--idx))*#{$d}/var(--n))));
}

We’re in the narrow case below 270px and have a flex-direction of column on our paragraphs. We also zero out both the lateral margins and the order for the numbering.

body { /* other styles not relevant here */ --k: calc(1 - var(--narr, 1)); @media (min-width: 270px) { --narr: 0 }
} p { /* other styles not relevant here */ flex-direction: var(--narr, column); &:before { /* other styles not relevant here */ margin: 0 /* top */ calc(var(--k)*var(--q)*.25em) /* right */ 0 /* bottom */ calc(var(--k)*var(--p)*.25em) /* left */; order: calc(var(--k)*var(--p)); }
}

Four-step infographic

Screenshot collage. On the left, there's the wide screen scenario. In the middle, there's the normal screen scenario. On the right, there's the narrow screen scenario.
A four-step infographic (live demo).

This works pretty much the same as the previous two examples. We have a flex layout on our paragraphs using a column direction in the narrow case. We also have a smaller font-size in that same case:

body { /* other styles not relevant here */ --k: var(--narr, 1); @media (min-width: 400px) { --narr: 0 }
} p { /* other styles not relevant here */ flex-direction: var(--narr, column); font-size: calc((1.25 - .375*var(--k))*1em);
}

The parity determines each paragraph’s text alignment, which lateral border gets a non-zero value, and the position and direction of the border gradient. Both the parity and whether we’re in the wide screen case or not determine the lateral margins and paddings.

body { /* other styles not relevant here */ --i: var(--wide, 1); --j: calc(1 - var(--i)); @media (max-width: $bar-w + .5*$bar-h) { --wide: 0 }
} p { /* other styles not relevant here */ margin: .5em /* top */ calc(var(--i)*var(--p)*#{.5*$bar-h}) /* right */ 0 /* bottom */ calc(var(--i)*var(--q)*#{.5*$bar-h}) /* left */; border-width: 0 /* top */ calc(var(--q)*#{$bar-b}) /* right */ 0 /* bottom */ calc(var(--p)*#{$bar-b}) /* left */; padding: $bar-p /* top */ calc((var(--j) + var(--i)*var(--q))*#{$bar-p}) /* right */ $bar-p /* bottom */ calc((var(--j) + var(--i)*var(--p))*#{$bar-p}) /* left */; background: linear-gradient(#fcfcfc, gainsboro) padding-box, linear-gradient(calc(var(--s)*90deg), var(--c0), var(--c1)) calc(var(--q)*100%) /* background-position */ / #{$bar-b} 100% /* background-size */; text-align: var(--parity, right);
}

The icon is created using the :before pseudo-element, and its order depends on the parity, but only if we’re not in the narrow screen scenario — in which case it’s always before the actual text content of the paragraph. Its lateral margin depends both on the parity and whether we are in the wide screen case or not. The big-valued component that positions it half out of its parent paragraph is only present in the wide screen case. The font-size also depends on whether we’re in the narrow screen case or not (and this influences its em dimensions and padding).

order: calc((1 - var(--k))*var(--p));
margin: 0 /* top */ calc(var(--i)*var(--p)*#{-.5*$ico-d} + var(--q)*#{$bar-p}) /* right */ 0 /* bottom */ calc(var(--i)*var(--q)*#{-.5*$ico-d} + var(--p)*#{$bar-p}) /* left */;
font-size: calc(#{$ico-s}/(1 + var(--k)));

The ring is created using an absolutely positioned :after pseudo-element (and its placement depends on parity), but only for the wide screen case.

content: var(--wide, '');

The two-dimension case

Screenshot collage. On the left, we have the wide screen scenario. Each article is laid out as a 2x2 grid, with the numbering occupying an entire column, either on the right for odd items or on the left for even items. The heading and the actual text occupy the other column. In the middle, we have the normal screen case. Here, we also have a 2x2 grid, but the numbering occupies only the top row on the same column as before, while the actual text content now spans both columns on the second row. On the right, we have the narrow screen case. In this case, we don't have a grid anymore, the numbering, the heading and the actual text are one under the other for each article.
Screenshot collage (live demo, no Edge support due to CSS variable and calc() bugs).

Here we have a bunch of article elements, each containing a heading. Let’s check out the most interesting aspects of how this responsive layout works!

On each article, we have a two-dimensional layout (grid) — but only if we’re not in the narrow screen scenario (--narr: 1), in which case we fall back on the normal document flow with the numbering created using a :before pseudo-element, followed by the heading, followed by the actual text. In this situation, we also add vertical padding on the heading since we don’t have the grid gaps anymore and we don’t want things to get too crammed.

html { --k: var(--narr, 0); @media (max-width: 250px) { --narr: 1 }
} article { /* other styles irrelevant here */ display: var(--narr, grid);
} h3 { /* other styles irrelevant here */ padding: calc(var(--k)*#{$hd3-p-narr}) 0;
}

For the grid, we create two columns of widths depending both on parity and on whether we’re in the wide screen scenario. We make the numbering (the :before pseudo-element) span two rows in the wide screen case, either on the second column or the first, depending on the parity. If we’re not in the wide screen case, then the paragraph spans both columns on the second row.

We set the grid-auto-flow to column dense in the wide screen scenario, letting it revert to the initial value of row otherwise. Since our article elements are wider than the combined widths of the columns and the column gap between them, we use place-content to position the actual grid columns inside at the right or left end depending on parity.

Finally, we place the heading at the end or start of the column, depending on parity, and we as well as the paragraph’s text alignment if we’re in the wide screen scenario.

$col-1-wide: calc(var(--q)*#{$col-a-wide} + var(--p)*#{$col-b-wide});
$col-2-wide: calc(var(--p)*#{$col-a-wide} + var(--q)*#{$col-b-wide}); $col-1-norm: calc(var(--q)*#{$col-a-norm} + var(--p)*#{$col-b-norm});
$col-2-norm: calc(var(--p)*#{$col-a-norm} + var(--q)*#{$col-b-norm}); $col-1: calc(var(--i)*#{$col-1-wide} + var(--j)*#{$col-1-norm});
$col-2: calc(var(--i)*#{$col-2-wide} + var(--j)*#{$col-2-norm}); html { --i: var(--wide, 1); --j: calc(1 - var(--i)); @media (max-width: $art-w-wide) { --wide: 0 }
} article { /* other styles irrelevant here */ --p: var(--parity, 1); --q: calc(1 - var(--p)); grid-template-columns: #{$col-1} #{$col-2}; grid-auto-flow: var(--wide, dense column); place-content: var(--parity, center end); &:before { /* other styles irrelevant here */ grid-row: 1/ span calc(1 + var(--i)); grid-column: calc(1 + var(--p))/ span 1; } &:nth-child(odd) { --parity: 0 }
} h3 { /* other styles irrelevant here */ justify-self: var(--parity, self-end);
} p { grid-column-end: span calc(1 + var(--j)); text-align: var(--wide, var(--parity, right));
}

We also have numerical values such as grid gaps, border radii, paddings, font-sizes, gradient directions, rotation and translation directions depending on the parity and/or whether we’re in the wide screen scenario or not.

Even more examples!

If you want more of this, I’ve created an entire collection of similar responsive demos for you to enjoy!

Screenshot of collection page on CodePen, showing the six most recent demos added.
Collection of responsive demos.

The post DRY State Switching With CSS Variables: Fallbacks and Invalid Values appeared first on CSS-Tricks.

DRY Switching with CSS Variables: The Difference of One Declaration

This is the first post of a two-part series that looks into the way CSS variables can be used to make the code for complex layouts and interactions less difficult to write and a lot easier to maintain. This first installment walks through various use cases where this technique applies. The second post covers the use of fallbacks and invalid values to extend the technique to non-numeric values.

What if I told you a single CSS declaration makes the difference in the following image between the wide screen case (left) and the second one (right)? And what if I told you a single CSS declaration makes the difference between the odd and even items in the wide screen case?

On the left, a screenshot of the wide screen scenario. Each item is limited in width and its components are arranged on a 2D 2x2 grid, with the first level heading occupying an entire column, either the one on the right (for odd items) or the one on the left (for even items). The second level heading and the actual text occupy the other column. The shape of the first level heading also varies depending on the parity — it has the top left and the bottom right corners rounded for the odd items and the other two corners rounded for the even items. On the right, a screenshot of the narrower scenario. Each item spans the full viewport width and its components are placed vertically, one under another — first level heading, second level heading below and, finally, the actual text.
Screenshot collage.

Or that a single CSS declaration makes the difference between the collapsed and expanded cases below?

Animated gif. Shows a green button with a magnifier icon. Clicking this button makes it slide right and its background to turn red while a text search field slides out of it to the left and the magnifier morphs into a close (crossmark) icon.
Expanding search.

How is that even possible?

Well, as you may have guessed from the title, it’s all in the power of CSS variables.

There are already plenty of articles out there on what CSS variables are and how to get started with them, so we won’t be getting into that here.

Instead, we’ll dive straight into why CSS variables are useful for achieving these cases and others, then we’ll move on to a detailed explanation of the how for various cases. We’ll code an actual example from scratch, step by step, and, finally, you’ll be getting some eye candy in the form of a few more demos that use the same technique.

So let’s get started!

Why CSS variables are useful

For me, the best thing about CSS variables is that they’ve opened the door for styling things in a logical, mathematical and effortless way.

One example of this is the CSS variable version of the yin and yang loader I coded last year. For this version, we create the two halves with the two pseudo-elements of the loader element.

Animated gif. The yin and yang symbol is rotating while its two lobes alternate increasing and decreasing in size - whenever one is increasing, it squishes the other one down.
Rotating ☯ symbol, with its two lobes increasing and decreasing in size.

We use the same background, border-color, transform-origin and animation-delay values for the two halves. These values all depend on a switch variable --i that’s initially set to 0 on both halves (the pseudo-elements), but then we change it to 1 for the second half (the :after pseudo-element), thus dynamically modifying the computed values of all these properties.

Without CSS variables, we’d have to set all these properties (border-color, transform-origin, background, animation-delay) again on the :after pseudo-element and risk making some typo or even forgetting to set some of them.

How switching works in the general case

Switching between a zero and a non-zero value

In the particular case of the yin and yang loader, all the properties we change between the two halves (pseudo-elements) go from a zero value for one state of the switch and a non-zero value for the other state.

If we want our value to be zero when the switch is off (--i: 0) and non-zero when the switch is on (--i: 1), then we multiply it with the switch value (var(--i)). This way, if our non-zero value should be, let’s say an angular value of 30deg, we have:

  • when the switch is off (--i: 0), calc(var(--i)*30deg) computes to 0*30deg = 0deg
  • when the switch is on (--i: 1), calc(var(--i)*30deg) computes to 1*30deg = 30deg

However, if we want our value to be non-zero when the switch is off (--i: 0) and zero when the switch is on (--i: 1), then we multiply it with the complementary of the switch value (1 - var(--i)). This way, for the same non-zero angular value of 30deg, we have:

  • when the switch is off (--i: 0), calc((1 - var(--i))*30deg) computes to (1 - 0)*30deg = 1*30deg = 30deg
  • when the switch is on (--i: 1), calc((1 - var(--i))*30deg) computes to (1 - 1)*30deg = 0*30deg = 0deg

You can see this concept illustrated below:

Animated gif. Shows how changing the switch value from 0 to 1 changes the rotation of two boxes. The first box is rotated to 30deg when the switch is off (its value is 0) and not rotated or rotated to 0deg when the switch is on (its value is 1). This means we have a rotation value of calc((1 - var(--i))*30deg), where --i is the switch value. The second box is not rotated or rotated to 0deg when the switch is off (its value is 0) and rotated to 30deg when the switch is on (its value is 1). This means we have a rotation value of calc(var(--i)*30deg), with --i being the switch value.
Switching between a zero and a non-zero value (live demo, no Edge support due to calc() not working for angle values)

For the particular case of the loader, we use HSL values for border-color and background-color. HSL stands for hue, saturation, lightness and can be best represented visually with the help of a bicone (which is made up of two cones with the bases glued together).

Two cones with their bases glued together in the middle, one vertex pointing down and one up. The hue is cyclic, distributed around the central (vertical) axis of the bicone. The saturation axis goes horizontally from the central axis towards the surface of the bicone - it's 0% right on the axis and 100% right on the surface. The lightness axis goes vertically from the black vertex to the white one - it's 0% at the black vertex and 100% at the white vertex.
HSL bicone.

The hues go around the bicone, being equivalent to 360° to give us a red in both cases.

Shows the red being at 0° (which is equivalent to 360° since the hue is cyclic), the yellow at 60°, the lime at 120°, the cyan at 180°, the blue at 240° and the magenta at 300°.
Hue wheel.

The saturation goes from 0% on the vertical axis of the bicone to 100% on the bicone surface. When the saturation is 0% (on the vertical axis of the bicone), the hue doesn’t matter anymore; we get the exact same grey for all hues in the same horizontal plane.

The “same horizontal plane” means having the same lightness, which increases along the vertical bicone axis, going from 0% at the black bicone vertex to 100% at the white bicone vertex. When the lightness is either 0% or 100%, neither the hue nor the saturation matter anymore – we always get black for a lightness value of 0% and white for a lightness value of 100%.

Since we only need black and white for our ☯ symbol, the hue and saturation are irrelevant, so we zero them and then switch between black and white by switching the lightness between 0% and 100%.

.yin-yang { /* other styles that are irrelevant here */ &:before, &:after { /* other styles that are irrelevant here */ --i: 0; /* lightness of border-color when * --i: 0 is (1 - 0)*100% = 1*100% = 100% (white) * --i: 1 is (1 - 1)*100% = 0*100% = 0% (black) */ border: solid $d/6 hsl(0, 0%, calc((1 - var(--i))*100%)); /* x coordinate of transform-origin when * --i: 0 is 0*100% = 0% (left) * --i: 1 is 1*100% = 100% (right) */ transform-origin: calc(var(--i)*100%) 50%; /* lightness of background-color when * --i: 0 is 0*100% = 0% (black) * --i: 1 is 1*100% = 100% (white) */ background: hsl(0, 0%, calc(var(--i)*100%)); /* animation-delay when * --i: 0 is 0*-$t = 0s * --i: 1 is 1*-$t = -$t */ animation: s $t ease-in-out calc(var(--i)*#{-$t}) infinite alternate; } &:after { --i: 1 }
}

Note that this approach doesn’t work in Edge due to the fact that Edge doesn’t support calc() values for animation-delay.

But what if we want to have a non-zero value when the switch is off (--i: 0) and another different non-zero value when the switch is on (--i: 1)?

Switching between two non-zero values

Let’s say we want an element to have a grey background (#ccc) when the switch is off (--i: 0) and an orange background (#f90) when the switch is on (--i: 1).

The first thing we do is switch from hex to a more manageable format such as rgb() or hsl().

We could do this manually either by using a tool such as Lea Verou’s CSS Colors or via DevTools. If we have a background set on an element we can cycle through formats by keeping the Shift key pressed while clicking on the square (or circle) in front of the value in DevTools. This works in both Chrome and Firefox, though it doesn’t appear to work in Edge.

Animated gif. Shows how to cycle through formats (hex/ RGB/ HSL) via DevTools. In both Chrome and Firefox, we do this by keeping the Shift key pressed and clicking the square or circle in front of the <color> value.”/><figcaption>Changing the format from DevTools.</figcaption></figure>
<p>Even better, if we’re using Sass, we can extract the components with <a href=red()/ green()/ blue() or hue()/ saturation()/ lightness() functions.

While rgb() may be the better known format, I tend to prefer hsl() because I find it more intuitive and it’s easier for me to get an idea about what to expect visually just by looking at the code.

So we extract the three components of the hsl() equivalents of our two values ($c0: #ccc when the switch is off and $c1: #f90 when the switch is on) using these functions:

$c0: #ccc;
$c1: #f90; $h0: round(hue($c0)/1deg);
$s0: round(saturation($c0));
$l0: round(lightness($c0)); $h1: round(hue($c1)/1deg);
$s1: round(saturation($c1));
$l1: round(lightness($c1))

Note that we’ve rounded the results of the hue(), saturation() and lightness() functions as they may return a lot of decimals and we want to keep our generated code clean. We’ve also divided the result of the hue() function by 1deg, as the returned value is a degree value in this case and Edge only supports unit-less values inside the CSS hsl() function. Normally, when using Sass, we can have degree values, not just unit-less ones for the hue inside the hsl() function because Sass treats it as the Sass hsl() function, which gets compiled into a CSS hsl() function with a unit-less hue. But here, we have a dynamic CSS variable inside, so Sass treats this function as the CSS hsl() function that doesn’t get compiled into anything else, so, if the hue has a unit, this doesn’t get removed from the generated CSS.

Now we have that:

  • if the switch is off (--i: 0), our background is
    hsl($h0, $s0, $l0)
  • if the switch is on (--i: 1), our background is
    hsl($h1, $s1, $l1)

We can write our two backgrounds as:

  • if the switch is off (--i: 0),
    hsl(1*$h0 + 0*$h1, 1*$s0 + 0*$s1, 1*$l0 + 1*$l1)
  • if the switch is on (--i: 1),
    hsl(0*$h0 + 1*$h1, 0*$s0 + 1*$s1, 0*$l0 + 1*$l1)

Using the switch variable --i, we can unify the two cases:

--j: calc(1 - var(--i));
background: hsl(calc(var(--j)*#{$h0} + var(--i)*#{$h1}), calc(var(--j)*#{$s0} + var(--i)*#{$s1}), calc(var(--j)*#{$l0} + var(--i)*#{$l1}))

Here, we’ve denoted by --j the complementary value of --i (when --i is 0, --j is 1 and when --i is 1, --j is 0).

Animated gif. Shows how changing the switch value from 0 to 1 changes the background of a box. The background is grey (of hue $h0, saturation $s0 and lightness $l0) when the switch is turned off (its value is zero) and orange (of hue $h1, saturation $s1 and lightness $l1) when the switch is turned on (its value is 1). This means we have a hue value of calc(var(--j)*#{$h0} + var(--i)*#{$h1}), a saturation value of calc(var(--j)*#{$s0} + var(--i)*#{$s1}) and a lightness value of calc(var(--j)*#{$l0} + var(--i)*#{$l1})), where --i is the switch variable.
Switching between two backgrounds (live demo)

The formula above works for switching in between any two HSL values. However, in this particular case, we can simplify it because we have a pure grey when the switch is off (--i: 0).

Purely grey values have equal red, green and blue values when taking into account the RGB model.

When taking into account the HSL model, the hue is irrelevant (our grey looks the same for all hues), the saturation is always 0% and only the lightness matters, determining how light or dark our grey is.

In this situation, we can always keep the hue of the non-grey value (the one we have for the “on” case, $h1).

Since the saturation of any grey value (the one we have for the “off” case, $s0) is always 0%, multiplying it with either 0 or 1 always gives us 0%. So, given the var(--j)*#{$s0} term in our formula is always 0%, we can just ditch it and our saturation formula reduces to the product between the saturation of the “on” case $s1 and the switch variable --i.

This leaves the lightness as the only component where we still need to apply the full formula.

--j: calc(1 - var(--i));
background: hsl($h1, calc(var(--i)*#{$s1}), calc(var(--j)*#{$l0} + var(--i)*#{d1l}))

The above can be tested in this demo.

Similarly, let’s say we want the font-size of some text to be 2rem when our switch is off (--i: 0) and 10vw when the switch is on (--i: 1). Applying the same method, we have:

font-size: calc((1 - var(--i))*2rem + var(--i)*10vw)
Animated gif. Shows how changing the switch value from 0 to 1 changes the font-size.
Switching between two font sizes (live demo)

Alright, let’s now move on to clearing another aspect of this: what is it exactly that causes the switch to flip from on to off or the other way around?

What triggers switching

We have a few options here.

Element-based switching

This means the switch is off for certain elements and on for other elements. For example, this can be determined by parity. Let’s say we want all the even elements to be rotated and have an orange background instead of the initial grey one.

.box { --i: 0; --j: calc(1 - var(--i)); transform: rotate(calc(var(--i)*30deg)); background: hsl($h1, calc(var(--i)*#{$s1}), calc(var(--j)*#{$l0} + var(--i)*#{$l1})); &:nth-child(2n) { --i: 1 }
}
Screenshot. Shows a bunch of squares in a row, the even ones being rotated and having an orange background instead of the initial grey one. This is achieved by making both the transform and the background properties depend on a switch variable --i that changes with parity: it's 0 initially, but then we change it to 1 for even items.
Switching triggered by item parity (live demo, not fully functional in Edge due to calc() not working for angle values)

In the parity case, we flip the switch on for every second item (:nth-child(2n)), but we can also flip it on for every seventh item (:nth-child(7n)), for the first two items (:nth-child(-n + 2)), for all items except the first and last two (:nth-child(n + 3):nth-last-child(n + 3)). We can also flip it on just for headings or just for elements that have a certain attribute.

State-based switching

This means the switch is off when the element itself (or a parent or one of its previous siblings) is in one state and off when it’s another state. In the interactive examples from the previous section, the switch was flipped when a checkbox before our element got checked or unchecked.

We can also have something like a white link that scales up and turns orange when focused or hovered:

$c: #f90; $h: round(hue($c)/1deg);
$s: round(saturation($c));
$l: round(lightness($c)); a { --i: 0; transform: scale(calc(1 + var(--i)*.25)); color: hsl($h, $s, calc(var(--i)*#{$l} + (1 - var(--i))*100%)); &:focus, &:hover { --i: 1 }
}

Since white is any hsl() value with a lightness of 100% (the hue and saturation are irrelevant), we can simplify things by always keeping the hue and saturation of the :focus/ :hover state and only changing the lightness.

Animated gif. Shows a white link that grows and turns orange when hovered or focused.
Switching triggered by state change (live demo, not fully functional in Edge due to calc() values not being supported inside scale() functions)

Media query-based switching

Another possibility is that switching is triggered by a media query, for example, when the orientation changes or when going from one viewport range to another.

Let’s say we have a white heading with a font-size of 1rem up to 320px, but then it turns orange ($c) and the font-size becomes 5vw and starts scaling with the viewport width.

h5 { --i: 0; color: hsl($h, $s, calc(var(--i)*#{$l} + (1 - var(--i))*100%)); font-size: calc(var(--i)*5vw + (1 - var(--i))*1rem); @media (min-width: 320px) { --i: 1 }
}
Animated gif. Shows a heading that's white and has a fixed font-size up to 320px, but as we resize the viewport above that, it becomes orange and its font-size starts scaling with the viewport width.
Switching triggered by viewport change (live demo)

Coding a more complex example from scratch

The example we dissect here is that of the expanding search shown at the beginning of this article, inspired by this Pen, which you should really check out because the code is pretty damn clever.

Animated gif. Shows a green button with a magnifier icon. Clicking this button makes it slide right and its background to turn red while a text search field slides out of it to the left and the magnifier morphs into a close (crossmark) icon.
Expanding search.

Note that from a usability point of view, having such a search box on a website may not be the best idea as one would normally expect the button following the search box to trigger the search, not close the search bar, but it’s still an interesting coding exercise, which is why I’ve chosen to dissect it here.

To begin with, my idea was to do it using only form elements. So, the HTML structure looks like this:

<input id='search-btn' type='checkbox'/>
<label for='search-btn'>Show search bar</label>
<input id='search-bar' type='text' placeholder='Search...'/>

What we do here is initially hide the text input and then reveal it when the checkbox before it gets checked — let’s dive into how that works!

First off, we use a basic reset and set a flex layout on the container of our input and label elements. In our case, this container is the body, but it could be another element as well. We also absolutely position the checkbox and move it out of sight (outside the viewport).

*, :before, :after { box-sizing: border-box; margin: 0; padding: 0; font: inherit
} html { overflow-x: hidden } body { display: flex; align-items: center; justify-content: center; margin: 0 auto; min-width: 400px; min-height: 100vh; background: #252525
} [id='search-btn'] { position: absolute; left: -100vh
}

So far, so good…

See the Pen by thebabydino (@thebabydino) on CodePen.

So what? We have to admit it’s not exciting at all, so let’s move on to the next step!

We turn the checkbox label into a big round green button and move its text content out of sight using a big negative-valued text-indent and overflow: hidden.

$btn-d: 5em; /* same as before */ [for='search-btn'] { overflow: hidden; width: $btn-d; height: $btn-d; border-radius: 50%; box-shadow: 0 0 1.5em rgba(#000, .4); background: #d9eb52; text-indent: -100vw; cursor: pointer;
}

See the Pen by thebabydino (@thebabydino) on CodePen.

Next, we polish the actual search bar by:

  • giving it explicit dimensions
  • providing a background for its normal state
  • defining a different background and a glow for its focused state
  • rounding the corners on the left side using a border-radius that equals half its height
  • Cleaning up the placeholder a bit
$btn-d: 5em;
$bar-w: 4*$btn-d;
$bar-h: .65*$btn-d;
$bar-r: .5*$bar-h;
$bar-c: #ffeacc; /* same as before */ [id='search-bar'] { border: none; padding: 0 1em; width: $bar-w; height: $bar-h; border-radius: $bar-r 0 0 $bar-r; background: #3f324d; color: #fff; font: 1em century gothic, verdana, arial, sans-serif; &::placeholder { opacity: .5; color: inherit; font-size: .875em; letter-spacing: 1px; text-shadow: 0 0 1px, 0 0 2px } &:focus { outline: none; box-shadow: 0 0 1.5em $bar-c, 0 1.25em 1.5em rgba(#000, .2); background: $bar-c; color: #000; }
}

See the Pen by thebabydino (@thebabydino) on CodePen.

At this point, the right edge of the search bar coincides with the left edge of the button. However, we want a bit of overlap — let’s say an overlap such that the right edge of the search bar coincides with the button’s vertical midline. Given that we have a flexbox layout with align-items: center on the container (the body in our case), the assembly made up of our two items (the bar and the button) remains middle-aligned horizontally even if we set a margin on one or on the other or on both in between those items. (On the left of the leftmost item or on the right of the rightmost item is a different story, but we won’t be getting into that now.)

Illustration showing the bar plus button assembly in the initial state (bar's right edge coinciding with the button's left edge) vs. the overlap state (the bar's right edge coincides with the button's vertical midline). In both cases, the assembly is middle aligned.
Creating overlap, keeping alignment (live demo).

That’s an overlap of .5*$btn-d minus half a button diameter, which is equivalent to the button’s radius. We set this as a negative margin-right on the bar. We also adjust the padding on the right of the bar so that we compensate for the overlap:

$btn-d: 5em;
$btn-r: .5*$btn-d; /* same as before */ [id='search-bar'] { /* same as before */ margin-right: -$btn-r; padding: 0 calc(#{$btn-r} + 1em) 0 1em;
}

We now have the bar and the button in the positions for the expanded state:

See the Pen by thebabydino (@thebabydino) on CodePen.

Except the bar follows the button in DOM order, so it’s placed on top of it, when we actually want the button on top. Fortunately, this has an easy fix (at least for now — it won’t be enough later, but let’s deal with one issue at a time).

[for='search-btn'] { /* same as before */ position: relative;
}

Now that we’ve given the button a non-static position value, it’s on top of the bar:

See the Pen by thebabydino (@thebabydino) on CodePen.

In this state, the total width of the bar and button assembly is the bar width $bar-w plus the button’s radius $btn-r (which is half the button diameter $btn-d) because we have an overlap for half the button. In the collapsed state, the total width of the assembly is just the button diameter $btn-d.

Illustration showing the bar plus button assembly in the expanded state (the bar's right edge coincides with the button's vertical midline) and in the collapsed state (the bar is collapsed and the assembly is reduced to just the button). In both cases, the assembly is middle aligned.
Expanded vs. collapsed state (live).

Since we want to keep the same central axis when going from the expanded to the collapsed state, we need to shift the button to the left by half the assembly width in the expanded state (.5*($bar-w + $btn-r)) minus the button’s radius ($btn-r).

We call this shift $x and we use it with minus on the button (since we shift the button to the left and left is the negative direction of the x axis). Since we want the bar to collapse into the button, we set the same shift $x on it, but in the positive direction (as we shift the bar to the right of the x axis).

We’re in the collapsed state when the checkbox isn’t checked and in the expanded state when it isn’t. This means our bar and button are shifted with a CSS transform when the checkbox isn’t checked and in the position we currently have them in (no transform) when the checkbox is checked.

In order to do this, we set a variable --i on the elements following our checkbox — the button (created with the label for the checkbox) and the search bar. This variable is 0 in the collapsed state (when both elements are shifted and the checkbox isn’t checked) and 1 in the expanded state (when our bar and button are in the positions they currently occupy, no shift, and the checkbox is checked).

$x: .5*($bar-w + $btn-r) - $btn-r; [id='search-btn'] { position: absolute; left: -100vw; ~ * { --i: 0; --j: calc(1 - var(--i)) /* 1 when --i is 0, 0 when --i is 1 */ } &:checked ~ * { --i: 1 }
} [for='search-btn'] { /* same as before */ /* if --i is 0, --j is 1 => our translation amount is -$x * if --i is 1, --j is 0 => our translation amount is 0 */ transform: translate(calc(var(--j)*#{-$x}));
} [id='search-bar'] { /* same as before */ /* if --i is 0, --j is 1 => our translation amount is $x * if --i is 1, --j is 0 => our translation amount is 0 */ transform: translate(calc(var(--j)*#{$x}));
}

And we now have something interactive! Clicking the button toggles the checkbox state (because the button has been created using the label of the checkbox).

See the Pen by thebabydino (@thebabydino) on CodePen.

Except now the button is a bit difficult to click since it’s under the text input again (because we’ve set a transform on the bar and this establishes a stacking context). The fix is pretty straightforward — we need to add a z-index to the button and this moves it above the bar.

[for='search-btn'] { /* same as before */ z-index: 1;
}

See the Pen by thebabydino (@thebabydino) on CodePen.

But we still have another bigger problem: we can see the bar coming out from under the button on the right side. In order to fix this, we set clip-path with an inset() value on the bar. This specifies a clipping rectangle with the help of the distances from the top, right, bottom and left edges of the element’s border-box. Everything outside this clipping rectangle gets cut out and only what’s inside is displayed.

Illustration showing what the four values of the inset() function represent. The first one is the offset of the top edge of the clipping rectangle with respect to the top edge of the border-box. The second one is the offset of the right edge of the clipping rectangle with respect to the right edge of the border-box. The third one is the offset of the bottom edge of the clipping rectangle with respect to the bottom edge of the border-box. The fourth one is the offset of the left edge of the clipping rectangle with respect to the left edge of the border-box. Everything outside the
How the inset() function works (live).

In the illustration above, each distance is going inward from the edges of the border-box. In this case, they’re positive. But they can also go outwards, in which case they’re negative and the corresponding edges of the clipping rectangle are outside the element’s border-box.

At first, you may think we’d have no reason to ever do that, but in our particular case, we do!

We want the distances from the top (dt), bottom (db) and left (dl) to be negative and big enough to contain the box-shadow that extends outside the element’s border-box in the :focus state as we don’t want it to get clipped out. So the solution is to create a clipping rectangle with edges outside the element’s border-box in these three directions.

The distance from the right (dr) is the full bar width $bar-w minus a button radius $btn-r in the collapsed case (checkbox not checked, --i: 0) and 0 in the expanded case (checkbox checked, --i: 1).

$out-d: -3em; [id='search-bar'] { /* same as before */ clip-path: inset($out-d calc(var(--j)*#{$bar-w - $btn-r}) $out-d $out-d);
}

We now have a search bar and button assembly that expands and collapses on clicking the button.

See the Pen by thebabydino (@thebabydino) on CodePen.

Since we don’t want an abrupt change in between the two states, we use a transition:

[id='search-btn'] { /* same as before */ ~ * { /* same as before */ transition: .65s; }
}

We also want our button’s background to be green in the collapsed case (checkbox not checked, --i: 0) and pink in the expanded case (checkbox checked, --i: 1). For this, we use the same technique as before:

[for='search-btn'] { /* same as before */ $c0: #d9eb52; // green for collapsed state $c1: #dd1d6a; // pink for expanded state $h0: round(hue($c0)/1deg); $s0: round(saturation($c0)); $l0: round(lightness($c0)); $h1: round(hue($c1)/1deg); $s1: round(saturation($c1)); $l1: round(lightness($c1)); background: hsl(calc(var(--j)*#{$h0} + var(--i)*#{$h1}), calc(var(--j)*#{$s0} + var(--i)*#{$s1}), calc(var(--j)*#{$l0} + var(--i)*#{$l1}));
}

Now we’re getting somewhere!

See the Pen by thebabydino (@thebabydino) on CodePen.

What we still need to do is create the icon that morphs between a magnifier in the collapsed state and an “x” in the expanded state to indicate a closing action. We do this with the :before and :after pseudo-elements. We begin by deciding on a diameter for the magnifier and how much of this diameter the width of the icon lines represent.

$ico-d: .5*$bar-h;
$ico-f: .125;
$ico-w: $ico-f*$ico-d;

We absolutely position both pseudo-elements in the middle of the button taking their dimensions into account. We then make them inherit their parent’s transition. We give the :before a background, as this will be the handle of our magnifier, make the :after round with border-radius and give it an inset box-shadow.

[for='search-btn'] { /* same as before */ &:before, &:after { position: absolute; top: 50%; left: 50%; margin: -.5*$ico-d; width: $ico-d; height: $ico-d; transition: inherit; content: '' } &:before { margin-top: -.4*$ico-w; height: $ico-w; background: currentColor } &:after { border-radius: 50%; box-shadow: 0 0 0 $ico-w currentColor } }

We can now see the magnifier components on the button:

See the Pen by thebabydino (@thebabydino) on CodePen.

In order to make our icon to look more like a magnifier, we translate both of its components outwards by a quarter of the magnifier’s diameter. This means translating the handle to the right, in the positive direction of the x axis by .25*$ico-d and the main part to the left, in the negative direction of the x axis by the same .25*$ico-d.

We also scale the handle (the :before pseudo-element) horizontally to half its width with respect to its right edge (which means a transform-origin of 100% along the x axis).

We only want this to happen in the collapsed state (checkbox not checked, --i is 0 and, consequently --j is 1), so we multiply the translation amounts by --j and also use --j to condition the scaling factor:

[for='search-btn'] { /* same as before */ &:before { /* same as before */ height: $ico-w; transform: /* collapsed: not checked, --i is 0, --j is 1 * translation amount is 1*.25*$d = .25*$d * expanded: checked, --i is 1, --j is 0 * translation amount is 0*.25*$d = 0 */ translate(calc(var(--j)*#{.25*$ico-d})) /* collapsed: not checked, --i is 0, --j is 1 * scaling factor is 1 - 1*.5 = 1 - .5 = .5 * expanded: checked, --i is 1, --j is 0 * scaling factor is 1 - 0*.5 = 1 - 0 = 1 */ scalex(calc(1 - var(--j)*.5)) } &:after { /* same as before */ transform: translate(calc(var(--j)*#{-.25*$ico-d})) } }

We now have thew magnifier icon in the collapsed state:

See the Pen by thebabydino (@thebabydino) on CodePen.

Since we want both icon components to be rotated by 45deg, we add this rotation on the button itself:

[for='search-btn'] { /* same as before */ transform: translate(calc(var(--j)*#{-$x})) rotate(45deg);
}

Now we have the look we want for the collapsed state:

See the Pen by thebabydino (@thebabydino) on CodePen.

This still leaves the expanded state, where we need to turn the round :after pseudo-element into a line. We do this by scaling it down along the x axis and bringing its border-radius from 50% to 0%. The scaling factor we use is the ratio between the width $ico-w of the line we want to get and the diameter $ico-d of the circle it forms in the collapsed state. We’ve called this ratio $ico-f.

Since we only want to do this in the expanded state, when the checkbox is checked and --i is 1, we make both the scaling factor and the border-radius depend on --i and --j:

$ico-d: .5*$bar-h;
$ico-f: .125;
$ico-w: $ico-f*$ico-d; [for='search-btn'] { /* same as before */ &:after{ /* same as before */ /* collapsed: not checked, --i is 0, --j is 1 * border-radius is 1*50% = 50% * expanded: checked, --i is 1, --j is 0 * border-radius is 0*50% = 0 */ border-radius: calc(var(--j)*50%); transform: translate(calc(var(--j)*#{-.25*$ico-d})) /* collapsed: not checked, --i is 0, --j is 1 * scaling factor is 1 + 0*$ico-f = 1 * expanded: checked, --i is 1, --j is 0 * scaling factor is 0 + 1*$ico-f = $ico-f */ scalex(calc(1 - var(--j)*.5)) }
}

See the Pen by thebabydino (@thebabydino) on CodePen.

Hmm, almost, but not quite. Scaling has also shrunk our inset box-shadow along the x axis, so let’s fix that with a second inset shadow that we only get in the expanded state (when the checkbox is checked and --i is 1) and therefore, its spread and alpha depend on --i:

$ico-d: .5*$bar-h;
$ico-f: .125;
$ico-w: $ico-f*$ico-d; [for='search-btn'] { /* same as before */ --hsl: 0, 0%, 0%; color: HSL(var(--hsl)); &:after{ /* same as before */ box-shadow: inset 0 0 0 $ico-w currentcolor, /* collapsed: not checked, --i is 0, --j is 1 * spread radius is 0*.5*$ico-d = 0 * alpha is 0 * expanded: checked, --i is 1, --j is 0 * spread radius is 1*.5*$ico-d = .5*$ico-d * alpha is 1 */ inset 0 0 0 calc(var(--i)*#{.5*$ico-d}) HSLA(var(--hsl), var(--i)) }
}

This gives us our final result!

See the Pen by thebabydino (@thebabydino) on CodePen.

A few more quick examples

The following are a few more demos that use the same technique. We won’t be building these from scratch — we’ll merely go through the basic ideas behind them.

Responsive banners

On the left, a screenshot of the wide screen scenario. In the middle, a screenshot of the normal screen scenario. On the right, a screenshot of the narrow screen scenario.
Screenshot collage (live demo, not fully functional in Edge due to using a calc() value for font-size).

In this case, our actual elements are the smaller rectangles in front, while the number squares and the bigger rectangles in the back are created with the :before and :after pseudo-elements, respectively.

The backgrounds of the number squares are individual and set using a stop list variable --slist that’s different for each item.

<p style='--slist: #51a9ad, #438c92'><!-- 1st paragraph text --></p>
<p style='--slist: #ebb134, #c2912a'><!-- 2nd paragraph text --></p>
<p style='--slist: #db4453, #a8343f'><!-- 3rd paragraph text --></p>
<p style='--slist: #7eb138, #6d982d'><!-- 4th paragraph text --></p>

The things that influence the styles on the banners are the parity and whether we’re in the wide, normal or narrow case. These give us our switch variables:

html { --narr: 0; --comp: calc(1 - var(--narr)); --wide: 1; @media (max-width: 36em) { --wide: 0 } @media (max-width: 20em) { --narr: 1 }
} p { --parity: 0; &:nth-child(2n) { --parity: 1 }
}

The number squares are absolutely positioned and their placement depends on parity. If the --parity switch is off (0), then they’re on the left. If it’s on (1), then they’re on the right.

A value of left: 0% aligns with the left edge of the number square along the left edge of its parent, while a value of left: 100% aligns its left edge along the parent’s right edge.

In order to have the right edge of the number square aligned with the right edge of its parent, we need to subtract its own width out of the previous 100% value. (Remember that % values in the case of offsets are relative to the parent’s dimensions.)

left: calc(var(--parity)*(100% - #{$num-d}))

…where $num-d is the size of the numbering square.

In the wide screen case, we also push the numbering outwards by 1em — this means subtracting 1em from the offset we have so far for odd items (having the --parity switch off) and adding 1em to the offset we have so far for even items (having the --parity switch on).

Now the question here is… how do we switch the sign? The simplest way to do it is by using the powers of -1. Sadly, we don’t have a power function (or a power operator) in CSS, even though it would be immensely useful in this case:

/* * for --parity: 0, we have pow(-1, 0) = +1 * for --parity: 1, we have pow(-1, 1) = -1 */
pow(-1, var(--parity))

This means we have to make it work with what we do have (addition, subtraction, multiplication and division) and that leads to a weird little formula… but, hey, it works!

/* * for --parity: 0, we have 1 - 2*0 = 1 - 0 = +1 * for --parity: 1, we have 1 - 2*1 = 1 - 2 = -1 */
--sign: calc(1 - 2*var(--parity))

This way, our final formula for the left offset, taking into account both the parity and whether we’re in the wide case (--wide: 1) or not (--wide: 0), becomes:

left: calc(var(--parity)*(100% - #{$num-d}) - var(--wide)*var(--sign)*1em)

We also control the width of the paragraphs with these variables and max-width as we want it to have an upper limit and only fully cover its parent horizontally in the narrow case (--narr: 1):

width: calc(var(--comp)*80% + var(--narr)*100%);
max-width: 35em;

The font-size also depends on whether we’re in the narrow case (--narr: 1) or not (--narr: 0):

calc(.5rem + var(--comp)*.5rem + var(--narr)*2vw)

…and so do the horizontal offsets for the :after pseudo-element (the bigger rectangle in the back) as they’re 0 in the narrow case (--narr: 1) and a non-zero offset $off-x otherwise (--narr: 0):

right: calc(var(--comp)*#{$off-x}); left: calc(var(--comp)*#{$off-x});

Hover and focus effects

Animated gif. Shows red diagonal sliding bands covering the white button underneath the black text on hover/focus. On mouseout/ blur, the bands slide out the other way, not the way they entered.
Effect recording (live demo, not fully functional in Edge due to nested calc() bug).

This effect is created with a link element and its two pseudo-elements sliding diagonally on the :hover and :focus states. The link’s dimensions are fixed and so are those of its pseudo-elements, set to the diagonal of their parent $btn-d (computed as the hypotenuse in the right triangle formed by a width and a height) horizontally and the parent’s height vertically.

The :before is positioned such that its bottom left corner coincides to that of its parent, while the :after is positioned such that its top right corner coincides with that of its parent. Since both should have the same height as their parent, the vertical placement is resolved by setting top: 0 and bottom: 0. The horizontal placement is handled in the exact same way as in the previous example, using --i as the switch variable that changes value between the two pseudo-elements and --j, its complementary (calc(1 - var(--i))):

left: calc(var(--j)*(100% - #{$btn-d}))

We set the transform-origin of the :before to its left-bottom corner (0% 100%) and :after to its right-top corner (100% 0%), again, with the help of the switch --i and its complementary --j:

transform-origin: calc(var(--j)*100%) calc(var(--i)*100%)

We rotate both pseudo-elements to the angle between the diagonal and the horizontal $btn-a (also computed from the triangle formed by a height and a width, as the arctangent of the ratio between the two). With this rotation, the horizontal edges meet along the diagonal.

We then shift them outwards by their own width. This means we’ll use a different sign for each of the two, again depending on the switch variable that changes value in between the :before and :after, just like in the previous example with the banners:

transform: rotate($btn-a) translate(calc((1 - 2*var(--i))*100%))

In the :hover and :focus states, this translation needs to go back to 0. This means we multiply the amount of the translation above by the complementary --q of the switch variable --p that’s 0 in the normal state and 1 in the :hover or :focus state:

transform: rotate($btn-a) translate(calc(var(--q)*(1 - 2*var(--i))*100%))

In order to make the pseudo-elements slide out the other way (not back the way they came in) on mouse-out or being out of focus, we set the switch variable --i to the value of --p for :before and to the value of --q for :after, reverse the sign of the translation, and make sure we only transition the transform property.

Responsive infographic

On the left, a screenshot of the wide screen scenario. We have a three row, two column grid with the third row collapsed (height zero). The first level heading occupies either the column on the right (for odd items) or the one on the left (for even items). The second level heading is on the other column and on the first row, while the paragraph text is below the second level heading on the second row. On the right, a screenshot of the narrower scenario. In this case, the third row has a height enough to fit the paragraph text, but the second column is collapsed. The first and second level heading occupy the first and second row respectively.
Screenshot collage with the grid lines and gaps highlighted (live demo, no Edge support due to CSS variable and calc() bugs).

In this case, we have a three-row, two-column grid for each item (article element), with the third row collapsed in the wide screen scenario and the second column collapsed in the narrow screen scenario. In the wide screen scenario, the widths of the columns depend on the parity. In the narrow screen scenario, the first column spans the entire content-box of the element and the second one has width 0. We also have a gap in between the columns, but only in the wide screen scenario.

// formulas for the columns in the wide screen case, where
// $col-a-wide is for second level heading + paragraph
// $col-b-wide is for the first level heading
$col-1-wide: calc(var(--q)*#{$col-a-wide} + var(--p)*#{$col-b-wide});
$col-2-wide: calc(var(--q)*#{$col-b-wide} + var(--p)*#{$col-a-wide}); // formulas for the general case, combining the wide and normal scenarios
$row-1: calc(var(--i)*#{$row-1-wide} + var(--j)*#{$row-1-norm});
$row-2: calc(var(--i)*#{$row-2-wide} + var(--j)*#{$row-2-norm});
$row-3: minmax(0, auto);
$col-1: calc(var(--i)*#{$col-1-wide} + var(--j)*#{$col-1-norm});
$col-2: calc(var(--i)*#{$col-2-wide}); $art-g: calc(var(--i)*#{$art-g-wide}); html { --i: var(--wide, 1); // 1 in the wide screen case --j: calc(1 - var(--i)); @media (max-width: $art-w-wide + 2rem) { --wide: 0 }
} article { --p: var(--parity, 0); --q: calc(1 - var(--p)); --s: calc(1 - 2*var(--p)); display: grid; grid-template: #{$row-1} #{$row-2} #{$row-3}/ #{$col-1} #{$col-2}; grid-gap: 0 $art-g; grid-auto-flow: column dense; &:nth-child(2n) { --parity: 1 }
}

Since we’ve set grid-auto-flow: column dense, we can get away with only setting the first level heading to cover an entire column (second one for odd items and first one for even items) in the wide screen case and let the second level heading and the paragraph text fill the first free available cells.

// wide case, odd items: --i is 1, --p is 0, --q is 1
// we're on column 1 + 1*1 = 2
// wide case, even items: --i is 1, --p is 1, --q is 0
// we're on column 1 + 1*0 = 1
// narrow case: --i is 0, so var(--i)*var(--q) is 0 and we're on column 1 + 0 = 1
grid-column: calc(1 + var(--i)*var(--q)); // always start from the first row
// span 1 + 2*1 = 3 rows in the wide screen case (--i: 1)
// span 1 + 2*0 = 1 row otherwise (--i: 0)
grid-row: 1/ span calc(1 + 2*var(--i));

For each item, a few other properties depend on whether we’re in the wide screen scenario or not.

The vertical margin, vertical and horizontal padding values, box-shadow offsets and blur are all bigger in the wide screen case:

$art-mv: calc(var(--i)*#{$art-mv-wide} + var(--j)*#{$art-mv-norm});
$art-pv: calc(var(--i)*#{$art-pv-wide} + var(--j)*#{$art-p-norm});
$art-ph: calc(var(--i)*#{$art-ph-wide} + var(--j)*#{$art-p-norm});
$art-sh: calc(var(--i)*#{$art-sh-wide} + var(--j)*#{$art-sh-norm}); article { /* other styles */ margin: $art-mv auto; padding: $art-pv $art-ph; box-shadow: $art-sh $art-sh calc(3*#{$art-sh}) rgba(#000, .5);
}

We have a non-zero border-width and border-radius in the wide screen case:

$art-b: calc(var(--i)*#{$art-b-wide});
$art-r: calc(var(--i)*#{$art-r-wide}); article { /* other styles */ border: solid $art-b transparent; border-radius: $art-r;
}

In the wide screen scenario, we limit the items’ width, but let it be 100% otherwise.

$art-w: calc(var(--i)*#{$art-w-wide} + var(--j)*#{$art-w-norm}); article { /* other styles */ width: $art-w;
}

The direction of the padding-box gradient also changes with the parity:

background: linear-gradient(calc(var(--s)*90deg), #e6e6e6, #ececec) padding-box, linear-gradient(to right bottom, #fff, #c8c8c8) border-box;

In a similar manner, margin, border-width, padding, width, border-radius, background gradient direction, font-size or line-height for the headings and the paragraph text also depend on whether we’re in the wide screen scenario or not (and, in the case of the first level heading’s border-radius or background gradient direction, also on the parity).

The post DRY Switching with CSS Variables: The Difference of One Declaration appeared first on CSS-Tricks.

The All Powerful Front-End Developer

I posted a video of this talk some months back, but it was nearly an hour and a half long. Here’s an updated version that I gave at JAMstack_conf that’s only 30 minutes:

The gist is that the front-end stack is wildly powerful these days. Our front-end skillset can be expanded to give us power to do back-end-ish things and talk with APIs that allow us to build entire products in a way we haven’t quite been able to before.

The post The All Powerful Front-End Developer appeared first on CSS-Tricks.

Bridging the Gap Between CSS and JavaScript: CSS Modules, PostCSS and the Future of CSS

In the previous post in this two-part series, we explored the CSS-in-JS landscape and, we realized not only that CSS-in-JS can produce critical styles, but also that some libraries don’t even have a runtime. We saw that user experience can significantly improve by adding clever optimizations, which is why this series focuses on developer experience (the experience of authoring styles).

In this part, we’ll explore the tools for “plain ol’ CSS” by refactoring the Photo component from our existing example.

Controversy and #hotdrama

One of the most famous CSS debates is whether the language is fine just the way that it is. I think this debate stays alive because there is some truth to both sides. For example, while it’s true that CSS was initially designed to style a document rather than components of an application, it’s also true that upcoming CSS features will dramatically change this, and that many CSS mistakes stem from treating styling as an afterthought instead of taking time to learn it properly or hiring someone who’s good at it.

I don’t think that CSS tools themselves are the source of the controversy; we’ll probably always use them to some extent at the very least. But approaches like CSS-in-JS are different in that they patch up the shortcomings of CSS with client-side JavaScript. However, CSS-in-JS is not the only approach here; it is merely the newest. Remember when we used to have similar debates about preprocessors, like Sass? Sass has features, like mixins, that aren’t based on any CSS proposal (not to mention the entire indented syntax). However, Sass was born in a much different time and has reached a point where it’s no longer fair to include it in the debate because the debate itself has changed — so we started criticizing CSS-in-JS because it’s an easier target.

I think we should use tools that let us use proposed syntax today. Let’s use JavaScript Promises as an analogy. This feature isn’t supported by Internet Explorer, so many people include a polyfill for it. The point of polyfills is to enable us to pretend like the feature is supported everywhere by substituting native browser implementations with a patch. Same goes for transpiling new syntax with tools, like Babel. We can use it today because the code will be compiled to an older, well-supported syntax. This is a good approach because it allows us to use future features today while pushing JavaScript forward the way preprocessing tools, like Sass, have pushed CSS forward.

My take on the CSS controversy is that we should use tools that enable us to use future CSS today.

Preprocessors

We’ve already talked a bit about CSS preprocessors, so it’s worth discussing them in a little more details and how they fit into the CSS-in-JS conversation. We have Sass, Less and PostCSS (among others) that can imbue our CSS code with all kinds of new features.

For our example, we’re only going to be concerned with nesting, one of the most common and powerful features of preprocessors. I suggest using PostCSS because it gives us fine-grained control over the features we’re adding, which is exactly what we need in this case. The PostCSS plugin that we’re going to use is postcss-nesting because it follows the actual proposal for native CSS nesting.

The best way to use PostCSS with our compiling tool, webpack, is to add postcss-loader after css-loader in the configuration. When adding loaders after css-loader, it’s important to account for them in the css-loader options by setting importLoaders to the number of succeeding loaders, which in this case is 1:

{ test: /\.css$/, use: [ 'style-loader', { loader: 'css-loader', options: { importLoaders: 1, }, }, 'postcss-loader', ],
}

This ensures that CSS files imported from other CSS files will be processed with postcss-loader as well.

After setting up postcss-loader, we’ll install postcss-nesting and include it in the PostCSS configuration:

yarn add postcss-nesting

There are many ways to configure PostCSS. In this case, we’re going to add a postcss.config.js file at the root of our project:

module.exports = { plugins: { "postcss-nesting": {}, },
}

Now, we can write a CSS file for our Photo component. Let’s call it Photo.css:

.photo { width: 200px; &.rounded { border-radius: 1rem; }
} @media (min-width: 30rem) { .photo { width: 400px; }
}

Let’s also add a file called utils.css that contains a class for visually hiding elements, as we covered in the the first part of this series:

.visuallyHidden { border: 0; clip: rect(0 0 0 0); height: 1px; margin: -1px; overflow: hidden; padding: 0; position: absolute; width: 1px; white-space: nowrap;
}

Since our component relies on this utility, let’s include utils.css to Photo.css by adding an @import statement to the top:

@import url('utils.css');

This will ensure that webpack requires utils.css, thanks to css-loader. We can place utils.css anywhere we want and adjust the @import path. In this particular case, it’s a sibling of Photo.css.

Next, let’s import Photo.css into our JavaScript file and use the classes to style our component:

import React from 'react'
import { getSrc, getSrcSet } from './utils'
import './Photo.css' const Photo = ({ publicId, alt, rounded }) => ( <figure> <img className={rounded ? 'photo rounded' : 'photo'} src={getSrc({ publicId, width: 200 })} srcSet={getSrcSet({ publicId, widths: [200, 400, 800] })} sizes="(min-width: 30rem) 400px, 200px" /> <figcaption className="visuallyHidden">{alt}</figcaption> </figure>
) Photo.defaultProps = { rounded: false,
} export default Photo

While this will work, our class names are way too simple and they will most certainly clash with others completely unrelated to our .photo class. One of the ways of working around this is using a naming methodology, like BEM, to rename our classes (e.g. photo_rounded and photo__what-is-this--i-cant-even) to help prevent clashes from happening, but components quickly get complex and class names tend to get long, depending on the overall complexity of the project.

Meet CSS Modules.

CSS Modules

Simply put, CSS Modules are CSS files in which all class names and animations are scoped locally by default. They look a lot like regular CSS. For example, we can use our Photo.css and utils.css files as CSS Modules without modifying them at all, simply by passing modules: true to css-loader’s options:

{ loader: 'css-loader', options: { importLoaders: 1, modules: true, },
}

CSS Modules are an evolving feature and could be discussed at even greater length. Robin’s three-part series on it is a good overview and introduction.

While CSS Modules themselves look very similar to regular CSS, the way we use them is quite different. They are imported into JavaScript as objects where keys correspond to authored class names, and values are unique class names that are auto-generated for us that keep the scope limited to a component:

import React from 'react'
import { getSrc, getSrcSet } from './utils'
import styles from './Photo.css'
import stylesUtils from './utils.css' const Photo = ({ publicId, alt, rounded }) => ( <figure> <img className={rounded ? `${styles.photo} ${styles.rounded}` : styles.photo} src={getSrc({ publicId, width: 200 })} srcSet={getSrcSet({ publicId, widths: [200, 400, 800] })} sizes="(min-width: 30rem) 400px, 200px" /> <figcaption className={stylesUtils.visuallyHidden}>{alt}</figcaption> </figure>
) Photo.defaultProps = { rounded: false,
} export default Photo

Since we’re using utils.css as a CSS Module, we can remove the @import statement at the top of Photo.css. Also, notice that using camelCase to format class names makes them easier to use in JavaScript. If we had used dashes, we’d have to write things out in full, like stylesUtils['visually-hidden'].

CSS Modules have additional features, like composition. Right now, we’re importing utils.css into Photo.js to apply our component styles, but let’s say that we want to shift the responsibility of styling the caption to Photo.css instead. That way, as far as our JSX code is concerned, styles.caption is just another class name; it just so happens to visually hide the element, but it might be styled differently in the future. Either way, Photo.css will be making those decisions.

So let’s add a caption style to Photo.css to extend the properties of the visuallyHidden utility using composes:

.caption { composes: visuallyHidden from './utils.css';
}

We could just as well add more rules to that class, but this is all we need in this case. Now, we no longer need to import utils.css into Photo.js; we can simply use styles.caption instead:

<figcaption className={styles.caption}>{alt}</figcaption>

How does this work? Do the styles from visuallyHidden get copied over to caption? Let’s examine the value of styles.caption — whoa, two classes! That’s right: one is from visuallyHidden and the other one will apply any other styles we add to caption. CSS-in-JS makes it too easy to duplicate styles with libraries, like polished, but CSS Modules encourage you to reuse existing styles. No need to create a new VisuallyHidden React component to only apply several CSS rules.

Let’s take it even further by examining this uncomfortable class composition:

rounded ? `${styles.photo} ${styles.rounded}` : styles.photo

There are libraries for these situations, like classnames, which are useful for more complex class composition. In our example, though, we can keep on using composes and rename .rounded to .roundedPhoto:

.photo { width: 200px;
} .roundedPhoto { composes: photo; border-radius: 1rem;
} @media (min-width: 30rem) { .photo { width: 400px; }
} .caption { composes: visuallyHidden from './utils.css';
}

Now we can apply the class names to our component in a much more readable fashion:

rounded ? styles.roundedPhoto : styles.photo

But wait, what if we accidentally place the .roundedPhoto ruleset before .photo and some rules from .photo end up overriding rules from .roundedPhoto due to specificity? Don’t worry, CSS Modules prevent us from composing classes defined after the current class by throwing an error like this:

referenced class name "photo" in composes not found (2:3) 1 | .roundedPhoto {
> 2 | composes: photo; | ^ 3 | border-radius: 1rem; 4 | }

Note that it’s generally a good idea to use a file naming convention for CSS Modules, for example using the extension .module.css, because it’s common to want to apply some global styles as well.

Dynamic styles

So far, we’ve been conditionally applying predefined sets of styles, which is called conditional styling. What if we also want to be able to fine-tune the border radius of the rounded photos? This is called dynamic styling because we don’t know what the value is going to be in advance; it can change while the application is running.

There aren’t many use cases for dynamic styling — usually we’re styling conditionally, but in cases when we need this, how would we approach this? While we could get by with inline styles, a native solution for this type of problems is custom properties (a.k.a. CSS variables). A really valuable aspect of this feature is that browsers will update styles using custom properties when JavaScript changes them. We can set a custom property on an element through inline styles, which means that it will be scoped to that element and that element only:

style={typeof borderRadius !== 'undefined' ? { '--border-radius': borderRadius,
} : null}

In Photo.css, we can use this custom property by using var() and passing the default value as the second argument:

.roundedPhoto { composes: photo; border-radius: var(--border-radius, 1rem);
}

As far as JavaScript is concerned, it’s only passing a dynamic parameter to CSS, then when CSS takes over, it can apply the value as-is, calculate a new value from it using calc(), etc.

Fallback

At the time of this writing, the browser support for custom properties is… well, you decide for yourself. Not supporting these browsers is (probably) out of the question for a real-world application, but keep in mind that some styles are less important than others. In this case, it’s not a big deal if the border radius on IE is always 1rem. The application doesn’t have to look the same way on every browser.

The way we can automatically provide fallbacks for all custom properties is to install postcss-custom-properties and add it to our PostCSS configuration:

yarn add postcss-custom-properties
module.exports = { plugins: { 'postcss-nesting': {}, 'postcss-custom-properties': {}, },
}

This will generate a fallback for our border-radius rule:

.roundedPhoto { composes: photo; border-radius: 1rem; border-radius: var(--border-radius, 1rem);
}

Browsers that don’t understand var() will ignore that rule and use the previous one. Don’t let the name of the plugin fool you; it only partially improves the support for custom properties by providing static fallbacks. The dynamic aspect can’t be polyfilled.

Exposing values to JavaScript

In the previous part of this series, we explored how CSS-in-JS allows us to share almost anything between CSS and JavaScript, using media queries as an example. There is no possible way to achieve this here, right?

Thanks to Jonathan Neal, you can!

First, meet postcss-preset-env, the successor to cssnext. It’s a PostCSS plugin that acts as a preset similar to @babel/preset-env. It contains plugins like postcss-nesting, postcss-custom-properties, autoprefixer etc. so we can use future CSS today. It splits the plugins across four stages of standardization. Some of the features I’d like to show you aren’t included in the default range (stage 2+), so we’ll explicitly enable the ones we need:

yarn add postcss-preset-env
module.exports = { plugins: { 'postcss-preset-env': { features: { 'nesting-rules': true, 'custom-properties': true, // already included in stage 2+ 'custom-media-queries': true, // oooh, what's this? 🙂 }, }, },
}

Note that we replaced our existing plugins because this postcss-preset-env configuration includes them, meaning our existing code should work the same as before.

Using custom properties in media queries is invalid because that’s not what they were designed for. Instead we’ll use custom media queries:

@custom-media --photo-breakpoint (min-width: 30em); .photo { width: 200px;
} @media (--photo-breakpoint) { .photo { width: 400px; }
}

Even though this feature is in the experimental stage and therefore not supported in any browser, thanks to postcss-preset-env it just works! One catch is that PostCSS operates on a per-file basis, so this way only Photo.css can use --photo-breakpoint. Let’s do something about that.

Jonathan Neal recently implemented an importFrom option in postcss-preset-env, which is passed to other plugins that support it as well, like postcss-custom-properties and postcss-custom-media. Its value can be many things, but for the purpose of our example, it’s a path to a file that will be imported to the files PostCSS processes. Let’s call this one global.css and move our custom media query there:

@custom-media --photo-breakpoint (min-width: 30em);

…and let’s define importFrom, providing the path to global.css:

module.exports = { plugins: { 'postcss-preset-env': { importFrom: 'src/global.css', features: { 'nesting-rules': true, 'custom-properties': true, 'custom-media-queries': true, }, }, },
}

Now we can delete the @custom-media line at the top of Photo.css and our --photo-breakpoint value will still work, because postcss-preset-env will use the one from global.css to compile it. Same goes for custom properties and custom selectors.

Now, how to expose it to JavaScript? When experimental features like custom media queries get standardized and implemented in major browsers, we will be able to retrieve them natively from CSS. For example, this is how we would access a custom property called --font-family defined on :root:

const rootStyles = getComputedStyle(document.body)
const fontFamily = rootStyles.getPropertyValue('--font-family')

If custom media queries get standardized we will probably be able to access them in a similar way, but in the meantime we have to find an alternative. We could use the exportTo option to generate a JavaScript or JSON file, which we would import into JavaScript. However, the problem is that webpack would try to require it before it’s generated. Even if we generated it before running webpack, every update to global.css would cause webpack to re-compile twice, once to generate the output file, and once more to import it. I wanted a solution that’s unencumbered by its implementation.

For this series, I’ve created a brand new webpack loader called css-customs-loader just for you! It makes this task easy: all we need to is include it in our webpack configuration before css-loader:

{ test: /\.css$/, use: [ 'style-loader', 'css-customs-loader', { loader: 'css-loader', options: { importLoaders: 1, }, }, 'postcss-loader', ],
}

This exposes custom media queries, as well as custom properties, to JavaScript. We can access them simply by importing global.css:

import React from 'react'
import { getSrc, getSrcSet } from './utils'
import styles from './photo.module.css'
import { customMedia } from './global.css' const Photo = ({ publicId, alt, rounded, borderRadius }) => ( <figure> <img className={rounded ? styles.roundedPhoto : styles.photo} style={ typeof borderRadius !== 'undefined' ? { ['--border-radius']: borderRadius } : null } src={getSrc({ publicId, width: 200 })} srcSet={getSrcSet({ publicId, widths: [200, 400, 800] })} sizes={`${customMedia['--photo-breakpoint']} 400px, 200px`} /> <figcaption className={styles.caption}>{alt}</figcaption> </figure>
) Photo.defaultProps = { rounded: false,
} export default Photo

That’s it!

I created a repository demonstrating all of the concepts discussed in this series. Its readme also contains some advanced tips about the approach described in this post.

View Repo

Conclusion

It’s safe to say that tools like CSS Modules and PostCSS and upcoming CSS features are up to the task of dealing with many challenges of CSS. Whichever side of the CSS debate you’re on, this approach is worth exploring.

I have a strong CSS-in-JS background, but I’m very susceptible to hype, so keeping up with that world is very hard for me. While having styles next to the behavior can be succinct, it’s also mixing two very different languages — CSS is very verbose compared to JavaScript. This incentivized me to write less CSS because I wanted to avoid getting the file too crowded. This may be a matter of personal preference, but I didn’t want that to be an issue. Using a separate file for CSS finally gave my code some air.

While mastering this approach may not be as straightforward as CSS-in-JS, I believe it’s more rewarding in the long run. It will improve your CSS skills and make you better prepared for its future.

Article Series:

  1. CSS-in-JS
  2. CSS Modules, PostCSS and the Future of CSS (This post)

The post Bridging the Gap Between CSS and JavaScript: CSS Modules, PostCSS and the Future of CSS appeared first on CSS-Tricks.

CSS Grid in IE: Duplicate area names now supported!

Autoprefixer is now up to version 9.3.1 and there have been a lot of updates since I wrote the original three-part CSS Grid in IE series — the most important update of which is the new grid-areas system. This is mostly thanks to Bogdan Dolin, who has been working like crazy to fix loads of Autoprefixer issues. Autoprefixer’s grid translations were powerful before, but they have gotten far more powerful now!

Article Series:

  1. Debunking common IE Grid misconceptions
  2. CSS Grid and the new Autoprefixer
  3. Faking an auto-placement grid with gaps
  4. Duplicate area names now supported! (This Post)

How Autoprefixer supports duplicate area names

In Part 2 of the series, I pointed out why Autoprefixer wasn’t able to handle duplicate area names that were used across multiple selectors.

To summarize, this is the example I gave in the article:

.grid-alpha { grid-template-areas: "delta echo";
} .grid-beta { grid-template-areas: "echo delta";
} .grid-cell { /* What column does .grid-cell go in? */ -ms-grid-column: ???; grid-area: echo;
}

We thought that since Autoprefixer didn’t have access to the DOM, there was no way of knowing what grid the grid cell belonged to and thus which column the grid cell should go in.

However, I realized something. Grid cells are only ever affected by their direct parent element (ignoring display: contents). That meant that if the grid cell exists, it will always be a direct child of the grid template element. This gave me an idea for how we can solve the grid-area name conflict! 😁

.grid-alpha { grid-template-areas: "delta echo";
} .grid-beta { /* Uh oh, duplicate area names! */ grid-template-areas: "echo delta";
} .grid-cell { /* We will use the first occurrence by default */ -ms-grid-column: 2; grid-area: echo;
} /* We have detected a conflict! Prefix the class with the parent selector.
*/
.grid-beta > .grid-cell { /* NO MORE CONFLICT! */ -ms-grid-column: 1;
}

The entire grid-areas system needed to be re-written to achieve this, but it was totally worth the effort.

The way this new system works is that .grid-cell will default to using the first grid-template-areas property that it comes across. When it hits a conflicting grid template, it will create a new rule placing the parent selector in front of the child selector like so:

[full parent selector] > [direct child selector]

This is what we know purely from looking at the CSS and knowing how CSS Grid works:

  • A grid cell must be a direct descendant of the parent grid container for CSS Grid to work (assuming display: contents isn’t used).
  • grid-beta > .grid-cell will only work on .grid-cell elements that have been placed directly inside a .grid-beta element.
  • .grid-beta > .grid-cell will have no effect on .grid-cell elements placed directly inside .grid-alpha elements.
  • .grid-beta > .grid-cell will have no effect on .grid-cell elements nested deeply inside .grid-beta elements.
  • .grid-beta > .grid-cell will override the styling of the lonely .grid-cell CSS rule both because of rule order and specificity.

Because of those reasons, Autoprefixer can pretty safely resolve these conflicts without having any access to the DOM.

That last point in the list can be a little bit dangerous. It increases specificity which means that it may cause some IE styles to override others in a way that is different from how modern overrides are working. Since the generated rules only hold IE-specific grid styles, and they only apply under very specific circumstances, this is unlikely to cause an issue in 99.999% of circumstances. There is still potential for edge cases though.

If you ever find yourself needing to increase the specificity of the grid cell selector, here’s how to go about it.

Instead of writing this:

.grid { grid-template-areas: "area-name";
} .grid-cell { grid-area: area-name;
}

…we write the rule like this:

.grid { grid-template-areas: "area-name";
} .grid > .grid-cell { grid-area: area-name;
}

Autoprefixer will retain the selector and output something like this:

.grid { grid-template-areas: "area-name";
} .grid > .grid-cell { -ms-grid-column: 1; -ms-grid-row: 1; grid-area: area-name;
}

The exciting new possibilities!

So why is duplicate area name support so exciting?

Well, for one, it was just plain annoying when accidentally using a duplicate area name in older versions of Autoprefixer. It would break in IE and, in older versions of Autoprefixer, it would fail silently.

The main reason it is exciting though is that it opens up a whole new world of IE-friendly CSS Grid possibilities!

Use modifier classes to adjust a grid template

This was the use case that really made me want to get duplicate area name support into Autoprefixer. Think about this. You have a typical site with a header, a footer, a main area, and a sidebar down either side.

See the Pen Basic website layout by Daniel Tonon (@daniel-tonon) on CodePen.

Sometimes we want both sidebars, sometimes we want one sidebar, and sometimes we want no sidebars. This was very difficult to manage back when Autoprefixer didn’t support duplicate area names since we couldn’t share area names across multiple grid templates. We would have to do something like this for it to work:

/* Before Duplicate area names were supported - n = no side-bars - f = first sidebar only - s = second sidebar only - fs = first and second sidebars
*/
.main-area { display: grid; grid-template: "content-n" / 1fr;
} .main-area.has-first-sidebar { grid-template: "first-sb-f content-f" / 300px 1fr;
} .main-area.has-second-sidebar { grid-template: "content-s second-sb-s" / 1fr 300px;
}
.main-area.has-first-sidebar.has-second-sidebar { grid-template: "first-sb-fs content-fs second-sb-fs" / 200px 1fr 200px;
} .main-area > .content { grid-area: content-n; /* no side-bars */
} .main-area > .sidebar.first { grid-area: first-sb-f; /* first sidebar only */
} .main-area > .sidebar.second { grid-area: second-sb-s; /* second sidebar only */
} .main-area.has-first-sidebar > .content { grid-area: content-f; /* first sidebar only */
} .main-area.has-second-sidebar > .content { grid-area: content-s; /* second sidebar only */
} .main-area.has-first-sidebar.has-second-sidebar > .content { grid-area: content-fs; /* first and second sidebars */
} .main-area.has-first-sidebar.has-second-sidebar > .sidebar.first { grid-area: first-sb-fs; /* first and second sidebars */
} .main-area.has-first-sidebar.has-second-sidebar > .sidebar.second { grid-area: second-sb-fs; /* first and second sidebars */
}
Autoprefixer translation
/* Before Duplicate area names were supported - n = no sidebars - f = first sidebar only - s = second sidebar only - fs = first and second sidebars
*/ .main-area { display: -ms-grid; display: grid; -ms-grid-rows: auto; -ms-grid-columns: 1fr; grid-template: "content-n" / 1fr;
} .main-area.has-first-sidebar { -ms-grid-rows: auto; -ms-grid-columns: 300px 1fr; grid-template: "first-sb-f content-f" / 300px 1fr;
} .main-area.has-second-sidebar { -ms-grid-rows: auto; -ms-grid-columns: 1fr 300px; grid-template: "content-s second-sb-s" / 1fr 300px;
} .main-area.has-first-sidebar.has-second-sidebar { -ms-grid-rows: auto; -ms-grid-columns: 200px 1fr 200px; grid-template: "first-sb-fs content-fs second-sb-fs" / 200px 1fr 200px;
} .main-area > .content { -ms-grid-row: 1; -ms-grid-column: 1; grid-area: content-n; /* no side-bars */
} .main-area > .sidebar.first { -ms-grid-row: 1; -ms-grid-column: 1; grid-area: first-sb-f; /* first sidebar only */
} .main-area > .sidebar.second { -ms-grid-row: 1; -ms-grid-column: 2; grid-area: second-sb-s; /* second sidebar only */
} .main-area.has-first-sidebar > .content { -ms-grid-row: 1; -ms-grid-column: 2; grid-area: content-f; /* first sidebar only */
} .main-area.has-second-sidebar > .content { -ms-grid-row: 1; -ms-grid-column: 1; grid-area: content-s; /* second sidebar only */
} .main-area.has-first-sidebar.has-second-sidebar > .content { -ms-grid-row: 1; -ms-grid-column: 2; grid-area: content-fs; /* first and second sidebars */
} .main-area.has-first-sidebar.has-second-sidebar > .sidebar.first { -ms-grid-row: 1; -ms-grid-column: 1; grid-area: first-sb-fs; /* first and second sidebars */
} .main-area.has-first-sidebar.has-second-sidebar > .sidebar.second { -ms-grid-row: 1; -ms-grid-column: 3; grid-area: second-sb-fs; /* first and second sidebars */
}

🤢 Oh yeah, and don’t forget media queries! 🤮

That example was based off actual code that I had to write for an actual project… so yeah, I really wanted to get duplicate area name support into Autoprefixer.

Now that we do have duplicate area name support, we can simplify that code down to something much nicer 😊

/* Duplicate area names now supported! This code will work perfectly in IE with Autoprefixer 9.3.1
*/ .main-area { display: grid; grid-template: "content" / 1fr;
} .main-area.has-first-sidebar { grid-template: "first-sb content" / 300px 1fr;
} .main-area.has-second-sidebar { grid-template: "content second-sb" / 1fr 300px;
} .main-area.has-first-sidebar.has-second-sidebar { grid-template: "first-sb content second-sb" / 200px 1fr 200px;
} .content { grid-area: content;
} .sidebar.first { grid-area: first-sb;
} .sidebar.second { grid-area: second-sb;
}
Autoprefixer translation
.main-area { display: -ms-grid; display: grid; -ms-grid-rows: auto; -ms-grid-columns: 1fr; grid-template: "content" / 1fr;
} .main-area.has-first-sidebar { -ms-grid-rows: auto; -ms-grid-columns: 300px 1fr; grid-template: "first-sb content" / 300px 1fr;
} .main-area.has-second-sidebar { -ms-grid-rows: auto; -ms-grid-columns: 1fr 300px; grid-template: "content second-sb" / 1fr 300px;
} .main-area.has-first-sidebar.has-second-sidebar { -ms-grid-rows: auto; -ms-grid-columns: 200px 1fr 200px; grid-template: "first-sb content second-sb" / 200px 1fr 200px;
} .content { -ms-grid-row: 1; -ms-grid-column: 1; grid-area: content;
} .main-area.has-first-sidebar > .content { -ms-grid-row: 1; -ms-grid-column: 2;
} .main-area.has-second-sidebar > .content { -ms-grid-row: 1; -ms-grid-column: 1;
} .main-area.has-first-sidebar.has-second-sidebar > .content { -ms-grid-row: 1; -ms-grid-column: 2;
} .sidebar.first { -ms-grid-row: 1; -ms-grid-column: 1; grid-area: first-sb;
} .main-area.has-first-sidebar.has-second-sidebar > .sidebar.first { -ms-grid-row: 1; -ms-grid-column: 1;
} .sidebar.second { -ms-grid-row: 1; -ms-grid-column: 2; grid-area: second-sb;
} .main-area.has-first-sidebar.has-second-sidebar > .sidebar.second { -ms-grid-row: 1; -ms-grid-column: 3;
}

With that CSS, we are now able to use the has-first-sidebar and has-second-sidebar classes on the grid element to modify what grid template the browser uses. We no longer have to worry about defining exactly what grid grid cells are placed in.

Assign components reusable area names

One of the limitations of being forced to use unique area names was that components could not be given a single, consistent re-usable area name. We were forced to come up with unique area names for every grid.

Now that Autoprefixer has duplicate area name support, that limitation is gone. We can provide every component with a single area name that is based on their component name. We can then reference that one area name whenever we want to place that component inside a grid layout.

/* header.scss */
.header { grid-area: header;
} /* nav.scss */
.nav { grid-area: nav;
} /* sidebar.scss */
.sidebar { grid-area: sidebar;
} /* content.scss */
.content { grid-area: content;
} /* subscribe.scss */
.subscribe { grid-area: subscribe;
} /* footer.scss */
.footer { grid-area: footer;
} /* layout.scss */
.single-sidebar-layout { display: grid; grid-template-areas: "header header" "nav content" "subscribe content" "footer footer";
} .double-sidebar-layout { display: grid; grid-template-areas: "header header header" "nav content sidebar" "nav subscribe sidebar" "footer footer footer";
}
Autoprefixer translation
/* header.scss */
.header { -ms-grid-row: 1; -ms-grid-column: 1; -ms-grid-column-span: 2; grid-area: header;
} .double-sidebar-layout > .header { -ms-grid-row: 1; -ms-grid-column: 1; -ms-grid-column-span: 3;
} /* nav.scss */
.nav { -ms-grid-row: 2; -ms-grid-column: 1; grid-area: nav;
} .double-sidebar-layout > .nav { -ms-grid-row: 2; -ms-grid-row-span: 2; -ms-grid-column: 1;
} /* sidebar.scss */
.sidebar { -ms-grid-row: 2; -ms-grid-row-span: 2; -ms-grid-column: 3; grid-area: sidebar;
} /* content.scss */
.content { -ms-grid-row: 2; -ms-grid-row-span: 2; -ms-grid-column: 2; grid-area: content;
} .double-sidebar-layout > .content { -ms-grid-row: 2; -ms-grid-column: 2;
} /* subscribe.scss */
.subscribe { -ms-grid-row: 3; -ms-grid-column: 1; grid-area: subscribe;
} .double-sidebar-layout > .subscribe { -ms-grid-row: 3; -ms-grid-column: 2;
} /* footer.scss */
.footer { -ms-grid-row: 4; -ms-grid-column: 1; -ms-grid-column-span: 2; grid-area: footer;
} .double-sidebar-layout > .footer { -ms-grid-row: 4; -ms-grid-column: 1; -ms-grid-column-span: 3;
} /* layout.scss */
.single-sidebar-layout { display: -ms-grid; display: grid; grid-template-areas: "header header" "nav content" "subscribe content" "footer footer";
} .double-sidebar-layout { display: -ms-grid; display: grid; grid-template-areas: "header header header" "nav content sidebar" "nav subscribe sidebar" "footer footer footer";
}

Here’s what we’ve got. Note that this should be viewed in IE.

See the Pen component-area-name technique by Daniel Tonon (@daniel-tonon) on CodePen.

Duplicate area name limitation

There is one fairly common use case that Autoprefixer still can’t handle at the moment. When the parent selector of the grid cell does not match up with the grid template selector, it tries to resolve a duplicate area name:

.grand-parent .mother { grid-template-areas: "child";
} .grand-parent .father { grid-template-areas: "child child";
} /* This will work */
.grand-parent .mother .child { grid-area: child;
} /* This does not work because: - ".uncle" != ".grand-parent .mother" - ".uncle" != ".grand-parent .father" - "child" is a duplicate area name
*/
.uncle .child { grid-area: child;
}

Here is a more realistic scenario of the current limitation in Autoprefixer’s algorithm:

.component .grid { display: grid; grid-template-areas: "one two"; grid-template-columns: 1fr 1fr;
} /* This rule triggers duplicate area name conflicts. */
.component.modifier .grid { grid-template-areas: "one ... two"; grid-template-columns: 1fr 1fr 1fr;
} /* This does not work because: - ".component" != ".component .grid" - ".component" != ".component.modifier .grid" - area names "one" and "two" both have duplicate area name conflicts
*/
.component .cell-one { grid-area: one;
} .component .cell-two { grid-area: two;
}

There are really only three ways of resolving this conflict at the moment.

Option 1: Remove the parent selector from child elements

Without any safeguards in place for scoping styles to a particular component, this is by far the most dangerous way to resolve the issue. I don’t recommend it.

.component .grid { display: grid; grid-template-areas: "one two"; grid-template-columns: 1fr 1fr;
} .component.modifier .grid { grid-template-areas: "one ... two"; grid-template-columns: 1fr 1fr 1fr;
} .cell-one { grid-area: one;
} .cell-two { grid-area: two;
}
Autoprefixer translation
.component .grid { display: -ms-grid; display: grid; grid-template-areas: "one two"; -ms-grid-columns: 1fr 1fr; grid-template-columns: 1fr 1fr;
} .component.modifier .grid { grid-template-areas: "one ... two"; -ms-grid-columns: 1fr 1fr 1fr; grid-template-columns: 1fr 1fr 1fr;
} .cell-one { -ms-grid-row: 1; -ms-grid-column: 1; grid-area: one;
} .component.modifier .grid > .cell-one { -ms-grid-row: 1; -ms-grid-column: 1;
} .cell-two { -ms-grid-row: 1; -ms-grid-column: 2; grid-area: two;
} .component.modifier .grid > .cell-two { -ms-grid-row: 1; -ms-grid-column: 3;
}

Option 2: Go back to using unique area names

This solution is pretty ugly, but if you have no control over the HTML, then this is probably the best way of handling the issue.

.component .grid { display: grid; grid-template-areas: "one two"; grid-template-columns: 1fr 1fr;
} .component .cell-one { grid-area: one;
} .component .cell-two { grid-area: two;
} .component.modifier .grid { grid-template-areas: "modifier_one ... modifier_two"; grid-template-columns: 1fr 1fr 1fr;
} .component.modifier .cell-one { grid-area: modifier_one;
} .component.modifier .cell-two { grid-area: modifier_two;
}
Autoprefixer translation
.component .grid { display: -ms-grid; display: grid; grid-template-areas: "one two"; -ms-grid-columns: 1fr 1fr; grid-template-columns: 1fr 1fr;
} .component .cell-one { -ms-grid-row: 1; -ms-grid-column: 1; grid-area: one;
} .component .cell-two { -ms-grid-row: 1; -ms-grid-column: 2; grid-area: two;
} .component.modifier .grid { grid-template-areas: "modifier_one ... modifier_two"; -ms-grid-columns: 1fr 1fr 1fr; grid-template-columns: 1fr 1fr 1fr;
} .component.modifier .cell-one { -ms-grid-row: 1; -ms-grid-column: 1; grid-area: modifier_one;
} .component.modifier .cell-two { -ms-grid-row: 1; -ms-grid-column: 3; grid-area: modifier_two;
}

Option 3: Use a BEM-style naming convention

Yep, if you use the BEM naming convention (or something similar), this will practically never be an issue for you. This is easily the preferred way of dealing with the issue if it’s an option.

.component__grid { display: grid; grid-template-areas: "one two"; grid-template-columns: 1fr 1fr;
} .component__grid--modifier { grid-template-areas: "one ... two"; grid-template-columns: 1fr 1fr 1fr;
} .component__cell-one { grid-area: one;
} .component__cell-two { grid-area: two;
}
Autoprefixer translation
.component__grid { display: -ms-grid; display: grid; grid-template-areas: "one two"; -ms-grid-columns: 1fr 1fr; grid-template-columns: 1fr 1fr;
} .component__grid--modifier { grid-template-areas: "one ... two"; -ms-grid-columns: 1fr 1fr 1fr; grid-template-columns: 1fr 1fr 1fr;
} .component__cell-one { -ms-grid-row: 1; -ms-grid-column: 1; grid-area: one;
} .component__grid--modifier > .component__cell-one { -ms-grid-row: 1; -ms-grid-column: 1;
} .component__cell-two { -ms-grid-row: 1; -ms-grid-column: 2; grid-area: two;
} .component__grid--modifier > .component__cell-two { -ms-grid-row: 1; -ms-grid-column: 3;
}

I have some issues with BEM, but with a few tweaks to the syntax, it can be a really great way to control CSS specificity and scoping.

Other Autoprefixer updates

While I’m here, I’ll give you an update on a couple of other developments that have occurred in Autoprefixer since I wrote the original CSS Grid in IE series.

Before we dive in though, here is how to set things up in CodePen in case you want to follow along with your own experiments.

Using Autoprefixer Grid translations in CodePen

CodePen has already upgraded to version 9.3.1 of Autoprefixer. You will now easily be able to integrate IE-friendly CSS Grid styles into your pens. First, make sure that Autoprefixer is enabled in the CodePen settings (Settings > CSS > [Vendor Prefixing] > Autoprefixer).

Step 1: Turn on Autoprefixer in CodePen

Then add the all new /* autoprefixer grid: on */ control comment to the top of the CSS Panel. I will dive into this awesome new feature a bit more later.

Step 2: Add the control comment

You can now write modern IE-friendly CSS Grid code and CodePen will automatically add in all of the IE prefixes for you.

“View compiled CSS” button
The compiled CSS view

In order to actually test your pens in IE though, you will need to view the pen in “Debug Mode” (Change View > Debug Mode). The pen will need to be saved before you will have access to this view.

Enabling Debug Mode for testing in IE

If you’d like to try out a tool that shows you real-time Autoprefixer output, try the online tool just for that. Input CSS on the left. It will output Autoprefixer’s translated CSS on the right as you type.

autoprefixer.github.io

New Autoprefixer control comments

We don’t always have the ability to alter the Autoprefixer configuration settings. If you have ever tried to do some IE-friendly CSS Grid experiments in CodePen in the past, you will know the pain of not having direct access to the Autoprefixer settings. There are also some frameworks like Create React App and Angular that don’t allow users to alter the Autoprefixer settings. There was also a bit of a skill barrier that prevented some users from using Grid because they were unsure of how to enable Grid translations.

This limitation was causing pain for many users but then Andrey Alexandrov submitted a pull request that introduced a new control comment. This new control comment gave users the ability to easily turn grid translations on and off from inside the CSS file. This was far easier than doing it through configuration settings. It was also accessible to all users no matter how they compile their CSS.

Adding /* autoprefixer grid: on */ will enable grid translations for that entire block while /* autoprefixer grid: off */ will disable grid translations for that block. Only the first grid control comment in a block will be applied.

/* Globally enable grid prefixes */
/* autoprefixer grid: on */ .grid { display: grid;
} .non-ie .grid { /* Turn off grid prefixes but only for this block */ /* autoprefixer grid: off */ display: grid; /* Grid control comments affect the whole block. This control comment is ignored */ /* autoprefixer grid: on */ grid-column: 1;
}

The above code translates into the following (I’ve filtered out the explanation comments):

/* autoprefixer grid: on */ .grid { display: -ms-grid; display: grid;
} .non-ie .grid { /* autoprefixer grid: off */ display: grid; /* autoprefixer grid: on */ grid-column: 1;
}

@supports can disable grid translations

The new control comments aren’t the only way to selectively prevent Autoprefixer from outputting grid translation code.

Autoprefixer will never be able to support implicit grid auto-placement. If you use an @supports statement that checks for something like grid-auto-rows: 0, Autoprefixer will not output Grid translations for code entered inside that statement.

.prefixed .grid { display: -ms-grid; display: grid;
} /* Checking grid-auto-* support prevents prefixes */
@supports (grid-auto-rows: 0) { .modern .grid { display: grid; }
} /* Checking basic grid support still outputs prefixes */
@supports (display: grid) { .still-prefixed .grid { display: -ms-grid; display: grid; }
}

To support IE, Autoprefixer can generate something like 50 lines of extra CSS sometimes. If you are writing Grid code inside an @supports statement, you will want IE translations to be turned off to reduce the weight of your CSS. If you don’t want to use @supports (grid-auto-rows: 0) then you can use a control comment inside the @supports statement instead.

@supports (display: grid) { .prefixed .grid { display: -ms-grid; display: grid; }
} @supports (display: grid) { /* autoprefixer grid: off */ .modern .grid { display: grid; }
}

Autoprefixer now inherits grid-gaps

In the original article series, I said that Autoprefixer was unable to inherit grid-gap values. In version 9.1.1, grid-gap values were able to inherit through media queries. In version 9.3.1, they have become inheritable through more specific selectors as well.

.grid { display: grid; grid-gap: 20px; /* grid-gap is stated here */ grid-template: "one two" / 1fr 1fr;
} @media (max-width: 600px) { .grid { /* grid-gap is now inhereited here */ grid-template: "one" "two" / 1fr; }
} .grid.modifier { /* grid-gap is now inhereited here as well */ grid-template: "one" "two" / 1fr;
} .one { grid-area: one; }
.two { grid-area: two; }
Autoprefixer translation
.grid { display: -ms-grid; display: grid; grid-gap: 20px; /* grid-gap is stated here */ -ms-grid-rows: auto; -ms-grid-columns: 1fr 20px 1fr; grid-template: "one two" / 1fr 1fr;
} @media (max-width: 600px) { .grid { /* grid-gap is now inhereited here */ -ms-grid-rows: auto 20px auto; -ms-grid-columns: 1fr; grid-template: "one" "two" / 1fr; }
} .grid.modifier { /* grid-gap is now inherited here as well */ -ms-grid-rows: auto 20px auto; -ms-grid-columns: 1fr; grid-template: "one" "two" / 1fr;
} .one { -ms-grid-row: 1; -ms-grid-column: 1; grid-area: one;
} .grid.modifier > .one { -ms-grid-row: 1; -ms-grid-column: 1;
} .two { -ms-grid-row: 1; -ms-grid-column: 3; grid-area: two;
} .grid.modifier > .two { -ms-grid-row: 3; -ms-grid-column: 1;
} @media (max-width: 600px) { .one { -ms-grid-row: 1; -ms-grid-column: 1; } .two { -ms-grid-row: 3; -ms-grid-column: 1; }
}

grid-template: span X; now works

In Part 2 this series, I mentioned that the following syntax doesn’t work:

.grid-cell { grid-column: span 2;
}

This was fixed in version 9.1.1. It now translates into the following:

.grid-cell { -ms-grid-column-span: 2; grid-column: span 2;
}

New warning for mixing manual and area based placement

We had a user complain that Autoprefixer wasn’t handling grid areas correctly. Upon further investigation, we realized that they were applying both a grid-area and a grid-column setting within the same CSS rule.

.grid { display: grid; grid-template-areas: "a b";
} /* This doesn't work very well */
.grid-cell { grid-column: 2; grid-area: a;
}

Either use grid-area on its own, or us grid-column & grid-row. Never use both at the same time.

If you are curious, this is what Autoprefixer outputs for the above code:

.grid { display: -ms-grid; display: grid; grid-template-areas: "a b";
} /* This doesn't work very well */
.grid-cell { -ms-grid-row: 1; -ms-grid-column: 1; -ms-grid-column: 2; grid-column: 2; grid-area: a;
}

There is one exception to this. You may need a grid cell to overlap other grid cells in your design. You want to use grid-template-areas for placing your grid cells due to how much easier Autoprefixer makes the overall cell placement. You can’t really create cell overlaps using grid-template-areas though. To create this overlap, you can use grid-[column/row]-end: span X; after the grid-area declaration to force the overlap.

.grid { display: grid; grid-template-areas: "a . ." "a b b" "a . ."; grid-template-columns: 1fr 1fr 1fr; grid-template-rows: 1fr 1fr 1fr;
} .grid-cell-a { grid-area: a; /* place cell span after the grid-area to force an overlap */ grid-column-end: span 2;
}

See the Pen column-span + grid-area experiment by Daniel Tonon (@daniel-tonon) on CodePen.

However if you have declared grid gaps in your grid, you will need to write the column/row span prefix manually while taking the extra columns/rows generated by Autoprefixer into consideration (IE doesn’t support grid-gap). There is an issue for this on GitHub.

.grid { display: grid; grid-template-areas: "a . ." "a b c" "a . ."; grid-template-columns: 1fr 1fr 1fr; grid-template-rows: 1fr 1fr 1fr; grid-gap: 10px;
} .grid-cell-a { grid-area: a; /* IE column span added manually */ -ms-grid-column-span: 3; /* IE spans 2 columns + the gap */ grid-column-end: span 2; /* Modern browsers only span 2 columns */
}

See the Pen column-span + grid-area experiment 2 by Daniel Tonon (@daniel-tonon) on CodePen.

This technique currently produces a warning message in Autoprefixer that can’t easily be hidden. An issue for this is on GitHub.

New warning when using [align/justify/place]-[content/items]

Unfortunately. IE does not have the ability to align grid cells from the parent container. The only way to align grid cells in IE is by using the -ms-grid-row-align and the -ms-grid-column-align properties. These translate to align-self and justify-self in modern day grid syntax.

The following properties will trigger a warning in Autoprefixer if used in conjunction with a CSS Grid property:

  • align-items
  • align-content
  • justify-items
  • justify-content
  • place-items
  • place-content

The align-content, justify-content and place-content properties are pretty much impossible to replicate in IE. The align-items, justify-items and place-items properties, on the other hand, are quite easy to replicate using their self equivalents on the child elements (place-self support was added in 9.3.0).

/* [align/justify/place]-items is *NOT* IE friendly */ .align-justify-items { display: grid; align-items: start; justify-items: end;
} .place-items { display: grid; place-items: start end;
} /*[align/justify/place]-self *IS* IE friendly */ .align-justify-grid { display: grid; }
.align-justify-grid > * { align-self: start; justify-self: end;
} .place-grid { display: grid; }
.place-grid > * { place-self: start end;
}
Autoprefixer translation
/* [align/justify/place]-items is *NOT* IE friendly */ .align-justify-items { display: -ms-grid; display: grid; align-items: start; justify-items: end;
} .place-items { display: -ms-grid; display: grid; place-items: start end;
} /*[align/justify/place]-self *IS* IE friendly */ .align-justify-grid { display: -ms-grid; display: grid; }
.align-justify-grid > * { -ms-grid-row-align: start; align-self: start; -ms-grid-column-align: end; justify-self: end;
} .place-grid { display: -ms-grid; display: grid; }
.place-grid > * { -ms-grid-row-align: start; -ms-grid-column-align: end; place-self: start end;
}

.grid { [align/justify/place]-items } and .grid > * { [align/justify/place]-self } are generally quite interchangeable with one another. This substitution doesn’t always work, but in most cases, the two methods tend to act in much the same way.

Below is a pen demonstrating the difference between the IE friendly alignment method and the IE unfriendly alignment method. In modern browsers they look identical, but in IE one looks the same as the modern browsers and one does not.

See the Pen place-items experiment by Daniel Tonon (@daniel-tonon) on CodePen.

That’s all, folks

I hope you have enjoyed reading about the awesome new improvements the Autoprefixer community has made to the project over the past few months. The new control comments make enabling and disabling grid translations an absolute breeze. The new grid-areas system is also exciting. I love all the new IE-friendly CSS Grid options that the new areas system opens up, and I think you will too. 🙂

Article Series:

  1. Debunking common IE Grid misconceptions
  2. CSS Grid and the new Autoprefixer
  3. Faking an auto-placement grid with gaps
  4. Duplicate area names now supported! (This Post)

The post CSS Grid in IE: Duplicate area names now supported! appeared first on CSS-Tricks.