Theming

A theme is a coherent style applied on UI components. Themes make it easier to customize your app's design for a specific brand or app state (for example, 'light mode' and 'dark mode').

Create a base theme

A theme needs a base theme component to apply the same style values on multiple UI components. Run the following to create a new component that will later be used as a base theme for your components:

$bit
Copiedcopy

Then we will:

  1. create design tokens with default values
  2. implement provide/inject pairs for theming
  3. implement the theme provider
  4. advanced: support styles on <body> element
  5. advanced: support native CSS variables

If you are not interested in the details, you can check out the full version of the base theme component directly and skip to the next section.

To be noticed that here we use CSS custom properties (CSS variables) to make the theme work in CSS directly, which is one of the simplest ways. But you can also use other ways instead like SASS variables, etc.

Create design tokens with default values

To create your basic or default theme, start by composing a list of design tokens. These are pairs of style attributes and values which represent all design decisions.

Add a base-theme.ts file to your component, to place your design tokens:

export const DEFAULT_THEME = {
  primary: '#000000',
  secondary: '#333333',
  background: '#FFFFFF',
};
CopiedCopy

Design tokens are usually maintained in a JSON format, to make them compatible across a wide range of technologies.

Implement provide/inject pairs for theming

We can use provide() / inject() pairs to pass the theme object to all components in the app. This is a common pattern in Vue apps.

Let's create a function called setupTheme():

import { ref, provide } from 'vue';

export const setupTheme = (theme) => {
  const themeRef = ref(theme || DEFAULT_THEME);
  provide('theme', themeRef);

  // we will extend this function later
}
CopiedCopy

The setupTheme() function receives a theme object and provides it to all components in the app. If the theme is not provided, it uses the default theme.

Correspondingly, we can create a useTheme() function to inject the theme object into child components:

import { inject } from 'vue';

export const useTheme = () => {
  const themeRef = inject('theme');
  return themeRef;
}
CopiedCopy

Implement the theme provider

Now we can create a theme provider component in base-theme.vue that uses the setupTheme() function to provide the theme object to all components in the app:

<script setup>
import { setupTheme } from './base-theme';

const { theme } = defineProps(['theme']);
setupTheme(theme);

// we will extend this component later
</script>

<template>
  <div class="theme-provider">
    <slot />
  </div>
</template>
CopiedCopy

In the setup script, we use the defineProps() function to define a theme prop. This prop is passed to the setupTheme() function, which provides it to all components in the app.

So further you can use this Vue component as a theme provider. e.g.:

<script setup>
import Theme from '@my-org/my-scope.themes.base-theme';
import App from '...';
const theme = { ... }
</script>

<template>
  <Theme :theme="theme">
    <App />
  </Theme>
</template>
CopiedCopy

At the same time, we can use the useTheme() function to inject the theme object into child components:

<script setup>
import { useTheme } from '@my-org/my-scope.themes.base-theme';
const theme = useTheme();

// we will simplify this component later
</script>

<template>
  <div id="hello-world">
    Hello World
  </div>
</template>

<style>
#hello-world {
  color: v-bind(theme.primary);
  background-color: v-bind(theme.background);
}
</style>
CopiedCopy

Advanced: support styles on <body> element

For some use cases, you may want to apply the theme styles on the <body> element. For example, if you want to apply a dark theme on the entire app, you can add the following to the setupTheme() function:

import { ref, provide, watchEffect } from 'vue';

export const setupTheme = (theme, isGlobal) => {
  const themeRef = ref(theme || DEFAULT_THEME);
  provide('theme', themeRef);

  if (isGlobal) {
    watchEffect(() => {
      document.body.style.color = themeRef.value.primary;
      document.body.style.backgroundColor = themeRef.value.background;
    });
  }

  // we will extend this function later
}
CopiedCopy

At the same time, we can add a global prop to the base-theme.vue component:

<script setup>
import { setupTheme } from './base-theme';

const { theme } = defineProps(['theme', 'global']);
setupTheme(theme, global);

// we will extend this component later
</script>

<template>
  <div class="theme-provider">
    <slot />
  </div>
</template>
CopiedCopy

Now you can add this prop if you want to apply the theme styles on the <body> element:

<script setup>
import Theme from '@my-org/my-scope.themes.base-theme';
import App from '...';
const theme = { ... }
</script>

<template>
  <Theme :theme="theme" global>
    <App />
  </Theme>
</template>
CopiedCopy

Advanced: support native CSS variables

