Vue.js Style Provider Pattern

  You block advertising 😢
Would you like to buy me a ☕️ instead?

I recently played around with the idea of using renderless provider components not only for data but for styles too. This pattern seems especially promising when it comes to building base components with style modifier props.

<template>
  <BaseCard class="ArticleTeaser">
    <BaseCardImage src="..." alt="...">
    <BaseCardBody padding="['m', 'l@m']">
      <!-- ... -->
    </BaseCardBody>
  </BaseCard>
</template>

In the example code snippet above, you can see that we use BaseCard components to build an ArticleTeaser component. The BaseCardBody has a padding of m (medium) by default and l (large) starting from the m (medium) breakpoint. The padding property is a style modifier prop.

Padding Style Provider Component

In the following example, you can see a simplified version of a ProvideStylePadding component. You can take a closer look at it at GitHub if you want to see a more advanced example, making it possible to set the padding for different sides individually.

<template>
  <slot
    :$$padding="{
      class: $style.root,
      style,
    }"
  />
</template>

<script>
import { spacings } from '../style.config.json';

const RESPONSIVE_SUFFIX_SEPARATOR = `@`;
const VARIABLE_BREAKPOINT_SEPARATOR = `-bp-`;

export default {
  name: 'ProvideStylePadding',
  props: {
    padding: {
      default: ['m'],
      type: Array,
    },
  },
  setup(props) {
    let style = {};
  
    for (let propertyValue of props.padding) {
      let [option, breakpoint] = propertyValue.split(RESPONSIVE_SUFFIX_SEPARATOR);
      let name = [`--padding`, breakpoint]
        .filter(x => x) // Remove `undefined` breakpoint.
        .join(VARIABLE_BREAKPOINT_SEPARATOR);

      style[name] = spacings[option];
    }

    return { style };
  },
};
</script>

<style lang="scss" module>
.root {
  /**
   * Default values.
   * These are overwritten if they are
   * explicitly set in the styles.
   */
  --padding-bp-s: var(--padding);
  --padding-bp-m: var(--padding-bp-s);
  --padding-bp-l: var(--padding-bp-m);

  padding: var(--padding);

  @media (min-width: 376px) {
    padding: var(--padding-bp-s);
  }

  @media (min-width: 768px) {
    padding: var(--padding-bp-m);
  }

  @media (min-width: 1024px) {
    padding: var(--padding-bp-l);
  }
}
</style>

The component you can see above might look complicated, but it helps us to achieve a few fantastic things.

  • It serves as an abstraction for responsive style modifier properties.
  • It helps us enforce our design system by only allowing the use of predefined values for padding.
  • No global styles; if we remove the last instance of this component, the styles also disappear from our output bundle.
  • It enables us to reuse (CSS) code without the problems that typically come with globally reusable CSS.
  • Fixed number of lines of code regardless of the number of spacing sizes, thanks to CSS custom properties.

Using Style Provider Components

Now let’s take a look at how we can use our ProvideStylePadding component to build the BaseCardBody component we’ve seen at the beginning.

<template>
  <ProvideStylePadding
    v-slot="{ $$padding }"
    :padding="padding"
  >
    <div
      :class="$$padding.class"
      :style="$$padding.style"
    >
      <slot/>
    </div>
  </ProvideStylePadding>
</template>

<script>
import ProvideStylePadding from './ProvideStylePadding.vue'

export default {
  name: 'BaseCardBody',
  components: {
    ProvideStylePadding,
  },
  props: {
    padding: {
      default: ['m'],
      type: Array,
    },
  },
};
</script>

Instead of implementing the logic for converting responsive style modifier props (e.g., m@l) again and again in every component like BaseCardBody that need such props, we now can use the abstraction in every place where we need it.

<template>
  <BaseCard>
    <BaseCardImage src="..." alt="...">
    <BaseCardBody padding="['m', 'l@s', 'xl@l']">
      <!-- ... -->
    </BaseCardBody>
  </BaseCard>
</template>

In this example, we pass through the padding prop as is. But in your app, you might want to allow only specific padding sizes on card bodies. You can use either TypeScript or prop validation to limit the permitted modifiers.

Wrapping It Up

Although I like the concept, there are also a few downsides to this. Debugging becomes more complicated because of the heavy use of custom properties. Overall, this seems to be a lot of overhead compared to using global CSS classes or even a utility class-based framework.


Do you want to learn how to build advanced Vue.js applications?

Register for the Newsletter of my upcoming book: Advanced Vue.js Application Architecture.



Do you enjoy reading my blog?

You can buy me a ☕️ on Ko-fi!

☕️ Support Me on Ko-fi