Tuesday, May 3, 2022
HomeWebsite DesignNested Parts in a Design System | CSS-Tips

Nested Parts in a Design System | CSS-Tips


When making a component-based, front-end infrastructure, one of many largest ache factors I’ve personally encountered is making elements which are each reusable and responsive when there are nested elements inside elements.

Take the next “name to motion” (<CTA />) element, for instance:

On smaller gadgets we wish it to appear to be this:

That is easy sufficient with fundamental media queries. If we’re utilizing flexbox, a media question can change the flex route and makes the button go the complete width. However we run into an issue once we begin nesting different elements in there. For instance, say we’re utilizing a element for the button and it already has a prop that makes it full-width. We are literally duplicating the button’s styling when making use of a media question to the mum or dad element. The nested button is already able to dealing with it!

It is a small instance and it wouldn’t be that unhealthy of an issue, however for different situations it may trigger lots of duplicated code to duplicate the styling. What if sooner or later we needed to vary one thing about how full-width buttons are styled? We’d have to undergo and alter it in all these completely different locations. We must always be capable of change it within the button element and have that replace in every single place.

Wouldn’t or not it’s good if we may transfer away from media queries and have extra management of the styling? We needs to be utilizing a element’s present props and be capable of go completely different values primarily based on the display width.

Effectively, I’ve a means to try this and can present you the way I did it.

I’m conscious that container queries can clear up lots of these points, nevertheless it’s nonetheless in early days and doesn’t clear up the problem with passing a wide range of props primarily based on display width.

Monitoring the window width

First, we have to observe the present width of the web page and set a breakpoint. This may be completed with any front-end framework, however I’m utilizing a Vue composable right here as to reveal the concept:

// 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.worth = bps.xl
    } else if(windowWidth >= 992) {
      currentBreakpoint.worth = bps.lg
    } else if(windowWidth >= 768) {
      currentBreakpoint.worth = bps.md
    } else if(windowWidth >= 576) {
      currentBreakpoint.worth = bps.sm
    } else {
      currentBreakpoint.worth = bps.xs
    }
  }

  return {
    currentBreakpoint: readonly(currentBreakpoint),
    bps: readonly(bps),
    updateBreakpoint,
  };
};

The rationale we’re utilizing numbers for the currentBreakpoint object will change into clear later.

Now we are able to pay attention for window resize occasions and replace the present breakpoint utilizing the composable in the primary App.vue file:

// App.vue

<script>
import useBreakpoints from "@/composables/useBreakpoints";
import { onMounted, onUnmounted } from 'vue'

export default {
  identify: 'App',
  
  setup() {
    const { updateBreakpoint } = useBreakpoints()

    onMounted(() => {
      updateBreakpoint();
      window.addEventListener('resize', updateBreakpoint)
    })

    onUnmounted(() => {
      window.removeEventListener('resize', updateBreakpoint)
    })
  }
}
</script>

We most likely need this to be debounced, however I’m retaining issues easy for brevity.

Styling elements

We will replace the <CTA /> element to simply accept a brand new prop for the way it needs to be styled:

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

The naming right here is completely arbitrary. You should utilize no matter names you’d like for every of the element modes.

We will then use this prop to vary the mode primarily based on the present breakpoint:

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

You possibly can see now why we’re utilizing a quantity to symbolize the present breakpoint — it’s so the proper mode may be utilized to all breakpoints under or above a sure quantity.

We will then use this within the CTA element to fashion in accordance with the mode handed by:

// elements/CTA.vue

<template>
  <div class="cta" :class="displayMode">
    
    <div class="cta-content">
      <h5>title</h5>
      <p>description</p>
    </div>
    
    <Btn :block="displayMode === 'compact'">Proceed</Btn>
    
  </div>
</template>

<script>
import Btn from "@/elements/ui/Btn";
export default {
  identify: "CTA",
  elements: { Btn },
  props: {
    displayMode: {
      sort: String,
      default: "default"
    },
  }
}
</script>

