How to use conditional classes in Vue
Using conditional classes in Vue enables dynamic styling that responds to component state, user interactions, and data changes for creating interactive and responsive user interfaces. As the creator of CoreUI, a widely used open-source UI library, I’ve implemented conditional classes in thousands of Vue components for theme switching, state visualization, interactive elements, and responsive layouts across enterprise applications. From my expertise, the most effective approach is to use the v-bind:class directive with object and array syntax. This method provides reactive styling, excellent performance, and clean template syntax while maintaining separation of concerns between logic and presentation.
Use v-bind:class
(or :class
) with object and array syntax to conditionally apply CSS classes based on component state.
<template>
<div class="conditional-classes-demo">
<!-- 1. Object syntax for conditional classes -->
<div
:class="{
active: isActive,
disabled: isDisabled,
'has-error': hasError,
'text-large': isLargeText
}"
class="base-element"
>
Object Syntax Example
</div>
<!-- 2. Array syntax for conditional classes -->
<div
:class="[
'base-class',
isActive ? 'active' : '',
themeClass,
{ 'highlighted': isHighlighted }
]"
>
Array Syntax Example
</div>
<!-- 3. Mixed syntax combining static and dynamic classes -->
<div
class="card"
:class="[
cardVariant,
{
'card--elevated': isElevated,
'card--interactive': isClickable,
'card--loading': isLoading
}
]"
>
Mixed Syntax Card
</div>
<!-- 4. Computed properties for complex conditions -->
<div class="user-avatar" :class="avatarClasses">
<img :src="user.avatar" :alt="user.name">
<div class="status-indicator" :class="statusClasses"></div>
</div>
<!-- 5. Button with multiple states -->
<button
:class="buttonClasses"
:disabled="isDisabled || isLoading"
@click="handleClick"
>
<span v-if="isLoading" class="spinner"></span>
{{ buttonText }}
</button>
<!-- 6. Form field with validation states -->
<div class="form-field" :class="fieldClasses">
<label :class="labelClasses">{{ label }}</label>
<input
v-model="inputValue"
:class="inputClasses"
@focus="handleFocus"
@blur="handleBlur"
@input="validateInput"
>
<div v-if="errorMessage" class="error-message">
{{ errorMessage }}
</div>
</div>
<!-- 7. Navigation menu with active states -->
<nav class="navigation">
<ul class="nav-list">
<li
v-for="item in menuItems"
:key="item.id"
:class="getNavItemClasses(item)"
>
<a
:href="item.href"
:class="getNavLinkClasses(item)"
@click="setActiveItem(item.id)"
>
{{ item.label }}
</a>
</li>
</ul>
</nav>
<!-- 8. Grid layout with responsive classes -->
<div class="grid-container">
<div
v-for="item in gridItems"
:key="item.id"
:class="getGridItemClasses(item)"
>
<div class="grid-item-content" :class="getContentClasses(item)">
{{ item.content }}
</div>
</div>
</div>
<!-- 9. Modal with animation states -->
<div v-if="showModal" class="modal-overlay" :class="modalClasses">
<div class="modal-content" :class="modalContentClasses">
<h2>Modal Title</h2>
<p>Modal content goes here</p>
<button @click="closeModal" :class="closeButtonClasses">Close</button>
</div>
</div>
<!-- 10. Interactive controls -->
<div class="controls">
<label>
<input type="checkbox" v-model="isActive">
Active State
</label>
<label>
<input type="checkbox" v-model="isDisabled">
Disabled State
</label>
<label>
<input type="checkbox" v-model="hasError">
Error State
</label>
<label>
<input type="checkbox" v-model="isLoading">
Loading State
</label>
<select v-model="selectedTheme">
<option value="light">Light Theme</option>
<option value="dark">Dark Theme</option>
<option value="auto">Auto Theme</option>
</select>
<select v-model="selectedSize">
<option value="small">Small</option>
<option value="medium">Medium</option>
<option value="large">Large</option>
</select>
</div>
</div>
</template>
<script>
export default {
name: 'ConditionalClassesDemo',
data() {
return {
isActive: false,
isDisabled: false,
hasError: false,
isLargeText: false,
isHighlighted: false,
isElevated: true,
isClickable: true,
isLoading: false,
isFocused: false,
selectedTheme: 'light',
selectedSize: 'medium',
activeNavItem: 1,
showModal: false,
modalAnimating: false,
inputValue: '',
errorMessage: '',
user: {
name: 'John Doe',
avatar: 'https://via.placeholder.com/100',
status: 'online'
},
menuItems: [
{ id: 1, label: 'Home', href: '/', badge: 0 },
{ id: 2, label: 'Products', href: '/products', badge: 5 },
{ id: 3, label: 'About', href: '/about', badge: 0 },
{ id: 4, label: 'Contact', href: '/contact', badge: 2 }
],
gridItems: [
{ id: 1, content: 'Item 1', featured: true, category: 'primary' },
{ id: 2, content: 'Item 2', featured: false, category: 'secondary' },
{ id: 3, content: 'Item 3', featured: true, category: 'primary' },
{ id: 4, content: 'Item 4', featured: false, category: 'tertiary' }
]
}
},
computed: {
themeClass() {
return `theme--${this.selectedTheme}`
},
cardVariant() {
return `card--${this.selectedSize}`
},
// Complex conditional classes using computed properties
avatarClasses() {
return {
'avatar--small': this.selectedSize === 'small',
'avatar--medium': this.selectedSize === 'medium',
'avatar--large': this.selectedSize === 'large',
'avatar--active': this.isActive,
'avatar--disabled': this.isDisabled
}
},
statusClasses() {
return [
'status',
`status--${this.user.status}`,
{
'status--animated': this.isActive,
'status--large': this.selectedSize === 'large'
}
]
},
buttonClasses() {
return [
'btn',
`btn--${this.selectedSize}`,
`btn--${this.selectedTheme}`,
{
'btn--active': this.isActive,
'btn--disabled': this.isDisabled,
'btn--loading': this.isLoading,
'btn--error': this.hasError
}
]
},
fieldClasses() {
return {
'form-field--focused': this.isFocused,
'form-field--error': this.hasError || this.errorMessage,
'form-field--disabled': this.isDisabled,
'form-field--large': this.selectedSize === 'large'
}
},
labelClasses() {
return [
'form-label',
{
'form-label--active': this.isFocused || this.inputValue,
'form-label--error': this.hasError || this.errorMessage
}
]
},
inputClasses() {
return [
'form-input',
`form-input--${this.selectedSize}`,
{
'form-input--focused': this.isFocused,
'form-input--error': this.hasError || this.errorMessage,
'form-input--valid': this.inputValue && !this.errorMessage
}
]
},
modalClasses() {
return {
'modal--visible': this.showModal,
'modal--animating': this.modalAnimating,
'modal--theme-dark': this.selectedTheme === 'dark'
}
},
modalContentClasses() {
return [
'modal-content',
`modal-content--${this.selectedSize}`,
{
'modal-content--elevated': this.isElevated
}
]
},
closeButtonClasses() {
return [
'btn',
'btn--secondary',
`btn--${this.selectedSize}`
]
},
buttonText() {
if (this.isLoading) return 'Loading...'
if (this.hasError) return 'Error'
if (this.isActive) return 'Active Button'
return 'Click Me'
}
},
methods: {
// Method-based class calculations
getNavItemClasses(item) {
return {
'nav-item': true,
'nav-item--active': this.activeNavItem === item.id,
'nav-item--has-badge': item.badge > 0,
'nav-item--highlighted': item.badge > 3
}
},
getNavLinkClasses(item) {
return [
'nav-link',
{
'nav-link--active': this.activeNavItem === item.id,
'nav-link--disabled': this.isDisabled
}
]
},
getGridItemClasses(item) {
return [
'grid-item',
`grid-item--${item.category}`,
{
'grid-item--featured': item.featured,
'grid-item--large': this.selectedSize === 'large',
'grid-item--interactive': this.isClickable
}
]
},
getContentClasses(item) {
return {
'content--primary': item.category === 'primary',
'content--secondary': item.category === 'secondary',
'content--tertiary': item.category === 'tertiary',
'content--featured': item.featured
}
},
// Event handlers
handleClick() {
if (!this.isDisabled && !this.isLoading) {
this.isActive = !this.isActive
}
},
handleFocus() {
this.isFocused = true
},
handleBlur() {
this.isFocused = false
},
validateInput() {
if (this.inputValue.length < 3) {
this.errorMessage = 'Input must be at least 3 characters'
this.hasError = true
} else {
this.errorMessage = ''
this.hasError = false
}
},
setActiveItem(itemId) {
this.activeNavItem = itemId
},
closeModal() {
this.modalAnimating = true
setTimeout(() => {
this.showModal = false
this.modalAnimating = false
}, 300)
}
}
}
</script>
<style scoped>
/* Base styles */
.base-element {
padding: 16px;
border: 1px solid #ddd;
border-radius: 4px;
transition: all 0.3s ease;
}
.base-element.active {
background-color: #e3f2fd;
border-color: #2196f3;
}
.base-element.disabled {
opacity: 0.5;
pointer-events: none;
}
.base-element.has-error {
border-color: #f44336;
background-color: #ffebee;
}
.base-element.text-large {
font-size: 18px;
font-weight: bold;
}
/* Theme classes */
.theme--light {
background-color: #ffffff;
color: #333333;
}
.theme--dark {
background-color: #333333;
color: #ffffff;
}
/* Button classes */
.btn {
padding: 8px 16px;
border: none;
border-radius: 4px;
cursor: pointer;
transition: all 0.3s ease;
position: relative;
}
.btn--small { padding: 4px 8px; font-size: 12px; }
.btn--medium { padding: 8px 16px; font-size: 14px; }
.btn--large { padding: 12px 24px; font-size: 16px; }
.btn--light {
background-color: #2196f3;
color: white;
}
.btn--dark {
background-color: #1976d2;
color: white;
}
.btn--active {
background-color: #4caf50;
transform: scale(1.05);
}
.btn--disabled {
opacity: 0.5;
cursor: not-allowed;
}
.btn--loading {
opacity: 0.7;
}
.btn--error {
background-color: #f44336;
}
/* Card classes */
.card {
padding: 16px;
border-radius: 8px;
background: white;
transition: all 0.3s ease;
}
.card--small { padding: 8px; }
.card--medium { padding: 16px; }
.card--large { padding: 24px; }
.card--elevated {
box-shadow: 0 4px 8px rgba(0,0,0,0.1);
}
.card--interactive:hover {
transform: translateY(-2px);
box-shadow: 0 8px 16px rgba(0,0,0,0.15);
}
.card--loading {
opacity: 0.7;
pointer-events: none;
}
/* Form classes */
.form-field {
margin-bottom: 16px;
position: relative;
}
.form-field--focused .form-label {
color: #2196f3;
}
.form-field--error .form-label {
color: #f44336;
}
.form-input {
width: 100%;
padding: 8px 12px;
border: 1px solid #ddd;
border-radius: 4px;
transition: all 0.3s ease;
}
.form-input--focused {
border-color: #2196f3;
box-shadow: 0 0 0 2px rgba(33, 150, 243, 0.2);
}
.form-input--error {
border-color: #f44336;
box-shadow: 0 0 0 2px rgba(244, 67, 54, 0.2);
}
.form-input--valid {
border-color: #4caf50;
}
/* Navigation classes */
.nav-item {
position: relative;
}
.nav-item--active .nav-link {
background-color: #2196f3;
color: white;
}
.nav-item--has-badge::after {
content: attr(data-badge);
position: absolute;
top: -8px;
right: -8px;
background: #f44336;
color: white;
border-radius: 50%;
width: 20px;
height: 20px;
display: flex;
align-items: center;
justify-content: center;
font-size: 12px;
}
/* Grid classes */
.grid-item {
padding: 16px;
border: 1px solid #ddd;
border-radius: 4px;
transition: all 0.3s ease;
}
.grid-item--featured {
border-color: #2196f3;
background-color: #e3f2fd;
}
.grid-item--primary { border-left: 4px solid #2196f3; }
.grid-item--secondary { border-left: 4px solid #ff9800; }
.grid-item--tertiary { border-left: 4px solid #9c27b0; }
/* Modal classes */
.modal-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0,0,0,0.5);
display: flex;
align-items: center;
justify-content: center;
opacity: 0;
transition: opacity 0.3s ease;
}
.modal--visible {
opacity: 1;
}
.modal-content {
background: white;
padding: 24px;
border-radius: 8px;
transform: scale(0.9);
transition: transform 0.3s ease;
}
.modal--visible .modal-content {
transform: scale(1);
}
/* Animation classes */
.spinner {
width: 12px;
height: 12px;
border: 2px solid transparent;
border-top: 2px solid currentColor;
border-radius: 50%;
animation: spin 1s linear infinite;
display: inline-block;
margin-right: 8px;
}
@keyframes spin {
to { transform: rotate(360deg); }
}
</style>
Vue’s conditional classes use the :class
directive with object syntax ({ className: condition }
) for simple conditions and array syntax for complex scenarios combining static and dynamic classes. Use computed properties for complex class logic to maintain reactivity and performance. The object syntax is perfect for toggling single classes, while array syntax allows mixing multiple conditions and static classes. Always use kebab-case for CSS class names in templates. Computed properties provide excellent performance as they’re cached and only re-evaluate when dependencies change.
Best Practice Note:
This is the same conditional class approach we use in CoreUI Vue components for dynamic styling and state visualization. Use computed properties for complex class logic, combine object and array syntax for flexibility, maintain consistent naming conventions, leverage CSS transitions for smooth state changes, and test all conditional states to ensure proper styling across different component states.