Tag: Flexible

Developer Decisions For Building Flexible Components

Blog posts that get into the whole “how to think like a front-end developer” vibe are my favorite. Michelle Barker nails that in this post, and does it without sharing a line of code!

We simply can no longer design and develop only for “optimal” content or browsing conditions. Instead, we must embrace the inherent flexibility and unpredictability of the web, and build resilient components. Static mockups cannot cater to every scenario, so many design decisions fall to developers at build time. Like it or not, if you’re a UI developer, you are a designer — even if you don’t consider yourself one!

There are a lot of unknowns in front-end development. Much longer than my little list. Content of unknown size and length is certainly one of them. Then square the possibilities with every component variation while ensuring good accessibility and you’ve got, well, a heck of a job to do.

Direct Link to ArticlePermalink


The post Developer Decisions For Building Flexible Components appeared first on CSS-Tricks. You can support CSS-Tricks by being an MVP Supporter.

CSS-Tricks

, , , ,

Hexagons and Beyond: Flexible, Responsive Grid Patterns, Sans Media Queries

A little while back, Chris shared this nice hexagonal grid. And true to its name, it’s using —wait for it — CSS Grid to form that layout. It’s a neat trick! Combining grid columns, grid gaps, and creative clipping churns out the final result.

A similar thing could be accomplished with flexbox, too. But I’m here to resurrect our old friend float to create the same sort of complex and responsive layout — but with less complexity and without a single media query.

I know, it’s hard to believe. So let’s start with a working demo:

This is a fully responsive hexagon grid made without media queries, JavaScript, or a ton of hacky CSS. Resize the demo screen and see the magic. In addition to being responsive, the grid also scales. For example, we can chuck more hexagons in there by adding more divs, and control both the sizing and spacing using CSS variables.

Cool, right? And this is only one example among many grids we will build in the same manner.

Making a grid of hexagons

First, we create our hexagon shape. This task is fairly easy using clip-path. We will consider a variable S that will define the dimension of our element. Bennett Feely’s Clippy is a great online generator for clip paths.

Creating a hexagonal shape using clip-path

Each hexagon is an inline-block element. The markup can go something like this:

<div class="main">   <div class="container">     <div></div>     <div></div>     <div></div>     <!--etc. -->   </div> </div>

…and the CSS:

.main {   display: flex; /* we will talk about this later ... */   --s: 100px;  /* size  */   --m: 4px;   /* margin */ }  .container {   font-size: 0; /* disable white space between inline block element */ }  .container div {   width: var(--s);   margin: var(--m);   height: calc(var(--s) * 1.1547);   display: inline-block;   font-size: initial; /* we reset the font-size if we want to add some content */   clip-path: polygon(0% 25%, 0% 75%, 50% 100%, 100% 75%, 100% 25%, 50% 0%); }

Nothing complex so far. We have a main element that holds a container which, in turn, holds the hexagons. Since we are dealing with inline-block, we need to fight the common white space issue (using the font-size trick) and we consider some margin (defined with the variable M) to control the space.

Toggling the font-size of the first demo to illustrate the white space issue

Here’s the result so far:

Every other row needs some negative offset so the rows overlap rather than stack directly on top of each other. That offset will be equal to 25% of the element height (see Figure 1). We apply that offset to margin-bottom to get the following:

.container div {   width: var(--s);   margin: var(--m);   height: calc(var(--s) * 1.1547);   display: inline-block;   font-size: initial;   clip-path: polygon(0% 25%, 0% 75%, 50% 100%, 100% 75%, 100% 25%, 50% 0%);   margin-bottom: calc(var(--m) - var(--s) * 0.2886); /* some negative margin to create overlap */ }

…and the result becomes:

Now the real trick is how we can shift the second row to get a perfect hexagon grid. We’ve already scrunched things to the point where the rows overlap each other vertically, but what we need is to push every other row toward the right so the hexagons stagger rather than overlap. Here’s where float and shape-outside come into play.

Did you wonder why we have a .main element wrapping our container and having display: flex ? That div is also a part of the trick. In a previous article, I used float and I needed that flexbox container in order to be able to use height: 100%. I will be doing the same thing here.

.container::before {   content: "";   width: calc(var(--s)/2 + var(--m));   float: left;   height: 100%; }

I am using the container::before pseudo-element to create a float element that take up all the height at the left of the grid, and that has a width equal to half a hexagon (plus its margin). We get the following result:

The yellow area is our.container::before pseudo-element.

Now, we can reach for shape-outside. Let’s take a quick refresher on what it does. Robin defines it nicely in the CSS-Tricks Almanac. MDN describes it nicely as well:

The shape-outside CSS property defines a shape—which may be non-rectangular—around which adjacent inline content should wrap. By default, inline content wraps around its margin box; shape-outside provides a way to customize this wrapping, making it possible to wrap text around complex objects rather than simple boxes.

Emphasis mine

Notice “inline content” in the definition. This explains exactly why the hexagons need to be inline-block elements. But to understand what kind of shape we need, let’s zoom into the pattern.

What’s cool about shape-inside is that it actually works with gradients. But what kind of gradient fits our situation?

If, for example, we have 10 rows of hexagons, we only need to shift means every even row. Seen differently, we need to shift every second row so we need a kind of repetition — perfect for a repeating gradient!

We’ll create a gradient with two colors:

  • A transparent one to create the “free space” while allowing the first row to stay in place (illustrated by the blue arrow above).
  • An opaque color to shift the second row to the right so the hexagons aren’t directly stacked on top of one another (illustrated by the green arrow).

Our shape-outside value will look like this:

shape-outside: repeating-linear-gradient(#0000 0 A, #000 0 B); /* #0000 = transparent */

Now, let’s find the value of A and B. B will simply be equal to the height of two rows since our logic need to repeat each two rows.

The height of two rows is equal to the height of two hexagons (including their margins), minus twice the overlap (2*Height + 4*M - 2*Height*25% = 1.5*Height + 4*M ). Or, expressed in CSS with calc():

calc(1.732 * var(--s) + 4 * var(--m))

That’s a lot! So, let’s hold all of this in a CSS custom property, F.

The value of A (defined by the blue arrow in the previous figure) needs to be at least equal to the size of one hexagon, but it can also be bigger. In order to push the second row over to the right, we need few pixel of opaque color so A can simply be equal to B - Xpx, where X is a small value.

We end up with something like this:

shape-outside: repeating-linear-gradient(#0000 0 calc(var(--f) - 3px),#000 0 var(--f));

And the following result:

shape-outside is applied to the floated element, creating a floated area with a predating linear gradient.

See that? Our repeating linear gradient’s shape is pushing every other row to the right by one half the width of a hexagon to offset the pattern.

Let’s put that all together:

.main {   display:flex;   --s: 100px;  /* size  */   --m: 4px;    /* margin */   --f: calc(var(--s) * 1.732 + 4 * var(--m) - 1px);  }  .container {   font-size: 0; /* disable white space between inline block element */ }  .container div {   width: var(--s);   margin: var(--m);   height: calc(var(--s) * 1.1547);   display: inline-block;   font-size:initial;   clip-path: polygon(0% 25%, 0% 75%, 50% 100%, 100% 75%, 100% 25%, 50% 0%);   margin-bottom: calc(var(--m) - var(--s) * 0.2885); }  .container::before {   content: "";   width: calc(var(--s) / 2 + var(--m));   float: left;   height: 120%;    shape-outside: repeating-linear-gradient(#0000 0 calc(var(--f) - 3px), #000 0 var(--f)); }

That’s it! With no more than 15 CSS declarations, we have a responsive grid that fit nicely into all the screen sizes and we can easily adjust things by simply controling two variables.

You may have noticed that I am adding -1px to the variable F. Since we are dealing with calculation that involve decimals, the rounding may give us bad results. To avoid this we add or remove few pixels. I am also using 120% instead of 100% for the height of the floated element for similar reasons. There is no particular logic with theses values; we simply adjust them to make sure to cover most of the cases without any misaligning our shapes.

Want more shapes?

We can do more than hexagons with this approach! Let’s create a “rhombus” grid instead. Again, we start with our clip-path to create the shape:

Rhombus shape using clip-path

The code is basically the same. What’s changing are the calculations and values. Find below a table that will illustrate the changes.

Hexagon grid Rhombus grid
height calc(var(--s)*1.1547) var(--s)
clip-path polygon(0% 25%, 0% 75%, 50% 100%, 100% 75%, 100% 25%, 50% 0%) polygon(50% 0, 100% 50%, 50% 100%, 0 50%)
margin-bottom calc(var(--m) - var(--s)*0.2885) calc(var(--m) - var(--s)*0.5)
--f calc(var(--s)*1.7324 + 4*var(--m)) calc(var(--s) + 4*var(--m))

And we’re done! A mere four changes to our code gets us a completely new grid but with a different shape.

Just how flexible is this?

We saw how we were able to make the hexagon and rhombus grids using the exact same code structure, but different variables. Let me blow your mind with another idea: What about making that calculation a variable so that we can easily switch between different grids without changing the code? We can certainly do that!

We’ll use an octagonal shape because it’s more of a generic shape from that we can use to create other shapes (a hexagon, a rhombus, a rectangle, etc.) simply by changing a few values.

The points on this octagon shape are defined in the clip-path property.

Our octagon is defined with four variables:

  • S: the width.
  • R: the ratio that will help us defines the height based on the width.
  • hc and vc : both of these will control our clip-path values and the shape we want to get. hc will be based on the width while vc on the height

I know it looks hefty, but the clip-path is defined using eight points (like shown in the figure). Adding some CSS variables, we get this:

clip-path: polygon(    var(--hc) 0, calc(100% - var(--hc)) 0, /* 2 points at the top */    100% var(--vc),100% calc(100% - var(--vc)), /* 2 points at the right */    calc(100% - var(--hc)) 100%, var(--hc) 100%, /* 2 points at the bottom */    0 calc(100% - var(--vc)),0 var(--vc) /* 2 points at the left */ );

This is what we’re aiming for:

Let’s zoom in to identify the different values:

The overlap between each row (illustrated by the red arrow) can be expressed using the vc variable which gives us a margin-bottom equal to M - vc (where M is our margin).

In addition to the margin we applied between our element, we also need an additional horizontal margin (illustrated by the yellow arrow) equal to S - 2*hc. Let’s define another variable for the horizontal margin (MH) that is equal to M + (S - 2*hc)/2.

The height of two rows is equal to twice the size of a shape (plus the margin), minus twice the overlap, or 2*(S + 2*M) - 2*vc.

Let’s update our table of values to see how we’re calculating things between the different grids:

Hexagon grid Rhombus grid Octagon grid
height calc(var(--s)*1.1547) var(--s) calc(var(--s)*var(--r)))
clip-path polygon(0% 25%, 0% 75%, 50% 100%, 100% 75%, 100% 25%, 50% 0%) polygon(50% 0, 100% 50%, 50% 100%, 0 50%) polygon(var(--hc) 0, calc(100% - var(--hc)) 0,100% var(--vc),100% calc(100% - var(--vc)), calc(100% - var(--hc)) 100%,var(--hc) 100%,0 calc(100% - var(--vc)),0 var(--vc))
--mh calc(var(--m) + (var(--s) - 2*var(--hc))/2)
margin var(--m) var(--m) var(--m) var(--mh)
margin-bottom calc(var(--m) - var(--s)*0.2885) calc(var(--m) - var(--s)*0.5) calc(var(--m) - var(--vc))
--f calc(var(--s)*1.7324 + 4*var(--m)) calc(var(--s) + 4*var(--m)) calc(2*var(--s) + 4*var(--m) - 2*var(--vc))

Alright, let’s update our CSS with those adjustments:

.main {   display: flex;   --s: 100px;  /* size  */   --r: 1; /* ratio */    /* clip-path parameter */   --hc: 20px;    --vc: 30px;    --m: 4px; /* vertical margin */   --mh: calc(var(--m) + (var(--s) - 2*var(--hc))/2); /* horizontal margin */   --f: calc(2*var(--s) + 4*var(--m) - 2*var(--vc) - 2px); }  .container {   font-size: 0; /* disable white space between inline block element */ }  .container div {   width: var(--s);   margin: var(--m) var(--mh);   height: calc(var(--s)*var(--r));   display: inline-block;   font-size: initial;   clip-path: polygon( ... );   margin-bottom: calc(var(--m) - var(--vc)); }  .container::before {   content: "";   width: calc(var(--s)/2 + var(--mh));   float: left;   height: 120%;    shape-outside: repeating-linear-gradient(#0000 0 calc(var(--f) - 3px),#000 0 var(--f)); }

As we can see, the code structure is the same. We simply added more variable to control the shape and extend the margin property.

And below a working example. Adjust the different variables to control the shape while having a fully responsive grid:

An interactive demo, you say? You bet!

To make things easier, I am expressing the vc and hc as percetange of the width and height so we can easily scale our elements without breaking the clip-path

From the above we can easily get the initial hexagonal grid:

The rhombus grid:

And yet another hexagon grid:

A masonry-like grid:

And a checkerboard while we are at it:

A lot of possibilities to create a responsive grid with any kind of shape! All we have to do is adjust few variables.

Fixing the alignment

Let’s try to control the alignment of our shapes. Since we are dealing with inline-block elements, we’re dealing with default left alignment and some empty space at the end, depending on viewport width.

Notice that we alternate between two kind of grids based on the screen width:

Grid #1: A different number of items per row (NN-1,NN-1, etc.)
Grid #2: The same number of items per row (NNNN, etc.)

It would be good to always have one of the grid all the time (either #1 or #2) and center everything so that the free space is equally divided on both sides.

In order to get the first grid in the figure above, the container width needs to be a multiplier of the size of one shape, plus its margin, or N*(S + 2*MH), where N is an integer value.

This may sound impossible with CSS, but it’s indeed possible. I made it using CSS grid:

.main {   display: grid;   grid-template-columns: repeat(auto-fit, calc(var(--s) + 2*var(--mh)));   justify-content: center; }  .container {   grid-column: 1/-1; }

.main is now a grid container. Using grid-template-columns, I define the column width (as previously explained) and use the auto-fit value to get as many columns as possible into the available space. Then, the .container spans all of the grid columns using 1/-1 — which means that the width of our container will be a mutiplier of one column size.

All it takes to center things is justify-content: center.

Yes, CSS is magic!

Resize the demo and notice that not only do we have the first grid from the figure, but everything is perfectly centered as well.

But wait, we removed display: flex and swapped in display: grid… so how is the percentage-based height of the float still working? I had said that using a flex container was the key for that, no?

Well, turns out CSS grid sports that feature too. From the specification:

Once the size of each grid area is thus established, the grid items are laid out into their respective containing blocks. The grid area’s width and height are considered definite for this purpose.

Note: Since formulas calculated using only definite sizes, such as the stretch fit formula, are also definite, the size of a grid item which is stretched is also considered definite.

A grid item has a stretch alignment by default, so its height is definite, meaning using a percentage as a height inside it is perfectly valid.

Let’s say we instead want the second grid in the figure — we simply add an extra column with a width equal to half the width of the other columns:

.main {   display: grid;   grid-template-columns: repeat(auto-fit,calc(var(--s) + 2*var(--mh))) calc(var(--s)/2 + var(--mh));   justify-content :center; }

Now, in addition to a fully responsive grid that is flexible enough to take custom shapes, everything is perfectly centred!

Fighting the overflow

The use of negative margin-bottom on the last items and the float element pushing our items will create some unwanted overflow that may affect the content placed after our grid.

If you resize the demo, you will notice an overflow equal to the negative offset and sometimes it’s bigger. The fix is to add some padding-bottom to our container. I will make the padding equal to the height of one shape:

I have to admit that there isn’t a perfect solution to fight that overflow and to control the space below our grid. That space depends on a lot of factors and we may have to use a different padding value for each case. The safest solution is to consider a big value that covers most of the cases.

Wait, one more: a pyramidal grid

Let’s take everything we’ve learned and build another amazing grid. This time, we’ll transform the grid we just made into a pyramidal one.

Consider that, unlike the grid we’ve made so far, the number of elements is important especially for the responsive part. It’s required to know the number of elements and more precesily the number of rows.

Different pyramidal grid based on the number of items

It doesn’t mean we need a bunch of hardcoded values; rather we use an extra variable to adjust things based on the number of rows.

The logic is based on the number of rows because different numbers of elements may give us the same number of rows. For example, there are five rows when we have between 11 and 15 elements, even if the last row is not fully occupied. Having between 16 and 21 elements gives us six rows, and so on. The number of rows is our new variable.

Before digging into the geometry and the math here is a working demo:

Notice that most of the code is the same as what we’ve done in the previous examples. So let’s focus on the new properties that we’ve added:

.main {   --nr: 5;  /* number of rows */ }  .container {   max-width: calc(var(--nr)*(var(--s) + 2*var(--mh)));   margin: 0 auto; }  .container::before , .container i {   content: "";   width: calc(50% - var(--mh) - var(--s)/2);   float: left;   height: calc(var(--f)*(var(--nr) - 1)/2);   shape-outside: linear-gradient(to bottom right, #000 50%, #0000 0); }  .container i {   float:right;   shape-outside: linear-gradient(to bottom left, #000 50%, #0000 0); }

NR is our variable for the number of rows. The width of the container needs to be equal to the last row of the pyramid to make sure it hold all the elements. If you check the previous figure, you’ll see that the number of the items contained in the last row is simply equal to the number of rows, which means the formula is: NR* (S + 2*MH).

You may have also noticed that we also added an <i> element in there. We did that because we need two floating elements where we will apply shape-outside.

To understand why we need two floating elements let’s see what is done behind the scenes:

A pyramid grid of octagon shapes. The octagons alternate between green and red. There are 5 rows of octagons.
Pyramidal grid

The blue elements are our floating elements. Each one is having a width equal to half the container size, minus half a shape size, plus margin. The height is equal to four rows in our case, and to NR - 1 in a more generic case. Earlier, we defined the height of two rows, F, so the height of one row is F/2. That’s how we landed at height: calc(var(--f)*(var(--nr) - 1)/2.

Now that we have the size of our elements, we need to apply a gradient to our shape-outside.

The purple coloration in the figure above is the restricted area for our elements (it need to be an opaque color). The remaining area is the free space where the elements can flow (it need to be a transparent color). This can be done using a diagonal gradient:

shape-outside: linear-gradient(to bottom right, #000 50%, #0000 0); 

We simply change right with left for the other floated element. You have probably noticed that this is not responsive. In fact, go ahead and adjust the viewport width of the demo and see just how unresponsive this is.

We have a couple of options to get responsive:

  1. We can fall back to the first grid when the container width is smaller than the viewport width. It’s a bit tricky to code, but it allows us to preserve the same size for our elements.
  2. We can reduce the size of our elements in order to keep the pyramidal grid. This is easier to code using the percentage-based value trick, but that could result in super tiny elements on smaller screen sizes.

Let’s go with the first solution. We like a good challenge, right?

To get the pyramidal grid, we needed two floated element. The initial grid needed just one floated element. Luckily, our structure allows us to have three floated elements without needing to add more elements to the markup, thanks to pseudo-elements. We will use container::before, i::before, i::after:

/* Same as before... */  /* The initial grid */ .container::before {   content: "";   width: calc(var(--s)/2 + var(--mh));   float: left;   height: 120%;    shape-outside: repeating-linear-gradient(#0000 0 calc(var(--f) - 3px),#000 0 var(--f)); }  /* The pyramidal grid */ .container i::before , .container i::after {   content: "";   width: calc(50% - var(--mh) - var(--s)/2);   float: left;   height: calc(var(--f)*(var(--nr) - 1)/2);   shape-outside: linear-gradient(to bottom right,#000 50%,#0000 0); }  .container i::after {   float:right;   shape-outside: linear-gradient(to bottom left,#000 50%,#0000 0); }

Now we need a trick that lets us use either the first floated element or the other two, but not all of them at the same time. This condition should be based on the width of our container:

  • If the container width is bigger than the width of the last row, we can have our pyramid and use the floated elements inside of <i>.
  • If the container width is smaller than the width of the last row, we switch to the other grid and use the first floated element.

We can use clamp() for this! It’s sort of like a conditional function that sets a minimum and maximum range and, within that range, we provide it an “ideal” value to use between those points. This way, we can “switch” between grids using our formulas as clamped values, and still avoid using media queries.

Our code will look like this:

.main {   /* the other variables won't change*/   --lw: calc(var(--nr)*(var(--s) + 2*var(--mh))); /* width of last row */ }  .container {   max-width: var(--lw); }  /* The initial grid */ .container::before {   width: clamp(0px, (var(--lw) - 100%)*1000, calc(var(--s)/2 + var(--mh))); }  /* The pyramidal grid */ .container i::before, .container i::after {   width: clamp(0px, (100% - var(--lw) + 1px)*1000, calc(50% - var(--mh) - var(--s)/2)); }

On larger screens, the width of the container (LW) is now equal to its max-width, so 100% == LW. That means that the width of .container::before is equal to 0px (and results in this floated element becoming disabled).

For the other floating elements, we clamp the width:

width: clamp(0px, (100% - var(--lw) + 1px)*1000, calc(50% - var(--mh) - var(--s)/2));

…where the middle value ((100% - LW + 1px)*1000) is equal to (0 + 1px)*1000 = 1000px (an intentionally large, but arbitrary value). It gets clamped to calc(50% - var(--mh) - var(--s)/2). In other words, these floated elements are enabled with the correct width (the one we defined previously)

Voilà! we have a pyramidal shape on large screen.

Now, when the container width get smaller, LW is going to be greater than 100%. So, (LW - 100%) will be positive. Multiplied by a big value, it’s clamped to calc(var(--s)/2 + var(--mh)), which enables the first floated element. For the other float elements, (100% - LW + 1px) resolves to a negative value and is clamped to 0px, which disables the float elements.

Resize the below demo and see how we switch between both grids

Let’s try adding more elements:

See that? Things are scaling perfectly. Let’s toss more elements at it just for kicks:

Still great. Notice that the last row isn’t even full. Just shows that this approach covers a bunch of cases. We can also combine this with the CSS grid alignment trick we used earlier:

Do you think “float” is such a bad thing now?

Want invert the pyramid?

Like illustrated with the above figure, two changes to the previous code can invert our pyramid:

  • I change the direction of the gradient from to bottom left|right to to top left|right,
  • I add a margin-top equal to the height of one row.

And, hey, we can swap between both pyramid easily:

Isn’t this beautiful? We have a responsive pyramidal grid with custom shapes that we can easily invert and that fallback to another responsive grid on small screen while everything is perfectly centred. All this without a single media query or JavaScript, but instead using the often overlooked float property.

You will probably notice some missalignment in some particular cases. Yes, it’s again some rounding issue related to the calculation we are doing and the fact that we are trying to make this generic with the interactive demos. To rectify this, we simply adjust few values manually (epsecially the percentage of the gradient) until we get back a perfect alignment.

That’s a float wrap!

There we have it: combining float with shape-outside can help us make complex, flexible and responsive layouts — long live float!

The article ends here but this is only the beginning. I provided you with the layout and now you can easily put any content inside the divs, apply a background, shadows, animations, etc.


The post Hexagons and Beyond: Flexible, Responsive Grid Patterns, Sans Media Queries appeared first on CSS-Tricks.

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

CSS-Tricks

, , , , , , , ,
[Top]

A Super Flexible CSS Carousel, Enhanced With JavaScript Navigation

Not sure about you, but I often wonder how to build a carousel component in such a way that you can easily dump a bunch of items into the component and get a nice working carousel — one that allows you to scroll smoothly, navigate with the dynamic buttons, and is responsive. If that is the thing you’d like to build, follow along and we’ll work on it together!

This is what we’re aiming for:

We’re going to be working with quite a bit of JavaScript, React and the DOM API from here on out.

First, let’s spin up a fresh project

Let’s start by bootstrapping a simple React application with styled-components tossed in for styling:

npx create-react-app react-easy-carousel  cd react-easy-carousel yarn add styled-components yarn install  yarn start

Styling isn’t really the crux of what we’re doing, so I have prepared aa bunch of predefined components for us to use right out of the box:

// App.styled.js import styled from 'styled-components'  export const H1 = styled('h1')`   text-align: center;   margin: 0;   padding-bottom: 10rem; ` export const Relative = styled('div')`   position: relative; ` export const Flex = styled('div')`   display: flex; ` export const HorizontalCenter = styled(Flex)`   justify-content: center;   margin-left: auto;   margin-right: auto;   max-width: 25rem; ` export const Container = styled('div')`   height: 100vh;   width: 100%;   background: #ecf0f1; ` export const Item = styled('div')`   color: white;   font-size: 2rem;   text-transform: capitalize;   width: $ {({size}) => `$ {size}rem`};   height: $ {({size}) => `$ {size}rem`};   display: flex;   align-items: center;   justify-content: center; `

Now let’s go to our App file, remove all unnecessary code, and build a basic structure for our carousel:

// App.js import {Carousel} from './Carousel'  function App() {   return (     <Container>       <H1>Easy Carousel</H1>       <HorizontalCenter>         <Carousel>         {/* Put your items here */}         </Carousel>       </HorizontalCenter>     </Container>   ) } export default App

I believe this structure is pretty straightforward. It’s the basic layout that centers the carousel directly in the middle of the page.

Let’s talk about the structure of our component. We’re gonna need the main <div> container which as our base. Inside that, we’re going to take advantage of native scrolling and put another block that serves as the scrollable area.

// Carousel.js  <CarouserContainer>   <CarouserContainerInner>     {children}   </CarouserContainerInner> </CarouserContainer>

You can specify width and height on the inner container, but I’d avoid strict dimensions in favor of some sized component on top of it to keep things flexible.

Scrolling, the CSS way

We want that scroll to be smooth so it’s clear there’s a transition between slides, so we’ll reach for CSS scroll snapping, set the scroll horizontally along the x-axis, and hide the actual scroll bar while we’re at it.

export const CarouserContainerInner = styled(Flex)`   overflow-x: scroll;   scroll-snap-type: x mandatory;   -ms-overflow-style: none;   scrollbar-width: none;    &::-webkit-scrollbar {     display: none;   }    & > * {     scroll-snap-align: center;   } `

Wondering what’s up with scroll-snap-type and scroll-snap-align? That’s native CSS that allows us to control the scroll behavior in such a way that an element “snaps” into place during a scroll. So, in this case, we’ve set the snap type in the horizontal (x) direction and told the browser it has to stop at a snap position that is in the center of the element.

In other words: scroll to the next slide and make sure that slide is centered into view. Let’s break that down a bit to see how it fits into the bigger picture.

Our outer <div> is a flexible container that puts it’s children (the carousel slides) in a horizontal row. Those children will easily overflow the width of the container, so we’ve made it so we can scroll horizontally inside the container. That’s where scroll-snap-type comes into play. From Andy Adams in the CSS-Tricks Almanac:

Scroll snapping refers to “locking” the position of the viewport to specific elements on the page as the window (or a scrollable container) is scrolled. Think of it like putting a magnet on top of an element that sticks to the top of the viewport and forces the page to stop scrolling right there.

Couldn’t say it better myself. Play around with it in Andy’s demo on CodePen.

But, we still need another CSS property set on the container’s children (again, the carousel slides) that tells the browser where the scroll should stop. Andy likens this to a magnet, so let’s put that magnet directly on the center of our slides. That way, the scroll “locks” on the center of a slide, allowing to be full in view in the carousel container.

That property? scroll-snap-align.

& > * {   scroll-snap-align: center; }

We can already test it out by creating some random array of items:

const colors = [   '#f1c40f',   '#f39c12',   '#e74c3c',   '#16a085',   '#2980b9',   '#8e44ad',   '#2c3e50',   '#95a5a6', ] const colorsArray = colors.map((color) => (   <Item     size={20}     style={{background: color, borderRadius: '20px', opacity: 0.9}}     key={color}   >     {color}   </Item> ))

And dumping it right into our carousel:

// App.js <Container>   <H1>Easy Carousel</H1>   <HorizontalCenter>     <Carousel>{colorsArray}</Carousel>   </HorizontalCenter> </Container>

Let’s also add some spacing to our items so they won’t look too squeezed. You may also notice that we have unnecessary spacing on the left of the first item. We can add a negative margin to offset it.

export const CarouserContainerInner = styled(Flex)`   overflow-x: scroll;   scroll-snap-type: x mandatory;   -ms-overflow-style: none;   scrollbar-width: none;   margin-left: -1rem;    &::-webkit-scrollbar {     display: none;   }    & > * {     scroll-snap-align: center;     margin-left: 1rem;   } `

Take a closer look at the cursor position while scrolling. It’s always centered. That’s the scroll-snap-align property at work!

And that’s it! We’ve made an awesome carousel where we can add any number of items, and it just plain works. Notice, too, that we did all of this in plain CSS, even if it was built as a React app. We didn’t really need React or styled-components to make this work.

Bonus: Navigation

We could end the article here and move on, but I want to take this a bit further. What I like about what we have so far is that it’s flexible and does the basic job of scrolling through a set of items.

But you may have noticed a key enhancement in the demo at the start of this article: buttons that navigate through slides. That’s where we’re going to put the CSS down and put our JavaScript hats on to make this work.

First, let’s define buttons on the left and right of the carousel container that, when clicked, scrolls to the previous or next slide, respectively. I’m using simple SVG arrows as components:

// ArrowLeft export const ArrowLeft = ({size = 30, color = '#000000'}) => (   <svg     xmlns="http://www.w3.org/2000/svg"     width={size}     height={size}     viewBox="0 0 24 24"     fill="none"     stroke={color}     strokeWidth="2"     strokeLinecap="round"     strokeLinejoin="round"   >     <path d="M19 12H6M12 5l-7 7 7 7" />   </svg> )  // ArrowRight export const ArrowRight = ({size = 30, color = '#000000'}) => (   <svg     xmlns="http://www.w3.org/2000/svg"     width={size}     height={size}     viewBox="0 0 24 24"     fill="none"     stroke={color}     strokeWidth="2"     strokeLinecap="round"     strokeLinejoin="round"   >     <path d="M5 12h13M12 5l7 7-7 7" />   </svg> )

Now let’s position them on both sides of our carousel:

// Carousel.js <LeftCarouselButton>   <ArrowLeft /> </LeftCarouselButton>  <RightCarouselButton>   <ArrowRight /> </RightCarouselButton>

We’ll sprinkle in some styling that adds absolute positioning to the arrows so that the left arrow sits on the left edge of the carousel and the right arrow sits on the right edge. A few other things are thrown in to style the buttons themselves to look like buttons. Also, we’re playing with the carousel container’s :hover state so that the buttons only show when the user’s cursor hovers the container.

// Carousel.styled.js  // Position and style the buttons export const CarouselButton = styled('button')`   position: absolute;   cursor: pointer;   top: 50%;   z-index: 1;   transition: transform 0.1s ease-in-out;   background: white;   border-radius: 15px;   border: none;   padding: 0.5rem; `  // Display buttons on hover export const LeftCarouselButton = styled(CarouselButton)`   left: 0;   transform: translate(-100%, -50%);    $ {CarouserContainer}:hover & {     transform: translate(0%, -50%);   } ` // Position the buttons to their respective sides export const RightCarouselButton = styled(CarouselButton)`   right: 0;   transform: translate(100%, -50%);    $ {CarouserContainer}:hover & {     transform: translate(0%, -50%);   } `

This is cool. Now we have buttons, but only when the user interacts with the carousel.

But do we always want to see both buttons? It’d be great if we hide the left arrow when we’re at the first slide, and hide the right arrow when we’re at the last slide. It’s like the user can navigate past those slides, so why set the illusion that they can?

I suggest creating a hook that’s responsible for all the scrolling functionality we need, as we’re gonna have a bunch of it. Plus, it’s just good practice to separate functional concerns from our visual component.

First, we need to get the reference to our component so we can get the position of the slides. Let’s do that with ref:

// Carousel.js const ref = useRef() const position = usePosition(ref)  <CarouserContainer>   <CarouserContainerInner ref={ref}>     {children}   </CarouserContainerInner>   <LeftCarouselButton>     <ArrowLeft />   </LeftCarouselButton>   <RightCarouselButton>     <ArrowRight />   </RightCarouselButton> </CarouserContainer>

The ref property is on <CarouserContainerInner> as it contains all our items and will allow us to do proper calculations.

Now let’s implement the hook itself. We have two buttons. To make them work, we need to keep track of the next and previous items accordingly. The best way to do so is to have a state for each one:

// usePosition.js export function usePosition(ref) {   const [prevElement, setPrevElement] = useState(null)   const [nextElement, setNextElement] = useState(null) }

The next step is to create a function that detects the position of the elements and updates the buttons to either hide or display depending on that position.

Let’s call it the update function. We’re gonna put it into React’s useEffect hook because, initially, we want to run this function when the DOM mounts the first time. We need access to our scrollable container which is available to use under the ref.current property. We’ll put it into a separate variable called element and start by getting the element’s position in the DOM.

We’re gonna use getBoundingClientRect() here as well. This is a very helpful function because it gives us an element’s position in the viewport (i.e. window) and allows us to proceed with our calculations.

// usePosition.js  useEffect(() => {   // Our scrollable container   const element = ref.current    const update = () => {     const rect = element.getBoundingClientRect() }, [ref])

We’ve done a heck of a lot positioning so far and getBoundingClientRect() can help us understand both the size of the element — rect in this case — and its position relative to the viewport.

Credit: Mozilla Developer Network

The following step is a bit tricky as it requires a bit of math to calculate which elements are visible inside the container.

First, we need to filter each item by getting its position in the viewport and checking it against the container boundaries. Then, we check if the child’s left boundary is bigger than the container’s left boundary, and the same thing on the right side.

If one of these conditions is met means that our child is visible inside the container. Let’s convert it into the code step-by-step:

  1. We need to loop and filter through all container children. We can use the children property available on each node. So, let’s convert it into an array and filter:
const visibleElements = Array.from(element.children).filter((child) => {}
  1. After that, we need to get the position of each element by using that handy getBoundingClientRect() function once again:
const childRect = child.getBoundingClientRect()
  1. Now let’s bring our drawing to life:
rect.left <= childRect.left && rect.right >= childRect.right

Pulling that together, this is our script:

// usePosition.js const visibleElements = Array.from(element.children).filter((child) => {   const childRect = child.getBoundingClientRect()    return rect.left <= childRect.left && rect.right >= childRect.right })

Once we’ve filtered out items, we need to check whether an item is the first or the last one so we know to hide the left or right button accordingly. We’ll create two helper functions that check that condition using previousElementSibling and nextElementSibling. This way, we can see if there is a sibling in the list and whether it’s an HTML instance and, if it is, we will return it.

To receive the first element and return it, we need to take the first item from our visible items list and check if it contains the previous node. We’ll do the same thing for the last element in the list, however, we need to get the last item in the list and check if it contains the next element after itself:

// usePosition.js function getPrevElement(list) {   const sibling = list[0].previousElementSibling    if (sibling instanceof HTMLElement) {     return sibling   }    return sibling }  function getNextElement(list) {   const sibling = list[list.length - 1].nextElementSibling   if (sibling instanceof HTMLElement) {     return sibling   }   return null }

Once we have those functions, we can finally check if there are any visible elements in the list, and then set our left and right buttons into the state:

// usePosition.js  if (visibleElements.length > 0) {   setPrevElement(getPrevElement(visibleElements))   setNextElement(getNextElement(visibleElements)) }

Now we need to call our function. Moreover, we want to call this function each time we scroll through the list — that’s when we want to detect the position of the element.

// usePosition.js export function usePosition(ref) {   const [prevElement, setPrevElement] = useState(null)   const [nextElement, setNextElement] = useState(null)   useEffect(() => {     const element = ref.current     const update = () => {       const rect = element.getBoundingClientRect()       const visibleElements = Array.from(element.children).filter((child) => {         const childRect = child.getBoundingClientRect()         return rect.left <= childRect.left && rect.right >= childRect.right       })       if (visibleElements.length > 0) {         setPrevElement(getPrevElement(visibleElements))         setNextElement(getNextElement(visibleElements))       }     }      update()     element.addEventListener('scroll', update, {passive: true})     return () => {       element.removeEventListener('scroll', update, {passive: true})     }   }, [ref])

Here’s an explanation for why we’re passing {passive: true} in there.

Now let’s return those properties from the hook and update our buttons accordingly:

// usePosition.js return {   hasItemsOnLeft: prevElement !== null,   hasItemsOnRight: nextElement !== null, }
// Carousel.js  <LeftCarouselButton hasItemsOnLeft={hasItemsOnLeft}>   <ArrowLeft /> </LeftCarouselButton>  <RightCarouselButton hasItemsOnRight={hasItemsOnRight}>   <ArrowRight /> </RightCarouselButton>
// Carousel.styled.js export const LeftCarouselButton = styled(CarouselButton)`   left: 0;   transform: translate(-100%, -50%);   $ {CarouserContainer}:hover & {     transform: translate(0%, -50%);   }   visibility: $ {({hasItemsOnLeft}) => (hasItemsOnLeft ? `all` : `hidden`)}; ` export const RightCarouselButton = styled(CarouselButton)`   right: 0;   transform: translate(100%, -50%);   $ {CarouserContainer}:hover & {     transform: translate(0%, -50%);   }   visibility: $ {({hasItemsOnRight}) => (hasItemsOnRight ? `all` : `hidden`)}; `

So far, so good. As you’ll see, our arrows show up dynamically depending on our scroll location in the list of items.

We’ve got just one final step to go to make the buttons functional. We need to create a function that’s gonna accept the next or previous element it needs to scroll to.

const scrollRight = useCallback(() => scrollToElement(nextElement), [   scrollToElement,   nextElement, ]) const scrollLeft = useCallback(() => scrollToElement(prevElement), [   scrollToElement,   prevElement, ])

Don’t forget to wrap functions into the useCallback hook in order to avoid unnecessary re-renders.

Next, we’ll implement the scrollToElement function. The idea is pretty simple. We need to take the left boundary of our previous or next element (depending on the button that’s clicked), sum it up with the width of the element, divided by two (center position), and offset this value by half of the container width. That will give us the exact scrollable distance to the center of the next/previous element.

Here’s that in code:

// usePosition.js   const scrollToElement = useCallback(   (element) => {     const currentNode = ref.current      if (!currentNode || !element) return      let newScrollPosition      newScrollPosition =       element.offsetLeft +       element.getBoundingClientRect().width / 2 -       currentNode.getBoundingClientRect().width / 2      currentNode.scroll({       left: newScrollPosition,       behavior: 'smooth',     })   },   [ref], )

scroll actually does the scrolling for us while passing the precise distance we need to scroll to. Now let’s attach those functions to our buttons.

// Carousel.js   const {   hasItemsOnLeft,   hasItemsOnRight,   scrollRight,   scrollLeft, } = usePosition(ref)  <LeftCarouselButton hasItemsOnLeft={hasItemsOnLeft} onClick={scrollLeft}>   <ArrowLeft /> </LeftCarouselButton>  <RightCarouselButton hasItemsOnRight={hasItemsOnRight} onClick={scrollRight}>   <ArrowRight /> </RightCarouselButton>

Pretty nice!

Like a good citizen, we ought to clean up our code a bit. For one, we can be more in control of the passed items with a little trick that automatically sends the styles needed for each child. The Children API is pretty rad and worth checking out.

<CarouserContainerInner ref={ref}>   {React.Children.map(children, (child, index) => (     <CarouselItem key={index}>{child}</CarouselItem>   ))} </CarouserContainerInner>

Now we just need to update our styled components. flex: 0 0 auto preserves the original sizes of the containers, so it’s totally optional

export const CarouselItem = styled('div')`   flex: 0 0 auto;    // Spacing between items   margin-left: 1rem; `
export const CarouserContainerInner = styled(Flex)`   overflow-x: scroll;   scroll-snap-type: x mandatory;   -ms-overflow-style: none;   scrollbar-width: none;   margin-left: -1rem; // Offset for children spacing    &::-webkit-scrollbar {     display: none;   }    $ {CarouselItem} & {     scroll-snap-align: center;   } `

Accessibility 

We care about our users, so we need to make our component not only functional, but also accessible so folks feel comfortable using it. Here are a couple things I’d suggest:

  • Adding role='region' to highlight the importance of this area.
  • Adding an area-label as an identifier.
  • Adding labels to our buttons so screen readers could easily identify them as “Previous” and “Next” and inform the user which direction a button goes.
// Carousel.js <CarouserContainer role="region" aria-label="Colors carousel">    <CarouserContainerInner ref={ref}>     {React.Children.map(children, (child, index) => (       <CarouselItem key={index}>{child}</CarouselItem>     ))}   </CarouserContainerInner>      <LeftCarouselButton hasItemsOnLeft={hasItemsOnLeft}     onClick={scrollLeft}     aria-label="Previous slide   >     <ArrowLeft />   </LeftCarouselButton>      <RightCarouselButton hasItemsOnRight={hasItemsOnRight}     onClick={scrollRight}     aria-label="Next slide"    >     <ArrowRight />   </RightCarouselButton>  </CarouserContainer>

Feel free to add additional carousels to see how it behaves with the different size items. For example, let’s drop in a second carousel that’s just an array of numbers.

const numbersArray = Array.from(Array(10).keys()).map((number) => (   <Item size={5} style={{color: 'black'}} key={number}>     {number}   </Item> ))  function App() {   return (     <Container>       <H1>Easy Carousel</H1>       <HorizontalCenter>         <Carousel>{colorsArray}</Carousel>       </HorizontalCenter>        <HorizontalCenter>         <Carousel>{numbersArray}</Carousel>       </HorizontalCenter>     </Container>   ) }

And voilà, magic! Dump a bunch of items and you’ve got fully workable carousel right out of the box.


Feel free to modify this and use it in your projects. I sincerely hope that this is a good starting point to use as-is, or enhance it even further for a more complex carousel. Questions? Ideas? Contact me on Twitter, GitHub, or the comments below!


The post A Super Flexible CSS Carousel, Enhanced With JavaScript Navigation appeared first on CSS-Tricks.

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

CSS-Tricks

, , , , ,
[Top]

Use CSS Clamp to create a more flexible wrapper utility

I like Andy’s idea here:

.wrapper {   width: clamp(16rem, 90vw, 70rem);   margin-left: auto;   margin-right: auto;   padding-left: 1.5rem;   padding-right: 1.5rem; }

Normally I’d just set a max-width there, but as Andy says:

This becomes a slight issue in mid-sized viewports, such as tablets in portrait mode, in long-form content, such as this article because contextually, the line-lengths feel very long.

So, on super large screens, you’ll get capped at 70rem (or whatever you think a good maximum is), and on small screens you’ll get full width, which is fine. But it’s those in-betweens that aren’t so great. I made a little demo to get a feel for it. This video makes it clear I think:

Direct Link to ArticlePermalink


The post Use CSS Clamp to create a more flexible wrapper utility appeared first on CSS-Tricks.

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

CSS-Tricks

, , , , ,
[Top]

Building Flexible Components With Transparency

Good thinking from Paul Herbert on the Cloudfour blog about colorizing a component. You might look at a design comp and see a card component with a header background of #dddddd, content background of #ffffff, on an overall background of #eeeeee. OK, easy enough. But what if the overall background becomes #dddddd? Now your header looks lost within it.

That darker header? Design-wise, it’s not being exactly #dddddd that’s important; it’s about looking slightly darker than the background. When that’s the case, a background of, say rgba(0, 0, 0, 0.135) is more resiliant.

That will then remain resilient against backgrounds of any kind.

Direct Link to ArticlePermalink


The post Building Flexible Components With Transparency appeared first on CSS-Tricks.

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

CSS-Tricks

, , ,
[Top]

Flexible Repeating SVG Masks

, ,
[Top]

Flexible Captioned Slanted Images

The end result of Eric Meyer’s tutorial on creating this row of slanted images is pretty classy. But it’s more about the journey than the destination (there isn’t even really an isolated demo for it). Eric does an amazing job at talking it through like a thought process.

We did that recently, only ours was sort of fake/generic which Eric’s was for a real-world design.

  • This is a row of boxes, so flexbox. Eric pondered if grid would have been as good or better of a choice since the widths are known and either can be made to accept more/less boxes without adjustment. I agree it’s a tough call here.
  • Since the image dimensions being manipulated, object-fit is a must, and the less-used object-position is used here to help with a focal point.
  • The captions are just pushed to the bottom of the boxes naturally by the images.
  • The slanting is done with clip-path, but it involves some trickery. The boxes need to be enlarged to clip without leaving blank space, then pulled together with negative margin. Percentages are used all around to keep things flexy.
  • Still more tweaks are needed to keep from clipping the captions, and then there is still opportunity for more clever design bits.

Sad that this is probably the last time I’ll link to 24 ways.

Direct Link to ArticlePermalink

The post Flexible Captioned Slanted Images appeared first on CSS-Tricks.

CSS-Tricks

, , ,
[Top]

Making width and flexible items play nice together

The short answer: flex-shrink and flex-basis are probably what you’re lookin’ for.

The long answer

Let’s say you want to align an image and some text next to each other with like this:

A small image of a yellow illustrated bird to the left of a block of text.

Now let’s say you reach for flexbox to make it happen. Setting the parent element to display: flex; is a good first start.

.container {    display: flex;  }

And this results in…

See the Pen
Flex-Shrink Example 1
by Robin Rendle (@robinrendle)
on CodePen.

Yikes! Well, that’s kinda okay, I guess. It makes sense that the image would bump right up against the text like that because we haven’t set a width on the image. Ideally, though, we’d like that image to have a fixed width and then the text should take up whatever space is left over.

Okay, so let’s go do that!

.container {    display: flex;  }  img {    width: 50px;   margin-right: 20px;  }

See the Pen
Flex-Shrink Example 2
by Robin Rendle (@robinrendle)
on CodePen.

This looks great in Chrome. But wait, what? If we inspect the image tag in Firefox DevTools, we’ll find that it’s not the width value that we set at all:

We could use min-width to force the image to the 50px width we want:

img {   min-width: 50px;   margin-right: 20px; }

Buuuuuuut, that only sets helps with the width so we’ve got to put a margin in as well.

img {   min-width: 50px;   margin-right: 20px; }

There we go. That’s better in Firefox and still works in Chrome.

The even longer answer

I realized the image is getting the squished treatment because we need to use the flex-shrink property to tell flex items not to decrease in size, regardless of whether or not they have a width.

All flex-items have a flex-shrink value of 1. We need to set the image element to 0:

.container {    display: flex;  }  img {   width: 50px;   margin-right: 20px;   flex-shrink: 0; }

See the Pen
Flex-Shrink Example 3
by Robin Rendle (@robinrendle)
on CodePen.

Getting better! But we can still do more to improve this.

The director’s cut answer

We can tidy things up further because flex-shrink is included in the flex shorthand property.

flex: none | [ <'flex-grow'> <'flex-shrink'>? || <'flex-basis'> ]

If we set the flex-shrink value to 0 and the flex-basis value to the default width we want the image to be, then we can get rid of the width property altogether.

.container {    display: flex;  }  img {   flex: 0 0 50px;   margin-right: 20px; }

Oh yeah:

See the Pen
Flex-Shrink Example 2
by Geoff Graham (@geoffgraham)
on CodePen.

Another example

That flex-shrink property solves a ton of other problems and is pretty dang important if you want to start using flexbox. Here’s another example why: I stumbled upon yet another problem like the one above and I mentioned it in a recent edition of the newsletter. I was building a navigation component that would let users scroll left and right through multiple items. I noticed the following problem when checking my work:

See the Pen
flex-shrink nav item 1
by Robin Rendle (@robinrendle)
on CodePen.

That longer navigation item shouldn’t break into multiple lines like that — but I finally understood why this was happening, thanks to the previous issue. If you set the flex-shrink property to 0 then it will tell each item in this navigation not to shrink and instead assume the width of the content instead, like this:

See the Pen
flex-shrink nav item
by Robin Rendle (@robinrendle)
on CodePen.

And, yes, we can go the extra step once again to use the flex property instead, this time using auto as the flex-basis since we want the maximum amount of space for all items to be considered when divvying up space in the navigation container.

See the Pen
Setting flex for flexible nav elements
by Geoff Graham (@geoffgraham)
on CodePen.

Huzzah! We figured it out. Even though the answer is a single line of code, it’s is pretty essential one to making truly flexible elements.

CSS-Tricks

, , , , , ,
[Top]

Responsive Designs and CSS Custom Properties: Building a Flexible Grid System

Last time, we looked at a few possible approaches for declaring and using CSS custom properties in responsive designs. In this article, we’ll take a closer look at CSS variables and how to use them in reusable components and modules. We will learn how to make our variables optional and set fallback values.

As an example, we will build a simple grid system based on flexbox. Grid systems play a vital role in responsive designs. However, building a grid system that is flexible and lightweight at the same time can be a tricky task. Let’s see what the common approaches towards grid systems are and how CSS custom properties can help us build them.

Article Series:

  1. Defining Variables and Breakpoints
  2. Building a Flexible Grid System (This Post)

A simple CSS grid system

Let’s start with a 12-column grid system:

.container { 	max-width: 960px; 	margin: 0 auto; 	display: flex; }  .col-1 { flex-basis: 8.333%; } .col-2 { flex-basis: 16.666%; } .col-3 { flex-basis: 25%; } .col-4 { flex-basis: 33.333%; } .col-5 { flex-basis: 41.666%; } .col-6 { flex-basis: 50%; } /* and so on up to 12... */

See the Pen
#5 Building responsive features with CSS custom properties
by Mikołaj (@mikolajdobrucki)
on CodePen.

There’s quite a lot of repetition and hard-coded values here. Not to mention how many more will be generated once we add more breakpoints, offset classes, etc.

Building a grid system with Sass

To make our grid example more readable and maintainable, let’s use Sass to preprocess our CSS:

$ columns: 12; // Number of columns in the grid system  .container { 	display: flex; 	flex-wrap: wrap; 	margin: 0 auto; 	max-width: 960px; }  @for $ width from 1 through $ columns { 	.col-#{$ width} { 		flex-basis: $ width / $ columns * 100%; 	}   }

See the Pen
#6 Building responsive features with CSS custom properties
by Mikołaj (@mikolajdobrucki)
on CodePen.

This is definitely much easier to work with. As we develop our grid further and, let’s say, would like to change it from 12 columns to 16 columns, all we have to do is to update a single variable (in comparison to dozens of classes and values). But… as long as our Sass is shorter and more maintainable now, the compiled code is identical to the first example. We are still going to end up with a massive amount of code in the final CSS file. Let’s explore what happens if we try to replace the Sass variables with CSS custom properties instead.

Building a grid system with CSS custom properties

Before we start playing with CSS custom properties, let’s start with some HTML first. Here’s the layout we’re aiming for:

It consists of three elements: a header, a content section and a sidebar. Let’s create markup for this view, giving each of the elements a unique semantic class (header, content, sidebar) and a column class which indicates that this element is a part of a grid system:

<div class="container"> 	<header class="header column"> 		header 	</header> 	<main class="content column"> 		content 	</main> 	<aside class="sidebar column"> 		sidebar 	</aside> </div>

Our grid system, as before, is based on a 12-column layout. You can envision it as an overlay covering our content areas:

So .header takes all 12 columns, .content takes eight columns (66.(6)% of the total width) and .sidebar takes four columns (33.(3)% of the total width). In our CSS, we would like to be able to control the width of each section by changing a single custom property:

.header { 	--width: 12; }  .content { 	--width: 8; }  .sidebar { 	--width: 4; }

To make it work, all we need to do is write a rule for the .column class. Lucky for us, most of the work is already done! We can re-use the Sass from the previous chapter and replace the Sass variables with CSS custom properties:

.container { 	display: flex; 	flex-wrap: wrap; 	margin: 0 auto; 	max-width: 960px; }  .column { 	--columns: 12; /* Number of columns in the grid system */ 	--width: 0; /* Default width of the element */  	flex-basis: calc(var(--width) / var(--columns) * 100%); }

Notice two important changes here:

  1. The --columns variable is now declared inside of the .column rule. The reason is that this variable is not supposed to be used outside of the scope of this class.
  2. The math equation we perform in the flex-basis property is now enclosed within a calc() function. Math calculations that are written in Sass are compiled by the preprocessor and don’t need additional syntax. calc(), on the other hand, lets us perform math calculations in live CSS. The equation always needs to be wrapped within a calc() function.

On a very basic level, that’s it! We’ve just built a 12-column grid system with CSS custom properties. Congratulations! We could call it a day and happily finish this article right now, but… we usually need a grid system that is a bit more sophisticated. And this is when things are getting really interesting.

See the Pen
#8 Building responsive features with CSS custom properties
by Mikołaj (@mikolajdobrucki)
on CodePen.

Adding a breakpoint to the grid

Most times, we need layouts to look different on various screen sizes. Let’s say that in our case we want the layout to remain as it is on a large viewport (e.g. desktop) but have all three elements become full-width on smaller screens (e.g. mobile).

So, in this case, we would like our variables to look as follows:

.header { 	--width-mobile: 12; }  .content { 	--width-mobile: 12; 	--width-tablet: 8; /* Tablet and larger */ }  .sidebar { 	--width-mobile: 12; 	--width-tablet: 4; /* Tablet and larger */ }

.content and .sidebar each hold two variables now. The first variable (--width-mobile) is a number of columns an element should take by default, and the second one (--width-tablet) is the number of columns an element should take on larger screens. The .header element doesn’t change; it always takes the full width. On larger screens, the header should simply inherit the width it has on mobile.

Now, let’s update our .column class.

CSS variables and fallback

To make the mobile version work as expected, we need to alter the .column class as follows:

.column { 	--columns: 12; /* Number of columns in the grid system */ 	--width: var(--width-mobile, 0); /* Default width of the element */ 	 	flex-basis: calc(var(--width) / var(--columns) * 100%); }

Basically, we replace the value of the --width variable with --width-mobile. Notice that the var() function takes two arguments now. The first of them is a default value. It says: “If a --width-mobile variable exists in a given scope, assign its value to the --width variable.” The second argument is a fallback. In other words: “If a --width-mobile variable is not declared in a given scope, assign this fallback value to the --width variable.” We set this fallback to prepare for a scenario where some grid elements won’t have a specified width.

For example, our .header element has a declared --width-mobile variable which means the --width variable will be equal to it and the flex-basis property of this element will compute to 100%:

.header { 	--width-mobile: 12; }  .column { 	--columns: 12; 	--width: var(--width-mobile, 0); /* 12, takes the value of --width-mobile */ 	 	flex-basis: calc(var(--width) / var(--columns) * 100%); /* 12 ÷ 12 × 100% = 100% */ }

If we remove the --width-mobile variable from the .header rule, then the --width variable will use a fallback value:

.header { 	/* Nothing here... */ }  .column { 	--columns: 12; 	--width: var(--width-mobile, 0); /* 0, takes the the fallback value */ 	 	flex-basis: calc(var(--width) / var(--columns) * 100%); /* 0 ÷ 12 × 100% = 0% */ }

Now, as we understand how to set fallback for CSS custom properties, we can create a breakpoint, by adding a media query to our code:

.column { 	--columns: 12; /* Number of columns in the grid system */ 	--width: var(--width-mobile, 0); /* Default width of the element */ 	 	flex-basis: calc(var(--width) / var(--columns) * 100%); }  @media (min-width: 576px) { 	.column { 		--width: var(--width-tablet); /* Width of the element on tablet and up */ 	} }

This works exactly as expected, but only for the content and sidebar, i.e. for the elements that have specified both --width-mobile and --width-tablet. Why?

The media query we created applies to all .column elements, even those that don’t have a --width-tablet variable declared in their scope. What happens if we use a variable that is not declared? The reference to the undeclared variable in a var() function is then considered invalid at computed-value time, i.e. invalid at the time a user agent is trying to compute it in the context of a given declaration.

Ideally, in such a case, we would like the --width: var(--width-tablet); declaration to be ignored and the previous declaration of --width: var(--width-mobile, 0); to be used instead. But this is not how custom properties work! In fact, the invalid --width-tablet variable will still be used in the flex-basis declaration. A property that contains an invalid var() function always computes to its initial value. So, as flex-basis: calc(var(--width) / var(--columns) * 100%); contains an invalid var() function the whole property will compute to auto (the initial value for flex-basis).

What else we can do then? Set a fallback! As we learned before, a var() function containing a reference to the undeclared variable, computes to its fallback value, as long as it’s specified. So, in this case, we can just set a fallback to the --width-tablet variable:

.column { 	--columns: 12; /* Number of columns in the grid system */ 	--width: var(--width-mobile, 0); /* Default width of the element */ 	 	flex-basis: calc(var(--width) / var(--columns) * 100%); }  @media (min-width: 576px) { 	.column { 		--width: var(--width-tablet, var(--width-mobile, 0)); 	} }

See the Pen
#9 Building responsive features with CSS custom properties
by Mikołaj (@mikolajdobrucki)
on CodePen.

This will create a chain of fallback values, making the --width property use --width-tablet when available, then --width-mobile if --width-tablet is not declared, and eventually, 0 if neither of the variables is declared. This approach allows us to perform numerous combinations:

.section-1 { 	/* Flexible on all resolutions */ }  .section-2 { 	/* Full-width on mobile, half of the container's width on tablet and up */ 	--width-mobile: 12; 	--width-tablet: 6; } 	 .section-3 { 	/* Full-width on all resolutions */ 	--width-mobile: 12; } 	 .section-4 { 	/* Flexible on mobile, 25% of the container's width on tablet and up */ 	--width-tablet: 3; }

One more thing we can do here is convert the default 0 value to yet another variable so we avoid repetition. It makes the code a bit longer but easier to update:

.column { 	--columns: 12; /* Number of columns in the grid system */ 	--width-default: 0; /* Default width, makes it flexible */ 	--width: var(--width-mobile, var(--width-default)); /* Width of the element */ 	 	flex-basis: calc(var(--width) / var(--columns) * 100%); }  @media (min-width: 576px) { 	.column { 		--width: var(--width-tablet, var(--width-mobile, var(--width-default))); 	} }

See the Pen
#10 Building responsive features with CSS custom properties
by Mikołaj (@mikolajdobrucki)
on CodePen.

Now, we have a fully functional, flexible grid! How about adding some more breakpoints?

Adding more breakpoints

Our grid is already quite powerful but we often need more than one breakpoint. Fortunately, adding more breakpoints to our code couldn’t be easier. All we have to do is to re-use the code we already have and add one variable more:

.column { 	--columns: 12; /* Number of columns in the grid system */ 	--width-default: 0; /* Default width, makes it flexible */ 	--width: var(--width-mobile, var(--width-default)); /* Width of the element */ 	 	flex-basis: calc(var(--width) / var(--columns) * 100%); }  @media (min-width: 576px) { 	.column { 		--width: var(--width-tablet, var(--width-mobile, var(--width-default))); 	} }  @media (min-width: 768px) { 	.column { 		--width: var(--width-desktop, var(--width-tablet, var(--width-mobile, var(--width-default)))); 	} }

See the Pen
#11 Building responsive features with CSS custom properties
by Mikołaj (@mikolajdobrucki)
on CodePen.

Reducing fallback chains

One thing that doesn’t look that great in our code is that feedback chains are getting longer and longer with every breakpoint. If we’d like to tackle this issue, we can change our approach to something like this:

.column { 	--columns: 12; /* Number of columns in the grid system */ 	--width: var(--width-mobile, 0); /* Width of the element */ 	 	flex-basis: calc(var(--width) / var(--columns) * 100%); }  @media (min-width: 576px) { 	.column { 		--width-tablet: var(--width-mobile); 		--width: var(--width-tablet); 	} }  @media (min-width: 768px) { 	.column { 		--width-desktop: var(--width-tablet); 		--width: var(--width-desktop); 	} }

See the Pen
#12 Building responsive features with CSS custom properties
by Mikołaj (@mikolajdobrucki)
on CodePen.

This code is doing exactly the same job but in a bit different way. Instead of creating a full fallback chain for each breakpoint, we set a value of each variable to the variable from the previous breakpoint as a default value.

Why so complicated?

It looks like we’ve done quite a lot of work to complete a relatively simple task. Why? The main answer is: to make the rest of our code simpler and more maintainable. In fact, we could build the same layout by using the techniques described in the previous part of this article:

.container { 	display: flex; 	flex-wrap: wrap; 	margin: 0 auto; 	max-width: 960px; }  .column { 	--columns: 12; /* Number of columns in the grid system */ 	--width: 0; /* Default width of the element */  	flex-basis: calc(var(--width) / var(--columns) * 100%); }  .header { 	--width: 12; }  .content { 	--width: 12; }  .sidebar { 	--width: 12; }  @media (min-width: 576px) { 	.content { 		--width: 6; 	} 	 	.sidebar { 		--width: 6; 	} }  @media (min-width: 768px) { 	.content { 		--width: 8; 	} 	 	.sidebar { 		--width: 4; 	} }

In a small project, this approach could work perfectly well. For the more complex solutions, I would suggest considering a more scalable solution though.

Why should I bother anyway?

If the presented code is doing a very similar job to what we can accomplish with preprocessors such as Sass, why should we bother at all? Are custom properties any better? The answer, as usually, is: it depends. An advantage of using Sass is better browser support. However, using custom properties has a few perks too:

  1. It’s plain CSS. In other words, it’s a more standardized, dependable solution, independent from any third parties. No compiling, no package versions, no weird issues. It just works (apart from the browsers where it just doesn’t work).
  2. It’s easier to debug. That’s a questionable one, as one may argue that Sass provides feedback through console messages and CSS does not. However, you can’t view and debug preprocessed code directly in a browser, whilst working with CSS variables, all the code is available (and live!) directly in DevTools.
  3. It’s more maintainable. Custom properties allow us to do things simply impossible with any preprocessor. It allows us to make our variables more contextual and, therefore, more maintainable. Plus, they are selectable by JavaScript, something Sass variables are not.
  4. It’s more flexible. Notice, that the grid system we’ve built is extremely flexible. Would you like to use a 12-column grid on one page and a 15-column grid on another? Be my guest—it’s a matter of a single variable. The same code can be used on both pages. A preprocessor would require generating code for two separate grid systems.
  5. It takes less space. As long as the weight of CSS files is usually not the main bottleneck of page load performance, it still goes without saying that we should aim to optimize CSS files when possible. To give a better image of how much can be saved, I made a little experiment. I took the grid system from Bootstrap and rebuilt it from scratch with custom properties. The results are as follows: the basic configuration of the Bootstrap grid generates over 54KB of CSS whilst a similar grid made with custom properties is a mere 3KB. That’s a 94% difference! What is more, adding more columns to the Bootstrap grid makes the file even bigger. With CSS variables, we can use as many columns as we want without affecting the file size at all.

The files can be compressed to minimize the difference a bit. The gzipped Bootstrap grid takes 6.4KB in comparison to 0.9KB for the custom properties grid. This is still an 86% difference!

Performance of CSS variables

Summing up, using CSS custom properties has a lot of advantages. But, if we are making the browser do all the calculations that had been done by preprocessors, are we negatively affecting the performance of our site? It’s true that using custom properties and calc() functions will use more computing power. However, in cases similar to the examples we discussed in this article, the difference will usually be unnoticeable. If you’d like to learn more about this topic, I would recommend reading this excellent article by Lisi Linhart.

Not only grid systems

After all, understanding the ins and outs of custom properties may not be as easy as it seems. It will definitely take time, but it’s worth it. CSS variables can be a huge help when working on reusable components, design systems, theming and customizable solutions. Knowing how to deal with fallback values and undeclared variables may turn out to be very handy then.

Thanks for reading and good luck on your own journey with CSS custom properties!

The post Responsive Designs and CSS Custom Properties: Building a Flexible Grid System appeared first on CSS-Tricks.

CSS-Tricks

, , , , , , ,
[Top]