Written by Clara Ekekenta✏️
As Vue.js applications scale up, component interaction management and dynamic styling can become progressively more difficult. The new defineExpose
feature and the <style vars>
syntax are two powerful features that can address these issues.
defineExpose
enhances component interaction by allowing control over which properties and methods are accessible externally; this reduces code cleaning and maintenance.
Meanwhile, <style vars>
supports dynamic theming so developers can switch across different themes within their applications.
Devs who use defineExpose
and <style vars>
will find their components to be more self-contained and flexible in design. I’ve listed implementation for both, and towards the end of the article you can find more general information about how these two are related to Vue 3.
Prerequisites
The following are required to get the best out of this tutorial:
- NodeJs installed on your local machine
- A good understanding of Vue.js
- Vue CLI installed:
npm install -g @vue/cli
- A new Vue project:
vue create my-app
Implementing defineExpose
Implementing defineExpose
in Vue 3 is hassle-free. You specify which components you want to reveal in the <script setup>
while keeping it structured since you’ll only allow access to the properties or methods that require exposure.
For instance, let's say you want to expose a simple counter component, you'd do it as follows:
<script setup>
import { ref } from 'vue'
const count = ref(0)
const increment = () => count.value++
const theme = ref({
primaryColor: '#42b983',
textColor: '#ffffff'
})
const changeTheme = (newColor, newTextColor) => {
theme.value = { primaryColor: newColor, textColor: newTextColor }
}
defineExpose({
count,
increment,
})
</script>
<template>
<button @click="increment" :style="{ backgroundColor: theme.primaryColor, color: theme.textColor }">
Count: {{ count }}
</button>
</template>
<style :style="{ primaryColor: theme.primaryColor, textColor: theme.textColor }">
button {
background-color: var(--primaryColor);
color: var(--textColor);
padding: 10px 20px;
border: none;
border-radius: 4px;
cursor: pointer;
}
</style>
In the example above, only the count
and increment
variables are revealed to the parent component. If you were to try and access something else related to the parent, it would be concealed. This makes your component API straightforward, disclosing only what is required.
A parent component could then access these exposed properties:
<script setup>
import { ref } from 'vue'
import Counter from './components/AppCounter.vue'
const counterRef = ref(null)
const handleIncrement = () => {
counterRef.value.increment()
}
</script>
<template>
<Counter ref="counterRef" />
<button @click="handleIncrement">Increment from Parent</button>
</template>
Here, the parent uses ref
to access the increment
method exposed via defineExpose
.
Implementing <style vars>
To set up dynamic theming in your <style vars>
, define CSS variables in the style
block of your Vue component, and bind them to reactive properties. These variables will update in sync with the reactive properties whenever those properties change.
For example, when toggling between light and dark themes in your ThemedButton
component:
<script setup>
import { ref } from 'vue'
const lightTheme = {
primaryColor: '#42b983',
textColor: '#fff'
}
const darkTheme = {
primaryColor: '#000000',
textColor: '#ffffff'
}
const theme = ref(darkTheme)
const toggleTheme = () => {
theme.value = theme.value.primaryColor === lightTheme.primaryColor ? darkTheme : lightTheme
}
</script>
<template>
<button :style="{ 'background-color': theme.primaryColor, color: theme.textColor }">
Themed Button
</button>
<button @click="toggleTheme">Toggle Theme</button>
</template>
<style scoped>
button {
padding: 0.5rem 1rem;
border: none;
border-radius: 0.25rem;
font-size: 1rem;
cursor: pointer;
}
</style>
In this example, the theme
property is able to change between lightTheme
and darkTheme
which causes dynamic changes to associated button colors. The toggleTheme
function first checks the current value of theme.value
and switches light and dark themes depending on the value.
The first button has a :style
directive definition, which defines a background using the theme.primaryColor
and theme.textColor
properties to color the button and the text respectively. This makes it possible for the button’s appearance to change according to the current theme.
The Toggle Theme
button performs the action of calling the toggleTheme
function on a click which changes the theme.value
, and as a result, re-renders the component changing button colors.
Integrating defineExpose
with <style vars>
Combining defineExpose
and <style vars>
will make your Vue apps more flexible, allowing you to create components that expose their internal states and methods to a parent component while simultaneously managing dynamic styling.
Parent components can gain control both over the behavior (like state changes) and the appearance (such as theme customization) of child components by using defineExpose
and CSS variables with <style vars>
.
Let's try using defineExpose
and <style vars>
in the real world to better understand this and then check some advanced use cases.
First, let’s look at an example of a button component in which both the state isActive
and the visual theme through CSS variables are exposed to the parent. The parent can control the button’s activity and its appearance dynamically:
<script setup>
import { ref } from 'vue'
const theme = ref({
primaryColor: '#ff6347',
textColor: '#ffffff'
})
const isActive = ref(false)
const toggleActive = () => {
isActive.value = !isActive.value
}
defineExpose({
theme,
isActive,
toggleActive
})
</script>
<template>
<button :class="{ active: isActive }" @click="toggleActive">
Themed Button
</button>
</template>
<style vars="{ primaryColor: theme.primaryColor, textColor: theme.textColor }">
button {
background-color: var(--primaryColor);
color: var(--textColor);
padding: 0.5rem 1rem;
border: none;
border-radius: 0.25rem;
font-size: 1rem;
cursor: pointer;
}
button.active {
border: 2px solid var(--textColor);
}
</style>
In the example above, we use defineExpose
to expose both the theme
object and the toggleActive
method. This allows the parent to manage both the button's state isActive
and theme dynamically as shown below:
<script setup>
import { ref } from 'vue'
import ThemedButton from './ThemedButton.vue'
const themedButtonRef = ref(null)
const globalTheme = ref({
primaryColor: '#4caf50',
textColor: '#ffffff'
})
const toggleTheme = () => {
globalTheme.value = globalTheme.value.primaryColor === '#4caf50'
? { primaryColor: '#ff5722', textColor: '#000000' }
: { primaryColor: '#4caf50', textColor: '#ffffff' }
}
</script>
<template>
<div>
<ThemedButton ref="themedButtonRef" :theme="globalTheme" />
<button @click="toggleTheme">Toggle Global Theme</button>
</div>
</template>
You may need to manage themes globally across different components in more complex applications. You can achieve this by passing a global theme handler and exposing it to various child components using defineExpose
.
This will allow for smooth theme transitions throughout the application. In practice, this would look like:
<script setup>
import { ref } from 'vue'
import ThemedButton from './ThemedButton.vue'
const globalTheme = ref({
primaryColor: '#4caf50',
textColor: '#ffffff'
})
const toggleTheme = () => {
globalTheme.value = globalTheme.value.primaryColor === '#4caf50'
? { primaryColor: '#ff5722', textColor: '#000000' }
: { primaryColor: '#4caf50', textColor: '#ffffff' }
}
</script>
<template>
<div>
<ThemedButton :theme="globalTheme" />
<ThemedButton :theme="globalTheme" />
<button @click="toggleTheme">Toggle Global Theme</button>
</div>
</template>
In this setup, both ThemedButton
components use the globally exposed theme
from the parent. When the toggleTheme
method is triggered, all instances of ThemedButton
update their appearance, enabling synchronized styling across the app.
For this to work, you need to modify the ThemedButton
component to accept theme
as a prop:
<script setup>
import { ref, defineProps } from 'vue'
const props = defineProps({
theme: {
type: Object,
default: () => ({
primaryColor: '#4caf50',
textColor: '#ffffff'
})
}
})
const isActive = ref(false)
const toggleActive = () => {
isActive.value = !isActive.value
}
defineExpose({
isActive,
toggleActive
})
</script>
<template>
<button :class="{ active: isActive }" @click="toggleActive">
Themed Button
</button>
</template>
<style vars="{ primaryColor: props.theme.primaryColor, textColor: props.theme.textColor }">
button {
background-color: var(--primaryColor);
color: var(--textColor);
padding: 0.5rem 1rem;
border: none;
border-radius: 0.25rem;
font-size: 1rem;
cursor: pointer;
}
button.active {
border: 2px solid var(--textColor);
}
</style>
Sometimes, you might want each component to maintain its state while sharing the same theme. This allows for very granular control over how things look and behave at a component level, for example, allowing some components to be on while others are off, even if they are adapted to the same theme:
<script setup>
import { ref, defineProps } from 'vue'
const props = defineProps({
theme: {
type: Object,
default: () => ({
primaryColor: '#4caf50',
textColor: '#ffffff'
})
}
})
const isActive = ref(false)
const theme = ref({ primaryColor: '#4caf50', textColor: '#ffffff' })
const toggleActive = () => {
isActive.value = !isActive.value
}
// Exposing both the theme and toggleActive method
defineExpose({
theme,
isActive,
toggleActive
})
</script>
<template>
<button :vars="{ '--primary-color': props.primaryColor, '--text-color': props.theme.textColor }"
:class="{ active: isActive }" @click="toggleActive">
Dynamic Themed Button
</button>
</template>
<style scoped>
button {
background-color: var(--primary-color);
color: var(--text-color);
}
button.active {
border: 2px solid var(--text-color);
}
</style>
Here, each instance of ThemedButton
manages its isActive
state, meaning that different components can be toggled on or off while still inheriting the common or global theme from the parent. This is a handy design for apps where different components need to respond to a global theme change while maintaining unique behavior, such as having an on or off-active state.
You also want to give the child multiple themes to choose from; the child components will expose their internal state and methods using defineExpose
, and the parent will dynamically change the theme based on user input or other conditions:
<script setup>
import { ref } from 'vue'
import ThemedButton from './ThemedButton.vue'
const lightTheme = { primaryColor: '#ffffff', textColor: '#000000' }
const darkTheme = { primaryColor: '#000000', textColor: '#ffffff' }
const currentTheme = ref(lightTheme)
const switchTheme = () => {
currentTheme.value = currentTheme.value.primaryColor === lightTheme.primaryColor ? darkTheme : lightTheme
}
</script>
<template>
<ThemedButton :theme="currentTheme" />
<ThemedButton :theme="currentTheme" />
<button @click="switchTheme">Switch Theme</button>
</template>
In the above example, the global theme is controlled by the parent component. The child elements just expose their functionality along with their state, which allows them to react to theme updates flowing through props dynamically. This setup works for applications that allow different themes or user-initiated style modifications.
More information about <style vars>
<style vars>
adds dynamic themes to your Vue.js applications by allowing you to use CSS variables for your styling rather than hardcoding the styles or classes for different component or theme. These CSS variables change based on your app state or user interactions on your app.
For example, if you want to implement light and dark theme in your app, using <style vars>
lets you switch themes without having to rebuild your whole style setup. When you control CSS variables from your Vue component, you can put multiple themes into action in a way that grows with your needs.
How <style vars>
enhances theme management in Vue 3
Dynamic theming can be difficult, especially in large applications with complex component structures. With <style vars>
, Vue 3 simplifies this by allowing you to define CSS variables directly within the component.
These reactive variables ensure that any changes are applied instantly, without the need to reload or re-render the component.
For example, in a themed button component:
<script setup>
import { ref } from 'vue'
const theme = ref({
primaryColor: '#ff6347',
textColor: '#ffffff'
})
defineExpose({
theme
})
</script>
<template>
<button>Themed Button</button>
</template>
<style vars="{ primaryColor: theme.primaryColor, textColor: theme.textColor }">
button {
background-color: var(--primaryColor);
color: var(--textColor);
padding: 0.5rem 1rem;
border: none;
border-radius: 0.25rem;
font-size: 1rem;
cursor: pointer;
}
</style>
Here, we dynamically set CSS variables (--primary-color
and --text-color
) based on the theme
object. Changing theme.primaryColor
will automatically update the button's background color without directly modifying the DOM.
Best practices and tips
Here are some things to keep in mind when you use defineExpose
and <style vars>
:
- Expose only the internal state or functions and functions that are needed outside your components
- Ensure that
defineExpose
does not break encapsulation of your component. Expose only the parts of the component that need interaction - Properly define your CSS variables (
<style vars>
) to maintain the same theme and styles across all your components. Use variable names that are easy to remember and understand for future maintenance - When using
defineExpose
, make sure that your reactive data is carefully managed. Expose reactive properties only when you need to make sure that state updates are reflected wherever required - Avoid using the
<style vars>
tag when applying component specific styles — use scoped styles instead to prevent your changes from affecting other components, which also helps prevent conflicts or unintended overrides in your components - Do not use
defineExpose
and<style vars>
too much because they can lead to performance issues, especially in large applications. Be mindful of what you are exposing and the complexity of your style variables
Conclusion
If you’re looking for more ways to enhance your Vue application, be sure to check out these related LogRocket blog articles:
- Understanding refs in Vue
- Optimizing rendering in Vue
- Reactivity with the Vue 3 Composition API:
ref()
andreactive()
- How to use props to pass data to child components in Vue 3
Experiment with them in your projects and see what you are able to create. Share your thoughts below in the comments.
Experience your Vue apps exactly how a user does
Debugging Vue.js applications can be difficult, especially when there are dozens, if not hundreds of mutations during a user session. If you’re interested in monitoring and tracking Vue mutations for all of your users in production, try LogRocket.
LogRocket is like a DVR for web apps, recording literally everything that happens in your Vue apps including network requests, JavaScript errors, performance problems, and much more. Instead of guessing why problems happen, you can aggregate and report on what state your application was in when an issue occurred.
The LogRocket Vuex plugin logs Vuex mutations to the LogRocket console, giving you context around what led to an error, and what state the application was in when an issue occurred.
Modernize how you debug your Vue apps - Start monitoring for free.
Top comments (0)