Best Practices for the Composition API
Vue 3's Composition API changes how logic gets shared between components, and this shift opens up new patterns for Tailwind CSS in Vue 3 projects. Reactive class lists, reusable composables for variants and clean cooperation with scoped slots turn scattered utility classes into a consistent, maintainable design system.
Table of Contents
- 1. Why the Composition API changes Tailwind styling
- 2. Project setup: setting up Tailwind CSS in Vue 3
- 3. Reactive class lists with computed and :class
- 4. Composables for variants and design tokens
- 5. Scoped slots and controlling utility classes from outside
- 6. Teleport, Transition and Tailwind animations
- 7. Script setup syntax and props typing with classes
- 8. State management with Pinia and UI states in Tailwind
- 9. Composition API versus Options API in the Tailwind context
- 10. Summary
- 11. FAQ
1. Why the Composition API changes Tailwind styling
Vue 3's Composition API organizes logic by domain concern rather than by option type, and this shift has a direct effect on how you work with Tailwind CSS in Vue 3 projects. Instead of spreading styling logic across several Options API blocks such as data, computed and methods, the Composition API lets you bundle all the logic for a component variant in one place, including the associated Tailwind class lists. That makes it easy to see which states trigger which visual effects.
A second important aspect is extracting logic into composables. Where the Options API used mixins with their well known naming conflicts, composables in Tailwind CSS with Vue 3 allow explicit, type safe reuse of styling logic across multiple components. A composable that returns button variants as Tailwind class lists can be imported into dozens of components without naming conflicts or unclear property origins, as was frequently the case with classic mixins.
2. Project setup: setting up Tailwind CSS in Vue 3
Setting up Tailwind CSS in Vue 3 today usually runs through Vite, the default build tool for new Vue projects. The official @tailwindcss/vite plugin gets registered in vite.config.ts, a global CSS file imports Tailwind CSS 4 and defines project specific design tokens through @theme. This configuration is independent of whether single file components are written with <script setup> or classic Composition API syntax.
Also important for Tailwind CSS with Vue 3 is correctly configuring source file detection, so Tailwind also picks up class names inside the template block of .vue files. Since Vue single file components combine template, script and style in one file, the Tailwind scanner needs to be explicitly aware of the .vue file format, which works automatically in current Tailwind CSS 4 versions with the Vite plugin, but had to be added manually to the content configuration in older setups.
# Vue 3 project with Vite and Tailwind CSS 4
npm create vue@latest tailwind-vue-demo
cd tailwind-vue-demo
npm install tailwindcss @tailwindcss/vite
# vite.config.ts
import { defineConfig } from 'vite';
import vue from '@vitejs/plugin-vue';
import tailwindcss from '@tailwindcss/vite';
export default defineConfig({
plugins: [vue(), tailwindcss()],
});
3. Reactive class lists with computed and :class
The core of reactive Tailwind styling in Vue 3 is the combination of :class bindings and computed properties. Instead of nesting several ternary expressions directly into the class name in the template, you extract the logic into a computed property that returns an object or array with the matching class names. This separation makes the template more readable and the styling logic independently testable, which for more complex Tailwind CSS Vue 3 components makes the difference between maintainable and opaque code.
The object syntax of :class, where keys are class names and values are boolean expressions, is particularly well suited for states like active, disabled or errored. For compound variants, for example size combined with color, a computed property that assembles the matching, full class list based on the props is preferable. It remains important here too: the individual class names in this list must appear as complete strings in the source code, so the Tailwind scanner detects them at build time.
<script setup lang="ts">
import { computed } from 'vue';
const props = defineProps<{
variant: 'primary' | 'secondary' | 'danger';
disabled?: boolean;
}>();
// Full class strings, mapped from props — never concatenated at runtime
const VARIANT_CLASSES = {
primary: 'bg-sky-600 text-white hover:bg-sky-700',
secondary: 'bg-slate-100 text-slate-800 hover:bg-slate-200',
danger: 'bg-red-600 text-white hover:bg-red-700',
} as const;
const buttonClasses = computed(() => [
'inline-flex items-center gap-2 rounded-lg px-4 py-2 font-semibold transition-colors',
VARIANT_CLASSES[props.variant],
{ 'opacity-50 cursor-not-allowed': props.disabled },
]);
</script>
<template>
<button :class="buttonClasses" :disabled="disabled">
<slot />
</button>
</template>
4. Composables for variants and design tokens
Composables are Vue 3's equivalent of React hooks and are excellent for reusing Tailwind variant logic across multiple components. A composable like useButtonVariants() encapsulates the complete mapping from props to class names and returns a computed property that can be imported in every component that renders buttons. For Tailwind CSS in Vue 3, this means design system changes, for example a new color for the primary button, only need to be made in a single place.
A further advantage of composables over global CSS classes with @apply is type safety. TypeScript typed composables prevent an invalid variant name like "primry" from slipping through unnoticed, because the compiler reports the typo already at development time. This combination of Tailwind CSS and Vue 3 composables is especially valuable in larger teams where multiple developers work on different components simultaneously but need a consistent visual vocabulary.
// composables/useButtonVariants.ts
import { computed, type Ref } from 'vue';
type Variant = 'primary' | 'secondary' | 'danger';
type Size = 'sm' | 'md' | 'lg';
const VARIANTS: Record<Variant, string> = {
primary: 'bg-sky-600 text-white hover:bg-sky-700',
secondary: 'bg-slate-100 text-slate-800 hover:bg-slate-200',
danger: 'bg-red-600 text-white hover:bg-red-700',
};
const SIZES: Record<Size, string> = {
sm: 'px-3 py-1.5 text-sm',
md: 'px-4 py-2 text-base',
lg: 'px-6 py-3 text-lg',
};
export function useButtonVariants(variant: Ref<Variant>, size: Ref<Size>) {
return computed(() => [
'inline-flex items-center gap-2 rounded-lg font-semibold transition-colors',
VARIANTS[variant.value],
SIZES[size.value],
]);
}
5. Scoped slots and controlling utility classes from outside
Scoped slots allow a child component to pass data back to the parent, which combined with Tailwind CSS in Vue 3 results in a powerful pattern for flexible yet controlled components. A list component can, for example, expose the current index and selection state through a scoped slot, while the parent component computes the matching Tailwind classes for each list item from that. That way, the list's core logic stays generic while the visual appearance depends entirely on the usage context.
A proven pattern for design systems is additionally to equip base components like cards or panels with named slots for header, body and footer areas that carry predefined Tailwind classes, while specific adjustments remain possible through a class prop that gets merged with the base component's default classes. Libraries like tailwind-merge reliably resolve which class actually wins in case of conflicting utility classes, for example when both the base component and the calling component set a background color.
6. Teleport, Transition and Tailwind animations
Vue 3's <Teleport> element renders content at a different location in the DOM tree, typically for modals, tooltips and notifications that need to sit visually above the rest of the application. This is relevant for Tailwind CSS with Vue 3, because a modal rendered via teleport directly under body can be positioned independently of the calling component's stacking context hierarchy, which significantly reduces z-index conflicts that otherwise frequently occur in deeply nested component trees.
Combined with the built in <Transition> element, fade in and fade out effects can be controlled through Tailwind classes for the various transition phases, for example enter-from, enter-active and enter-to. Vue automatically adds and removes these classes at the right moment, while Tailwind merely supplies the visual values for opacity, transform and timing. This combination of Tailwind CSS and Vue 3 transition mechanics produces animated modals and dropdowns without an additional animation library.
7. Script setup syntax and props typing with classes
The <script setup> syntax has by now become the standard for new Vue 3 components and significantly reduces boilerplate compared to the classic Composition API with an explicit setup() call. For Tailwind CSS in Vue 3 components, this means shorter, more focused files where props typing via defineProps with TypeScript generics sits directly next to the styling logic. This spatial proximity makes it easier to keep prop types and the class names derived from them consistent.
A pattern that has proven itself in many Tailwind CSS Vue 3 component libraries is using a discriminated union type for variant props, combined with an as const object for the associated class names. TypeScript then enforces at compile time that every possible variant actually has a matching class mapping, and a new variant value without matching classes immediately shows up as a compiler error, instead of only becoming visible at runtime in the browser.
8. State management with Pinia and UI states in Tailwind
Pinia has replaced Vuex as the standard state management solution for Vue 3 and internally uses the same Composition API philosophy as composables. For UI states that affect multiple components, for example a global dark mode flag or a sidebar collapsed state, a Pinia store is a good fit, its reactive state feeding directly into computed properties for Tailwind CSS in Vue 3 class lists. That way the state logic stays central, while each component locally decides how it reacts visually to that state.
A common example is a theme store that switches between light and dark mode and whose value sets the dark class on the root element via document.documentElement.classList, so that Tailwind's dark: variant applies. This combination of Pinia for state and Tailwind CSS for the visual implementation cleanly separates the question of what the current state is from the question of what that state looks like, which noticeably improves testability especially in larger Tailwind CSS Vue 3 applications.
9. Composition API versus Options API in the Tailwind context
Even though the Options API remains supported in Vue 3, working with Tailwind CSS in Vue 3 reveals a clear difference in the reusability of styling logic between the two approaches.
| Aspect | Options API | Composition API | Benefit |
|---|---|---|---|
| Sharing styling logic | Mixins with naming conflicts | Composables, explicit imports | Clear origin of every property |
| Variant logic | Via computed in an options block | Extracted into a composable | Reusable across components |
| TypeScript integration | Cumbersome with this typing | Native type inference | Fewer type errors on variants |
| Testability | Component must be mounted | Composable testable in isolation | Faster unit tests without DOM |
| Code organization | Separated by option type | Organized by domain concern | Related styling logic lives in one place |
In practice, many teams migrate existing Options API components incrementally, first extracting only the styling relevant computed properties into composables while the rest of the component remains unchanged for the time being. This incremental approach reduces the risk of a full rewrite and delivers noticeable improvements in the reusability of Tailwind CSS Vue 3 styling logic after only a short time.
Mironsoft
Vue 3, Tailwind CSS and maintainable component architecture
Building Vue 3 components with a clean Tailwind system?
We develop Vue 3 applications with the Composition API, composables for variants and a consistent Tailwind CSS design system that stays maintainable as the codebase grows.
Component library
Reusable Vue 3 components with a Tailwind variant system
Options to Composition migration
Incremental migration of existing Vue 2 and Vue 3 projects
Code review
Reviewing composables, typing and Tailwind class logic
10. Summary
Tailwind CSS with Vue 3 and the Composition API produces a pattern where styling logic can be structured just like domain logic: extracted into composables, type safe with TypeScript, and reactively bound to component state through computed properties. Reactive class lists, scoped slots for flexible base components, and the combination of Teleport and Transition for overlays cover most practical requirements without needing an additional animation library.
Anyone developing new Tailwind CSS Vue 3 components should extract variant logic into composables from the start instead of duplicating it across individual components. This investment pays off as soon as multiple components need the same visual patterns, and it turns later design system changes into a single, central adjustment instead of a search across dozens of files.
Tailwind CSS with Vue 3 — The essentials at a glance
Reactive classes
:class with computed properties instead of nested ternary expressions in the template.
Composables
Extract variant logic into typed composables, maintainable centrally across all components.
Teleport & Transition
Render modals via teleport outside the stacking context hierarchy, use Tailwind classes for transition phases.
State management
Pinia for global UI state, Tailwind class lists react to store state through computed.