<fashion scoped lang="scss">
.cta {
  show: flex;
  align-items: middle;
  
  .cta-content {
    margin-right: 2rem;
  }

  &.compact {
    flex-direction: column;
    .cta-content {
      margin-right: 0;
      margin-bottom: 2rem;
    }
  }
}
</fashion>

Already, we now have eliminated the necessity for media queries! You possibly can see this in motion on a demo web page I created.

Admittedly, this may increasingly seem to be a prolonged course of for one thing so easy. However when utilized to a number of elements, this method can massively enhance the consistency and stability of the UI whereas decreasing the full quantity of code we have to write. This manner of utilizing JavaScript and CSS courses to regulate the responsive styling additionally has one other profit…

Extensible performance for nested elements

There have been situations the place I’ve wanted to revert again to a earlier breakpoint for a element. For instance, if it takes up 50% of the display, I need it displayed within the small mode. However at a sure display dimension, it turns into full-width. In different phrases, the mode ought to change somehow when there’s a resize occasion.

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

I’ve additionally been in conditions the place the identical element is utilized in completely different modes on completely different pages. This isn’t one thing that frameworks like Bootstrap and Tailwind can do, and utilizing media queries to tug it off could be a nightmare. (You possibly can nonetheless use these frameworks utilizing this method, simply with out the necessity for the responsive courses they supply.)

We may use a media question that solely applies to center sized screens, however this doesn’t clear up the problem with various props primarily based on display width. Fortunately, the method we’re overlaying can clear up that. We will modify the earlier code to permit for a {custom} mode per breakpoint by passing it by an array, with the primary merchandise within the array being the smallest display dimension.

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

First, let’s replace the props that the <CTA /> element can settle for:

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

We will then add the next to generate to right 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 }
},

That is taking the mode from the array primarily based on the present breakpoint, and defaults to the displayMode if one isn’t discovered. Then we are able to use mode as a substitute to fashion the element.

Extraction for reusability

Many of those strategies may be extracted into further composables and mixins that may be reuseD with different elements.

Extracting computed mode

The logic for returning the proper mode may be extracted right 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 may repeat props was through the use of mixins, however there are noticeable drawbacks. Vue 3 permits us to merge these with different props utilizing the identical composable. There’s a small caveat with this, as IDEs appear unable to acknowledge props for autocompletion utilizing this technique. If that is too annoying, you should use a mixin as a substitute.

Optionally, we are able to additionally go {custom} validation to verify we’re utilizing the modes solely obtainable to every element, the place the primary worth handed by to the validator is the default.

// composables/useResponsive.js

// ...

export const withResponsiveProps = (validation, props) => {
  return {
    displayMode: {
      sort: String,
      default: validation[0],
      validator: operate (worth) {
        return validation.indexOf(worth) !== -1
      }
    },
    customMode: {
      sort: [Boolean, Array],
      default: false,
      validator: operate (worth) {
        return worth ? worth.each(mode => validation.contains(mode)) : true
      }
    },
    ...props
  }
}

Now let’s transfer the logic out and import these as a substitute:

// elements/CTA.vue

import Btn from "@/elements/ui/Btn";
import { useResponsive, withResponsiveProps } from "@/composables/useResponsive";

export default {
  identify: "CTA",
  elements: { Btn },
  props: withResponsiveProps(['default 'compact'], {
    extraPropExample: {
      sort: String,
    },
  }),
  
  setup(props) {
    const { mode } = useResponsive(props)
    return { mode }
  }
}

Conclusion

Making a design system of reusable and responsive elements is difficult and susceptible to inconsistencies. Plus, we noticed how simple it’s to wind up with a load of duplicated code. There’s a tremendous stability in relation to creating elements that not solely work in lots of contexts, however play effectively with different elements once they’re mixed.

I’m certain you’ve come throughout this kind of scenario in your individual work. Utilizing these strategies can scale back the issue and hopefully make the UI extra steady, reusable, maintainable, and straightforward to make use of.

RELATED ARTICLES

LEAVE A REPLY

Please enter your comment!
Please enter your name here

Most Popular

Recent Comments