Tag: Nested

Avoiding the Pitfalls of Nested Components in a Design System

When creating a component-based, front-end infrastructure, one of the biggest pain points I’ve personally encountered is making components that are both reusable and responsive when there are nested components within components.

Take the following “call to action” (<CTA />) component, for example:

On smaller devices we want it to look like this:

This is simple enough with basic media queries. If we’re using flexbox, a media query can change the flex direction and makes the button go the full width. But we run into a problem when we start nesting other components in there. For example, say we’re using a component for the button and it already has a prop that makes it full-width. We are actually duplicating the button’s styling when applying a media query to the parent component. The nested button is already capable of handling it!

This is a small example and it wouldn’t be that bad of a problem, but for other scenarios it could cause a lot of duplicated code to replicate the styling. What if in the future we wanted to change something about how full-width buttons are styled? We’d need to go through and change it in all these different places. We should be able to change it in the button component and have that update everywhere.

Wouldn’t it be nice if we could move away from media queries and have more control of the styling? We should be using a component’s existing props and be able to pass different values based on the screen width.

Well, I have a way to do that and will show you how I did it.

I am aware that container queries can solve a lot of these issues, but it’s still in early days and doesn’t solve the issue with passing a variety of props based on screen width.

Tracking the window width

First, we need to track the current width of the page and set a breakpoint. This can be done with any front-end framework, but I’m going using a Vue composable here as to demonstrate the idea:

// composables/useBreakpoints.js  import { readonly, ref } from "vue";  const bps = ref({ xs: 0, sm: 1, md: 2, lg: 3, xl: 4 }) const currentBreakpoint = ref(bps.xl);  export default () => {   const updateBreakpoint = () => {        const windowWidth = window.innerWidth;          if(windowWidth >= 1200) {       currentBreakpoint.value = bps.xl     } else if(windowWidth >= 992) {       currentBreakpoint.value = bps.lg     } else if(windowWidth >= 768) {       currentBreakpoint.value = bps.md     } else if(windowWidth >= 576) {       currentBreakpoint.value = bps.sm     } else {       currentBreakpoint.value = bps.xs     }   }    return {     currentBreakpoint: readonly(currentBreakpoint),     bps: readonly(bps),     updateBreakpoint,   }; };

The reason we are using numbers for the currentBreakpoint object will become clear later.

Now we can listen for window resize events and update the current breakpoint using the composable in the main App.vue file:

// App.vue  <script> import useBreakpoints from "@/composables/useBreakpoints"; import { onMounted, onUnmounted } from 'vue'  export default {   name: 'App',      setup() {     const { updateBreakpoint } = useBreakpoints()      onMounted(() => {       updateBreakpoint();       window.addEventListener('resize', updateBreakpoint)     })      onUnmounted(() => {       window.removeEventListener('resize', updateBreakpoint)     })   } } </script>

We probably want this to be debounced, but I’m keeping things simple for brevity.

Styling components

We can update the <CTA /> component to accept a new prop for how it should be styled:

// CTA.vue props: {   displayMode: {     type: String,     default: "default"   } }

The naming here is totally arbitrary. You can use whatever names you’d like for each of the component modes.

We can then use this prop to change the mode based on the current breakpoint:

<CTA :display-mode="currentBreakpoint > bps.md ? 'default' : 'compact'" />

You can see now why we’re using a number to represent the current breakpoint — it’s so the correct mode can be applied to all breakpoints below or above a certain number.

We can then use this in the CTA component to style according to the mode passed through:

// components/CTA.vue  <template>   <div class="cta" :class="displayMode">          <div class="cta-content">       <h5>title</h5>       <p>description</p>     </div>          <Btn :block="displayMode === 'compact'">Continue</Btn>        </div> </template>  <script> import Btn from "@/components/ui/Btn"; export default {   name: "CTA",   components: { Btn },   props: {     displayMode: {       type: String,       default: "default"     },   } } </script>  <style scoped lang="scss"> .cta {   display: flex;   align-items: center;      .cta-content {     margin-right: 2rem;   }    &.compact {     flex-direction: column;     .cta-content {       margin-right: 0;       margin-bottom: 2rem;     }   } } </style>

Already, we have removed the need for media queries! You can see this in action on a demo page I created.

Admittedly, this may seem like a lengthy process for something so simple. But when applied to multiple components, this approach can massively improve the consistency and stability of the UI while reducing the total amount of code we need to write. This way of using JavaScript and CSS classes to control the responsive styling also has another benefit…

Extensible functionality for nested components

There have been scenarios where I’ve needed to revert back to a previous breakpoint for a component. For example, if it takes up 50% of the screen, I want it displayed in the small mode. But at a certain screen size, it becomes full-width. In other words, the mode should change one way or the other when there’s a resize event.

Showing three versions of a call-to-action components with nested components within it.

I’ve also been in situations where the same component is used in different modes on different pages. This isn’t something that frameworks like Bootstrap and Tailwind can do, and using media queries to pull it off would be a nightmare. (You can still use those frameworks using this technique, just without the need for the responsive classes they provide.)

