Tag: Flexible

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

, , , , ,

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]