DEV Community

Cover image for defineExpose and <style vars> in Vue 3 for component interaction and theming
Megan Lee for LogRocket

Posted on • Originally published at blog.logrocket.com

defineExpose and <style vars> in Vue 3 for component interaction and theming

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>
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

In this setup, both ThemedButton components use the globally exposed theme from the parent. When the toggleThememethod 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>
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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:

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 Vue Example Demonstration

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)