We could use a media query that only applies to middle sized screens, but this doesn’t solve the issue with varying props based on screen width. Thankfully, the approach we’re covering can solve that. We can modify the previous code to allow for a custom mode per breakpoint by passing it through an array, with the first item in the array being the smallest screen size.

<CTA :custom-mode="['compact', 'default', 'compact']" />

First, let’s update the props that the <CTA /> component can accept:

props: {   displayMode: {     type: String,     default: "default"   },   customMode: {     type: [Boolean, Array],     default: false   }, }

We can then add the following to generate to correct mode:

import { computed } from "vue"; import useBreakpoints from "@/composables/useBreakpoints";  // ...  setup(props) {    const { currentBreakpoint } = useBreakpoints()    const mode = computed(() => {     if(props.customMode) {       return props.customMode[currentBreakpoint.value] ?? props.displayMode     }     return props.displayMode   })    return { mode } },

This is taking the mode from the array based on the current breakpoint, and defaults to the displayMode if one isn’t found. Then we can use mode instead to style the component.

Extraction for reusability

Many of these methods can be extracted into additional composables and mixins that can be reuseD with other components.

Extracting computed mode

The logic for returning the correct mode can be extracted into a composable:

// composables/useResponsive.js  import { computed } from "vue"; import useBreakpoints from "@/composables/useBreakpoints";  export const useResponsive = (props) => {    const { currentBreakpoint } = useBreakpoints()    const mode = computed(() => {     if(props.customMode) {       return props.customMode[currentBreakpoint.value] ?? props.displayMode     }     return props.displayMode   })    return { mode } }

Extracting props

In Vue 2, we could repeat props was by using mixins, but there are noticeable drawbacks. Vue 3 allows us to merge these with other props using the same composable. There’s a small caveat with this, as IDEs seem unable to recognize props for autocompletion using this method. If this is too annoying, you can use a mixin instead.

Optionally, we can also pass custom validation to make sure we’re using the modes only available to each component, where the first value passed through to the validator is the default.

// composables/useResponsive.js  // ...  export const withResponsiveProps = (validation, props) => {   return {     displayMode: {       type: String,       default: validation[0],       validator: function (value) {         return validation.indexOf(value) !== -1       }     },     customMode: {       type: [Boolean, Array],       default: false,       validator: function (value) {         return value ? value.every(mode => validation.includes(mode)) : true       }     },     ...props   } }

Now let’s move the logic out and import these instead:

// components/CTA.vue  import Btn from "@/components/ui/Btn"; import { useResponsive, withResponsiveProps } from "@/composables/useResponsive";  export default {   name: "CTA",   components: { Btn },   props: withResponsiveProps(['default 'compact'], {     extraPropExample: {       type: String,     },   }),      setup(props) {     const { mode } = useResponsive(props)     return { mode }   } }

Conclusion

Creating a design system of reusable and responsive components is challenging and prone to inconsistencies. Plus, we saw how easy it is to wind up with a load of duplicated code. There’s a fine balance when it comes to creating components that not only work in many contexts, but play well with other components when they’re combined.

I’m sure you’ve come across this sort of situation in your own work. Using these methods can reduce the problem and hopefully make the UI more stable, reusable, maintainable, and easy to use.


Avoiding the Pitfalls of Nested Components in a Design System originally published on CSS-Tricks. You should get the newsletter.

CSS-Tricks

, , , , ,

How to Create an Animated Chart of Nested Squares Using Masks

We have many well-known chart types: bar, donut, line, pie, you name it. All popular chart libraries support these. Then there are the chart types that do not even have a name. Check out this dreamt-up chart with stacked (nested) squares that can help visualize relative sizes, or how different values compare to one another:

What we’re making

Without any interactivity, creating this design is fairly straightforward. One way to do it is is to stack elements (e.g. SVG <rect> elements, or even HTML divs) in decreasing sizes, where all of their bottom-left corners touch the same point.

But things get trickier once we introduce some interactivity. Here’s how it should be: When we move our mouse over one of the shapes, we want the others to fade out and move away.

We’ll create these irregular shapes using rectangles and masks — literal <svg> with <rect> and <mask> elements. If you are entirely new to masks, you are in the right place. This is an introductory-level article. If you are more seasoned, then perhaps this cut-out effect is a trick that you can take with you.

Now, before we begin, you may wonder if a better alternative to SVG to using custom shapes. That’s definitely a possibility! But drawing shapes with a <path> can be intimidating, or even get messy. So, we’re working with “easier” elements to get the same shapes and effects.

For example, here’s how we would have to represent the largest blue shape using a <path>.

<svg viewBox="0 0 320 320" width="320" height="320">   <path d="M320 0H0V56H264V320H320V0Z" fill="#264653"/> </svg>

If the 0H0V56… does not make any sense to you, check out “The SVG path Syntax: An Illustrated Guide” for a thorough explanation of the syntax.

The basics of the chart

Given a data set like this:

