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').
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:
Then we will:
- create design tokens with default values
- implement provide/inject pairs for theming
- implement the theme provider
- advanced: support styles on
<body>
element - 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.
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', };
Design tokens are usually maintained in a JSON format, to make them compatible across a wide range of technologies.
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 }
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; }
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>
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>
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>
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 }
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>
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>
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; }
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>
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>
If you want to reuse this base theme, check out the full version component with types on Bit Cloud.
Create a new component to serve as the darker theme for your apps:
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', };
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>
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';
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>
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>
If you want to reuse this composition, check out this component on Bit Cloud.
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>
Don't forget to add the global
prop.
To see a real example, check out this component on Bit Cloud.
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>
To see a real example, check out this component on Bit Cloud.