Now you can use setupTheme() and useTheme() in your components via JavaScript. But that's not enough. To make it work in CSS directly, we need to convert the theme object to native CSS variables (CSS custom properties).

In the setupTheme() function, we can convert the theme object to native CSS variables:

import { ref, provide, computed } from 'vue';

export const setupTheme = (theme, isGlobal) => {
  const themeRef = ref(theme || DEFAULT_THEME);
  provide('theme', themeRef);

  if (isGlobal) {
    watchEffect(() => {
      document.body.style.color = themeRef.value.primary;
      document.body.style.backgroundColor = themeRef.value.background;
    });
  }

  const style = computed(() => {
    return {
      '--primary': themeRef.value.primary,
      '--secondary': themeRef.value.secondary,
      '--background': themeRef.value.background
    };
  });

  return style;
}
CopiedCopy

In the base-theme.vue component, we can bind the style object to the style attribute of the root element:

<script setup>
import { setupTheme } from './base-theme';

const { theme, global } = defineProps(['theme', 'global']);
const style = setupTheme(theme, global);
</script>

<template>
  <div :style="style">
    <slot />
  </div>
</template>
CopiedCopy

Now we can use the theme via CSS variables in child components, without any JavaScript:

<template>
  <div id="hello-world">
    Hello World
  </div>
</template>

<style>
#hello-world {
  color: var(--primary);
  background-color: var(--background);
}
</style>
CopiedCopy

Check out the full version

If you want to reuse this base theme, check out the full version component with types on Bit Cloud.

Create a custom theme

Create a new component to serve as the darker theme for your apps:

$bit
Copiedcopy

Let's create an object in a TS file dark-theme.ts with new design tokens for the dark theme:

export const darkTheme = {
  primary: '#ffffff',
  secondary: '#FF0000',
  background: '#000000',
};
CopiedCopy

Then create a Vue file dark-theme.vue as the theme provider:

<script setup lang="ts">
import BaseTheme from '@my-org/my-scope.themes.base-theme';
import { darkTheme } from './dark-theme.ts';
const { global } = defineProps(['global']);
</script>

<template>
  <BaseTheme
    :theme="darkTheme"
    :global="global"
  >
    <slot />
  </BaseTheme>
</template>
CopiedCopy

Make both the design tokens and the theme provider available to other components by exporting them from its index.ts file:

export { darkTheme } from './dark-theme';
export { default } from './dark-theme.vue';
CopiedCopy

Use the custom theme

Use the custom theme in composition preview

With this theme components, you can easily setup different themes in the composition preview.

First, set up the preview wrapper of your vue env:

<script setup>
import Theme from '@my-org/my-scope.themes.base-theme';
</script>

<template>
  <Theme id="my-wrapper">
    <slot />
  </Theme>
</template>

<style scoped>
#my-wrapper {
  /* Size of the preview box */
  min-width: 199px;
  min-height: 122px;

  /* Theme on the wrapper */
  background-color: var(--background);
  color: var(--primary);
}
</style>
CopiedCopy

If you want to reuse this wrapper, check out this component on Bit Cloud.

Then, you can get the theme in the composition preview:

<script setup>
import { useTheme } from '@my-org/my-scope.themes.base-theme';
useTheme().value = myTheme;
</script>

<template>
  <MyComponent>Hello World</MyComponent>
</template>
CopiedCopy

If you want to reuse this composition, check out this component on Bit Cloud.

Use the custom theme in a global themed app

You can also use this theme in a global themed app. e.g.:

<script setup>
import Theme from '@my-org/my-scope.themes.base-theme';
import App from '...';
const theme = { ... }
</script>

<template>
  <Theme :theme="theme" global>
    <App />
  </Theme>
</template>
CopiedCopy

Don't forget to add the global prop.

To see a real example, check out this component on Bit Cloud.

Use the custom theme in a nested themed app

To apply multiple themes in a nested structure, you can use nested <Theme> as well. e.g.:

<script setup>
import Theme from '@my-org/my-scope.themes.base-theme';

import App from '...';
import SectionA from '...';
import SectionB from '...';

const themeRoot = { ... }
const themeA = { ... }
const themeB = { ... }
</script>

<template>
  <Theme :theme="themeRoot">
    <App>
      <Theme :theme="themeA">
        <SectionA />
      </Theme>
      <Theme :theme="themeB">
        <SectionB />
      </Theme>
    </App>
  </Theme>
</template>
CopiedCopy

To see a real example, check out this component on Bit Cloud.