type DataSetEntry = {   label: string;   value: number; };  type DataSet = DataSetEntry[];  const rawDataSet: DataSet = [   { label: 'Bad', value: 1231 },   { label: 'Beginning', value: 6321 },   { label: 'Developing', value: 10028 },   { label: 'Accomplished', value: 12123 },   { label: 'Exemplary', value: 2120 } ];

…we want to end up with an SVG like this:

<svg viewBox="0 0 320 320" width="320" height="320">   <rect width="320" height="320" y="0" fill="..."></rect>   <rect width="264" height="264" y="56" fill="..."></rect>   <rect width="167" height="167" y="153" fill="..."></rect>   <rect width="56" height="56" y="264" fill="..."></rect>   <rect width="32" height="32" y="288" fill="..."></rect> </svg>

Determining the highest value

It will become apparent in a moment why we need the highest value. We can use the Math.max() to get it. It accepts any number of arguments and returns the highest value in a set.

const dataSetHighestValue: number = Math.max(   ...rawDataSet.map((entry: DataSetEntry) => entry.value) );

Since we have a small dataset, we can just tell that we will get 12123.

Calculating the dimension of the rectangles

If we look at the design, the rectangle representing the highest value (12123) covers the entire area of the chart.

We arbitrarily picked 320 for the SVG dimensions. Since our rectangles are squares, the width and height are equal. How can we make 12123 equal to 320? How about the less “special” values? How big is the 6321 rectangle?

Asked another way, how do we map a number from one range ([0, 12123]) to another one ([0, 320])? Or, in more math-y terms, how do we scale a variable to an interval of [a, b]?

For our purposes, we are going to implement the function like this:

const remapValue = (   value: number,   fromMin: number,   fromMax: number,   toMin: number,   toMax: number ): number => {   return ((value - fromMin) / (fromMax - fromMin)) * (toMax - toMin) + toMin; };  remapValue(1231, 0, 12123, 0, 320); // 32 remapValue(6321, 0, 12123, 0, 320); // 167 remapValue(12123, 0, 12123, 0, 320); // 320

Since we map values to the same range in our code, instead of passing the minimums and maximums over and over, we can create a wrapper function:

const valueRemapper = (   fromMin: number,   fromMax: number,   toMin: number,   toMax: number ) => {   return (value: number): number => {     return remapValue(value, fromMin, fromMax, toMin, toMax);   }; };  const remapDataSetValueToSvgDimension = valueRemapper(   0,   dataSetHighestValue,   0,   svgDimension );

We can use it like this:

remapDataSetValueToSvgDimension(1231); // 32 remapDataSetValueToSvgDimension(6321); // 167 remapDataSetValueToSvgDimension(12123); // 320

Creating and inserting the DOM elements

What remains has to do with DOM manipulation. We have to create the <svg> and the five <rect> elements, set their attributes, and append them to the DOM. We can do all this with the basic createElementNS, setAttribute, and the appendChild functions.

Notice that we are using the createElementNS instead of the more common createElement. This is because we are working with an SVG. HTML and SVG elements have different specs, so they fall under a different namespace URI. It just happens that the createElement conveniently uses the HTML namespace! So, to create an SVG, we have to be this verbose:

document.createElementNS('http://www.w3.org/2000/svg', 'svg') as SVGSVGElement;

Surely, we can create another helper function:

const createSvgNSElement = (element: string): SVGElement => {   return document.createElementNS('http://www.w3.org/2000/svg', element); };

When we are appending the rectangles to the DOM, we have to pay attention to their order. Otherwise, we would have to specify the z-index explicitly. The first rectangle has to be the largest, and the last rectangle has to be the smallest. Best to sort the data before the loop.

const data = rawDataSet.sort(   (a: DataSetEntry, b: DataSetEntry) => b.value - a.value );  data.forEach((d: DataSetEntry, index: number) => {   const rect: SVGRectElement = createSvgNSElement('rect') as SVGRectElement;   const rectDimension: number = remapDataSetValueToSvgDimension(d.value);    rect.setAttribute('width', `$ {rectDimension}`);   rect.setAttribute('height', `$ {rectDimension}`);   rect.setAttribute('y', `$ {svgDimension - rectDimension}`);    svg.appendChild(rect); });

The coordinate system starts from the top-left; that’s where the [0, 0] is. We are always going to draw the rectangles from the left side. The x attribute, which controls the horizontal position, defaults to 0, so we don’t have to set it. The y attribute controls the vertical position.

To give the visual impression that all of the rectangles originate from the same point that touches their bottom-left corners, we have to push the rectangles down so to speak. By how much? The exact amount that the rectangle does not fill. And that value is the difference between the dimension of the chart and the particular rectangle. If we put all the bits together, we end up with this:

We already added the code for the animation to this demo using CSS.

Cutout rectangles

We have to turn our rectangles into irregular shapes that sort of look like the number seven, or the letter L rotated 180 degrees.

If we focus on the “missing parts” then we can see they cutouts of the same rectangles we’re already working with.

We want to hide those cutouts. That’s how we are going to end up with the L-shapes we want.

Masking 101

A mask is something you define and later apply to an element. Typically, the mask is inlined in the <svg> element it belongs to. And, generally, it should have a unique id because we have to reference it in order to apply the mask to an element.

<svg>   <mask id="...">     <!-- ... -->   </mask> </svg>

In the <mask> tag, we put the shapes that serve as the actual masks. We also apply the mask attribute to the elements.

<svg>   <mask id="myCleverlyNamedMask">     <!-- ... -->   </mask>   <rect mask="url(#myCleverlyNamedMask)"></rect> </svg>

That’s not the only way to define or apply a mask, but it’s the most straightforward way for this demo. Let’s do a bit of experimentation before writing any code to generate the masks.

We said that we want to cover the cutout areas that match the sizes of the existing rectangles. If we take the largest element and we apply the previous rectangle as a mask, we end up with this code:

<svg viewBox="0 0 320 320" width="320" height="320">   <mask id="theMask">     <rect width="264" height="264" y="56" fill=""></rect>   </mask>   <rect width="320" height="320" y="0" fill="#264653" mask="url(#theMask)"></rect> </svg>

The element inside the mask needs a fill value. What should that be? We’ll see entirely different results based on the fill value (color) we choose.

The white fill

If we use a white value for the fill, then we get this:

Now, our large rectangle is the same dimension as the masking rectangle. Not exactly what we wanted.

The black fill

If we use a black value instead, then it looks like this:

We don’t see anything. That’s because what is filled with black is what becomes invisible. We control the visibility of masks using white and black fills. The dashed lines are there as a visual aid to reference the dimensions of the invisible area.

The gray fill

Now let’s use something in-between white and black, say gray:

It’s neither fully opaque or solid; it’s transparent. So, now we know we can control the “degree of visibility” here by using something different than white and black values which is a good trick to keep in our back pockets.

The last bit

Here’s what we’ve covered and learned about masks so far:

  • The element inside the <mask> controls the dimension of the masked area.
  • We can make the contents of the masked area visible, invisible, or transparent.

We have only used one shape for the mask, but as with any general purpose HTML tag, we can nest as many child elements in there as we want. In fact, the trick to achieve what we want is using two SVG <rect> elements. We have to stack them one on top of the other:

<svg viewBox="0 0 320 320" width="320" height="320">   <mask id="maskW320">     <rect width="320" height="320" y="0" fill="???"></rect>     <rect width="264" height="264" y="56" fill="???"></rect>   </mask>   <rect width="320" height="320" y="0" fill="#264653" mask="url(#maskW320)"></rect> </svg>

One of our masking rectangles is filled with white; the other is filled with black. Even if we know the rules, let’s try out the possibilities.

<mask id="maskW320">   <rect width="320" height="320" y="0" fill="black"></rect>   <rect width="264" height="264" y="56" fill="white"></rect> </mask>

The <mask> is the dimension of the largest element and the largest element is filled with black. That means everything under that area is invisible. And everything under the smaller rectangle is visible.

Now let’s do flip things where the black rectangle is on top:

<mask id="maskW320">   <rect width="320" height="320" y="0" fill="white"></rect>   <rect width="264" height="264" y="56" fill="black"></rect> </mask>

This is what we want!

Everything under the largest white-filled rectangle is visible, but the smaller black rectangle is on top of it (closer to us on the z-axis), masking that part.

Generating the masks

Now that we know what we have to do, we can create the masks with relative ease. It’s similar to how we generated the colored rectangles in the first place — we create a secondary loop where we create the mask and the two rects.

This time, instead of appending the rects directly to the SVG, we append it to the mask:

data.forEach((d: DataSetEntry, index: number) => {   const mask: SVGMaskElement = createSvgNSElement('mask') as SVGMaskElement;    const rectDimension: number = remapDataSetValueToSvgDimension(d.value);   const rect: SVGRectElement = createSvgNSElement('rect') as SVGRectElement;    rect.setAttribute('width', `$ {rectDimension}`);   // ...setting the rest of the attributes...    mask.setAttribute('id', `maskW$ {rectDimension.toFixed()}`);    mask.appendChild(rect);    // ...creating and setting the attributes for the smaller rectangle...    svg.appendChild(mask); });  data.forEach((d: DataSetEntry, index: number) => {     // ...our code to generate the colored rectangles... });

We could use the index as the mask’s ID, but this seems a more readable option, at least to me:

mask.setAttribute('id', `maskW$ {rectDimension.toFixed()}`); // maskW320, masW240, ...

As for adding the smaller rectangle in the mask, we have easy access the value we need because we previously ordered the rectangle values from highest to lowest. That means the next element in the loop is the smaller rectangle, the one we should reference. And we can do that by its index.

// ...previous part where we created the mask and the rectangle...  const smallerRectIndex = index + 1;  // there's no next one when we are on the smallest if (data[smallerRectIndex] !== undefined) {   const smallerRectDimension: number = remapDataSetValueToSvgDimension(     data[smallerRectIndex].value   );   const smallerRect: SVGRectElement = createSvgNSElement(     'rect'   ) as SVGRectElement;    // ...setting the rectangle attributes...    mask.appendChild(smallerRect); }  svg.appendChild(mask);

What is left is to add the mask attribute to the colored rectangle in our original loop. It should match the format we chose:

rect.setAttribute('mask', `url(#maskW$ {rectDimension.toFixed()})`); // maskW320, maskW240, ...

The final result

And we are done! We’ve successfully made a chart that’s made out of nested squares. It even comes apart on mouse hover. And all it took was some SVG using the <mask> element to draw the cutout area of each square.


The post How to Create an Animated Chart of Nested Squares Using Masks appeared first on CSS-Tricks. You can support CSS-Tricks by being an MVP Supporter.

CSS-Tricks

, , , , , ,
[Top]

Nested Media Queries

Using media queries in CSS as part of responsive websites is bread and butter stuff to todays front-end developer. Using preprocessors to make them more comfortable to write and easier to maintain has become common practice as well.

I spent a few months experimenting with a dozen different approaches to media queries in Sass and actually used a few in production. All of them eventually failed to cater for everything I needed to do in an elegant way. So I took what I liked about each of them and created a solution that covered all scenarios I came across.

Why use a preprocessor at all?

That’s a fair question. After all, what’s the point of doing all this if one can simply write media queries using pure CSS? Tidiness and maintainability.

The most common use for media queries is the transformation of a layout based on the browser’s viewport width. You can make a layout adapt in such a way that multiple devices with different screen sizes can enjoy an optimal experience. As a consequence, the expressions used to define the media queries will make reference to the typical screen width of those devices.

So if your code contains 5 media queries that target tablet devices with a width of 768px, you will hardcode that number 5 times, which is something ugly that my OCD would never forgive. First of all, I want my code to be easy to read to the point that anyone understands instantly that a media query is targeting tablet devices just by looking at it – I reckon the word tablet would do that better than 768px.

Also, what if that reference width changes in the future? I hate the idea of replacing it in 5 instances around the code, especially when it’s scattered around multiple files.

A first step would be to store that breakpoint in a variable and use it to construct the media query.

/* Using plain CSS */ @media (min-width: 768px) {    }  /* Using SCSS variables to store breakpoints */ $  breakpoint-tablet: 768px; @media (min-width: $  breakpoint-tablet) {    }

Another reason to write media queries with a preprocessor like Sass is that it can sometimes provide some precious help with the syntax, in particular when writing an expression with a logical or (represented with a comma in CSS).

For example, if you want to target retina devices, the pure CSS syntax starts getting a bit verbose:

/* Plain CSS */ @media (min-width: 768px) and         (-webkit-min-device-pixel-ratio: 2),         (min-width: 768px) and         (min-resolution: 192dpi) {  }  /* Using variables? */ @media (min-width: $  bp-tablet) and ($  retina) { // or #{$  retina}  } 

It does look nicer, but unfortunately it won’t work as expected.

A problem with logic

Because of the way the CSS “or” operator works, I wouldn’t be able to mix the retina conditions with other expressions since a (b or c) would be compiled into (a or b) c and not a b or a c.

$  retina: "(-webkit-min-device-pixel-ratio: 2), (min-resolution: 192dpi)";   // This will generate unwanted results! @media (min-width: 480px) and #{$  retina} {   body {     background-color: red;   } }
/* Not the logic we're looking for */ @media (min-width: 480px) and (-webkit-min-device-pixel-ratio: 2), (min-resolution: 192dpi) {   body {     background-color: red;   } }

I realized I needed something more powerful, like a mixin or a function, to address this. I tried a few solutions.

Dmitry Sheiko’s technique

One I tried was Dmitry Sheiko’s technique, which had a nice syntax and includes Chris’ retina declaration.

// Predefined Break-points $  mediaMaxWidth: 1260px; $  mediaBp1Width: 960px; $  mediaMinWidth: 480px;  @function translate-media-condition($  c) {   $  condMap: (     "screen": "only screen",     "print": "only print",     "retina": "(-webkit-min-device-pixel-ratio: 1.5), (min--moz-device-pixel-ratio: 1.5), (-o-min-device-pixel-ratio: 3/2), (min-device-pixel-ratio: 1.5), (min-resolution: 120dpi)",     ">maxWidth": "(min-width: #{$  mediaMaxWidth + 1})",     "<maxWidth": "(max-width: #{$  mediaMaxWidth})", 		     ">bp1Width": "(min-width: #{$  mediaBp1Width + 1})",     "<bp1Width": "(max-width: #{$  mediaBp1Width})",     ">minWidth": "(min-width: #{$  mediaMinWidth + 1})",     "<minWidth": "(max-width: #{$  mediaMinWidth})"   );   @return map-get( $  condMap, $  c ); }  // The mdia mixin @mixin media($  args...) {   $  query: "";   @each $  arg in $  args {     $  op: "";     @if ( $  query != "" ) {       $  op: " and ";     }     $  query: $  query + $  op + translate-media-condition($  arg);   }   @media #{$  query}  { @content; } }

But the problem with logical disjunction was still there.

.section {   @include media("retina", "<minWidth") {     color: white;   }; }
/* Not the logic we're looking for */ @media (-webkit-min-device-pixel-ratio: 1.5), (min--moz-device-pixel-ratio: 1.5), (-o-min-device-pixel-ratio: 3 / 2), (min-device-pixel-ratio: 1.5), (min-resolution: 120dpi) and (max-width: 480px) {   .section {     background: blue;     color: white;   } }

Landon Schropp’s technique

Landon Schropp’s was my next stop. Landon creates simple named mixins that do specific jobs. Like:

$  tablet-width: 768px; $  desktop-width: 1024px;  @mixin tablet {   @media (min-width: #{$  tablet-width}) and (max-width: #{$  desktop-width - 1px}) {     @content;   } }  @mixin desktop {   @media (min-width: #{$  desktop-width}) {     @content;   } }

He has a single-responsibility retina version as well.

But another problem hit me when I was styling an element that required additional rules on intermediate breakpoints. I didn’t want to pollute my list of global breakpoints with case-specific values just so I could still use the mixin, but I definitely didn’t want to forgo the mixin and go back to using plain CSS and hardcoding things every time I had to use custom values.

/* I didn't want to sometimes have this */ @include tablet {  }  /* And other times this */ @media (min-width: 768px) and (max-width: 950px) {  }

Breakpoint technique

Breakpoint-sass was next on my list, as it supports both variables and custom values in its syntax (and, as a bonus, it’s really clever with pixel ratio media queries).

I could write something like:

$  breakpoint-tablet: 768px;  @include breakpoint(453px $  breakpoint-tablet) {  }  @include breakpoint($  breakpoint-tablet 850px) {  }  /* Compiles to: */ @media (min-width: 453px) and (max-width: 768px) {  }  @media (min-width: 768px) and (max-width: 850px) {  }

Things were looking better, but I personally think that Breakpoint-sass’ syntax feels less natural than Dmitry’s. You can give it a number and it assumes it’s a min-width value, or a number and a string and it assumes a property/value pair, to name just a few of the combinations it supports.

That’s fine and I’m sure it works great once you’re used to it, but I hadn’t given up on finding a syntax that was both simple and as close as possible to the way I orally describe what a media query must target.

Also, if you look at the example above you’ll see that a device with a width of exactly 768px will trigger both media queries, which may not be exactly what we want. So I added the ability to write inclusive and exclusive breakpoints to my list of requirements.

My (Eduardo Bouças’s) technique

This is my take on it.

Clean syntax, dynamic declaration

I’m a fan of Dmitry’s syntax, so my solution was inspired by it. However, I’d like some more flexibility in the way breakpoints are created. Instead of hardcoding the names of the breakpoints in the mixin, I used a multidimensional map to declare and label them.

$  breakpoints: (phone: 640px,                 tablet: 768px,                 desktop: 1024px) !default;  @include media(">phone", "<tablet") { }  @include media(">tablet", "<950px") { }

The mixin comes with a set of default breakpoints, which you can override anywhere in the code by re-declaring the variable $ breakpoints.

Inclusive and exclusive breakpoints

I wanted to have a finer control over the intervals in the expressions, so I included support for the less-than-or-equal-to and greater-than-or-equal-to operators. This way I can use the same breakpoint declaration in two mutually exclusive media queries.

@include media(">=phone", "<tablet") {  }  @include media(">=tablet", "<=950px") {  }  /* Compiles to */ @media (min-width: 640px) and (max-width: 767px) {  }  @media (min-width: 768px) and (max-width: 950px) {  }

Infer media types and handle logic disjunction

Similarly to the breakpoints, there’s a list for media types and other static expressions declared by default (which you can override by setting the variable $ media-expressions). This adds support for optional media types, such as screen or handheld, but it’s also capable of correctly handling expressions with logical disjunctions, such as the retina media query we saw before. The disjunctions are declared as nested lists of strings.

$  media-expressions: (screen: "screen",                      handheld: "handheld",                     retina2x:                      ("(-webkit-min-device-pixel-ratio: 2)",                      "(min-resolution: 192dpi)")) !default;  @include media("screen", ">=tablet") {  }  @include media(">tablet", "<=desktop", "retina2x") {  }  /* Compiles to */ @media screen and (min-width: 768px) {  }  @media (min-width: 769px) and         (max-width: 1024px) and         (-webkit-min-device-pixel-ratio: 2),        (min-width: 769px) and         (max-width: 1024px) and         (min-resolution: 192dpi) {  }

There’s no rocket science under the hood, but the full implementation of the mixin isn’t something I could show in just a few lines of code. Instead of boring you with huge code snippets and neverending comments, I included a Pen with everything working and I’ll briefly describe the process it goes through to construct the media queries.

How it works

  1. The mixin receives multiple arguments as strings and starts by going through each one to figure out if it represents a breakpoint, a custom width, or one of the static media expressions.
  2. If an operator is found, it is extracted and any matching breakpoint will be returned, or else we assume it’s a custom value and cast it to a number (using SassyCast).
  3. If it’s a static media expression, it checks for any or operators and generates all the combinations necessary to represent the disjunction.
  4. The process is repeated for all the arguments and the results will by glued together by the and connector to form the media query expression.

If you’d like to look at the complete Sass for it, it’s here. It’s called include-media on GitHub.

Final thoughts

  • I’m a big fan of this technique to make Sass talk to JavaScript. Because we declare breakpoints as a multidimensional list with their names as keys, exporting them in bulk to JavaScript becomes really straightforward and can be done automatically with just a few lines of code.
  • I’m not trying to put down other people’s solutions and I’m definitely not saying this one is better. I mentioned them to show some of the obstacles I found along the way to my ideal solution, as well as some great things they introduced that inspired my own solution.
  • You might have some concerns about the length and complexity of this implementation. While I understand, the idea behind it is that you download one single file, @import it into your project and start using it without having to touch the source code. Ping me on Twitter though if you have any questions.
  • You can get it from GitHub and you are very welcome to contribute with issues/code/love. I’m sure there’s still a lot we can do to make it better.

Update!

Eduardo made a website for his approach: @include-media.

Direct Link to ArticlePermalink


The post Nested Media Queries appeared first on CSS-Tricks.

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

CSS-Tricks

, ,
[Top]

Smarter Ways to Generate a Deep Nested HTML Structure

Let’s say we want to have the following HTML structure:

<div class='boo'>   <div class='boo'>     <div class='boo'>       <div class='boo'>         <div class='boo'></div>       </div>     </div>   </div> </div>

That’s real a pain to write manually. And the reason why this post was born was being horrified on seeing it generated with Haml like this:

.boo   .boo     .boo       .boo         .boo

There were actually about twenty levels of nesting in the code I saw, but maybe some people are reading thing on a mobile phone, so let’s not fill the entire viewport with boos, even if Halloween is near.

As you can probably tell, manually writing out every level is far from ideal, especially when the HTML is generated by a preprocessor (or from JavaScript, or even a back-end language like PHP). I’m personally not a fan of deep nesting and I don’t use it much myself, but if you’re going for it anyway, then I think it’s worth doing in a manner that scales well and is easily maintainable.

So let’s first take a look at some better solutions for this base case and variations on it and then see some fun stuff done with this kind of deep nesting!

The base solution

What we need here is a recursive approach. For example, with Haml, the following bit of code does the trick:

- def nest(cls, n); -  return '' unless n > 0; -  "<div class='#{cls}'>#{nest(cls, n - 1)}</div>"; end  = nest('👻', 5)

There’s an emoji class in there because we can and because this is just a fun little example. I definitely wouldn’t use emoji classes on an actual website, but in other situations, I like to have a bit of fun with the code I write.

We can also generate the HTML with Pug:

mixin nest(cls, n)   div(class=cls)     if --n       +nest(cls, n)  +nest('👻', 5)

Then there’s also the JavaScript option:

function nest(_parent, cls, n) {   let _el = document.createElement('div'); 	   if(--n) nest(_el, cls, n);    _el.classList.add(cls);   _parent.appendChild(_el) };  nest(document.body, '👻', 5)

With PHP, we can use something like this:

<?php function nest($  cls, $  n) {   echo "<div class='$  cls'>";   if(--$  n > 0) nest($  cls, $  n);   echo "</div>"; }  nest('👻', 5); ?>

Note that the main difference between what each of these produce is related to formatting and white space. This means that targeting the innermost “boo” with .👻:empty is going to work for the Haml, JavaScript and PHP-generated HTML, but will fail for the Pug-generated one.

Adding level indicators

Let’s say we want each of our boos to have a level indicator as a custom property --i, which could then be used to give each of them a different background, for example.

You may be thinking that, if all we want is to change the hue, then we can do that with filter: hue-rotate() and do without level indicators. However, hue-rotate() doesn’t only affect the hue, but also the saturation and lightness. It also doesn’t provide the same level of control as using our own custom functions that depend on a level indicator, --i.

For example, this is something I used in a recent project in order to make background components smoothly change from level to level (the $ c values are polynomial coefficients):

--sq: calc(var(--i)*var(--i)); /* square */ --cb: calc(var(--sq)*var(--i)); /* cube */ --hue: calc(#{$  ch0} + #{$  ch1}*var(--i) + #{$  ch2}*var(--sq) + #{$  ch3}*var(--cb)); --sat: calc((#{$  cs0} + #{$  cs1}*var(--i) + #{$  cs2}*var(--sq) + #{$  cs3}*var(--cb))*1%); --lum: calc((#{$  cl0} + #{$  cl1}*var(--i) + #{$  cl2}*var(--sq) + #{$  cl3}*var(--cb))*1%);  background: hsl(var(--hue), var(--sat), var(--lum));

Tweaking the Pug to add level indicators looks as follows:

mixin nest(cls, n, i = 0)   div(class=cls style=`--i: $  {i}`)     if ++i < n       +nest(cls, n, i)  +nest('👻', 5)

The Haml version is not too different either:

- def nest(cls, n, i = 0); -   return '' unless i < n; -   "<div class='#{cls}' style='--i: #{i}'>#{nest(cls, n, i + 1)}</div>"; end  = nest('👻', 5)

With JavaScript, we have:

function nest(_parent, cls, n, i = 0) {   let _el = document.createElement('div');    _el.style.setProperty('--i', i); 	   if(++i < n) nest(_el, cls, n, i);    _el.classList.add(cls);   _parent.appendChild(_el) };  nest(document.body, '👻', 5)

And with PHP, the code looks like this:

<?php function nest($  cls, $  n, $  i = 0) {   echo "<div class='$  cls' style='--i: $  i'>";   if(++$  i < $  n) nest($  cls, $  n, $  i);   echo "</div>"; }  nest('👻', 5); ?>

A more tree-like structure

Let’s say we want each of our boos to have two boo children, for a structure that looks like this:

.boo   .boo     .boo       .boo       .boo     .boo       .boo       .boo   .boo     .boo       .boo       .boo     .boo       .boo       .boo

Fortunately, we don’t have to change our base Pug mixin much to get this (demo):

mixin nest(cls, n)   div(class=cls)     if --n       +nest(cls, n)       +nest(cls, n)  +nest('👻', 5)

The same goes for the Haml version:

- def nest(cls, n); -   return '' unless n > 0; -   "<div class='#{cls}'>#{nest(cls, n - 1)}#{nest(cls, n - 1)}</div>"; end  = nest('👻', 5)

The JavaScript version requires a bit more effort, but not too much:

function nest(_parent, cls, n) {   let _el = document.createElement('div');      if(n > 1) {     nest(_el, cls, n);     nest(_el, cls, n)   }    _el.classList.add(cls);   _parent.appendChild(_el) };  nest(document.body, '👻', 5)

With PHP, we only need to call the nest() function once more in the if block:

<?php function nest($  cls, $  n) {   echo "<div class='$  cls'>";   if(--$  n > 0) {     nest($  cls, $  n);     nest($  cls, $  n);   }   echo "</div>"; }  nest('👻', 5); ?>

Styling the top level element differently

We could of course add a special .top (or .root or anything similar) class only for the top level, but I prefer leaving this to the CSS:

:not(.👻) > .👻 {   /* Top-level styles*/ }

Watch out!

Some properties, such as transform, filter, clip-path, mask or opacity don’t only affect an element, but also also all of its descendants. Sometimes this is the desired effect and precisely the reason why nesting these elements is preferred to them being siblings.

However, other times it may not be what we want, and while it is possible to reverse the effects of transform and sometimes even filter, there’s nothing we can do about the others. We cannot, for example, set opacity: 1.25 on an element to compensate for its parent having opacity: .8.

Examples!

First off, we have this pure CSS dot loader I recently made for a CodePen challenge:

Here, the effects of the scaling transforms and of the animated rotations add up on the inner elements, as do the opacities.

Next up is this yin and yang dance, which uses the tree-like structure:

For every item, except the outermost one (:not(.☯️) > .☯️), the diameter is equal to half of that of its parent. For the innermost items (.☯️:empty, which I guess we can call the tree leaves), the background has two extra radial-gradient() layers. And just like the first demo, the effects of the animated rotations add up on the inner elements.

Another example would be these spinning candy tentacles:

Each of the concentric rings represents a level of nesting and combines the effects of the animated rotations from all of its ancestors with its own.

Finally, we have this triangular openings demo (note that it’s using individual transform properties like rotate and scale so the Experimental Web Platform features flag needs to be enabled in chrome://flags in order to see it working in Chromium browsers):

Triangular openings (live demo).

This uses a slightly modified version of the basic nesting mixin in order to also set a color on each level:

- let c = ['#b05574', '#f87e7b', '#fab87f', '#dcd1b4', '#5e9fa3']; - let n = c.length;  mixin nest(cls, n)   div(class=cls style=`color: $  {c[--n]}`)     if n       +nest(cls, n)  body(style=`background: $  {c[0]}`)   +nest('🔺', n)

What gets animated here are the individual transform properties scale and rotate. This is done so that we can set different timing functions for them.


The post Smarter Ways to Generate a Deep Nested HTML Structure appeared first on CSS-Tricks.

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

CSS-Tricks

, , , , , ,
[Top]

Careful with Nested `display: grid; height: 100%;`

It’s not every day you can feel CSS be slow at something. Reddit user jgbbrd discovered nesting grid containers that all have 100% height can cause many-seconds of rendering delay. Probably not something you’ll ever have to worry about, but still, interesting. From the comments:

  • What a funny use of the NSFW tag!
  • This is not an issue when using 100vh instead.

Direct Link to ArticlePermalink

The post Careful with Nested `display: grid; height: 100%;` appeared first on CSS-Tricks.

CSS-Tricks

, , , , ,
[Top]