display property (hides or shows it), but the element always remains in the DOM.message changes, Vue ensures the UI reflecting {{ message }} updates instantly without manual DOM manipulation.
v-model creates two-way data binding between form inputs and Vue data. When the input changes, the Vue data updates, and when the Vue data changes, the input reflects it.
<input v-model="username" />
<p>Hello, {{ username }}</p>
{ path: '/about', component: About }
computed: {
fullName() {
return this.firstName + ' ' + this.lastName
}
}
<button @click="increment">Click Me</button>
Here, the increment method will be called when the button is clicked.
v-model provides two-way data binding between form inputs and data.ref provides a direct reference to a DOM element or component, accessible with this.$refs.
watch: {
count(newVal, oldVal) {
console.log(`Count changed from ${oldVal} to ${newVal}`);
}
}
setup(). Instead of separating by options (data, methods, computed), you group by feature. Example:
setup() {
const count = ref(0);
function increment() { count.value++ }
return { count, increment }
}
export default {
data() { return { count: 0 } },
methods: { increment() { this.count++ } }
}
<template> <p>{{ message }}</p> </template>
<script>
export default {
data() { return { message: 'Hello Vue' } }
}
</script>
<style>
p { color: blue; }
</style>
<slot></slot>
It is replaced with the content between component tags.
<slot name="header"></slot>
<slot name="footer"></slot>
You can pass content with <template v-slot:header> in the parent component.
<component> element with :is. Example:
<component :is="currentView"></component>
Here currentView can be "Home", "About", etc.
const About = () => import('./About.vue');
export const myMixin = {
created() { console.log('Mixin used!') }
}
setup().
<teleport to="body">
<div>Modal Content</div>
</teleport>
<keep-alive> caches inactive components instead of destroying them. Useful for tabs or forms where you don’t want to lose state. Example:
<keep-alive>
<component :is="view"></component>
</keep-alive>
new Vue({
el: '#app',
data: { message: 'Hello Vue!' }
});
In Vue 3:
import { createApp } from 'vue';
createApp({ data: () => ({ message: 'Hello Vue 3!' }) }).mount('#app');
v-bind or : shorthand.
<img :src="imageUrl" :alt="description" />
v-for.
<li v-for="item in items" :key="item.id">{{ item.name }}</li>
<div :class="{ active: isActive, error: hasError }"></div>
<div :style="{ color: activeColor, fontSize: size + 'px' }"></div>
<form @submit.prevent="onSubmit">...</form>
<button @click.stop="doSomething">Click</button>
<input type="checkbox" v-model="checked" />
<p>{{ checked }}</p>
<input type="checkbox" v-model="fruits" value="Apple">
<input type="checkbox" v-model="fruits" value="Banana">
<p>{{ fruits }}</p>
<input v-model="username" placeholder="Enter name" />
<p>Welcome, {{ username }}</p>
<template>
<button @click="count++">Clicked {{ count }} times</button>
</template>
<script>
export default { data: () => ({ count: 0 }) }
</script>
props: ['value'],
watch: {
value(newVal, oldVal) {
console.log('Changed from', oldVal, 'to', newVal);
}
}
<button @click="$emit('custom-event', 'hello')">Send</button>
Parent:
<Child @custom-event="receiveMessage" />
computed: {
fullName: {
get() { return this.first + ' ' + this.last },
set(value) { [this.first, this.last] = value.split(' ') }
}
}
<li v-for="(value, key) in object" :key="key">{{ key }}: {{ value }}</li>
<template v-if="type === 'A'">
<p>A content</p>
</template>
<template v-else-if="type === 'B'">
<p>B content</p>
</template>
<template v-else>
<p>Other</p>
</template>
<input v-model.lazy="search" />
mounted() {
fetch('https://api.example.com/data')
.then(res => res.json())
.then(data => this.items = data)
}
import axios from 'axios';
mounted() {
axios.get('/api/users').then(res => this.users = res.data);
}
import { createApp } from 'vue';
import MyComponent from './MyComponent.vue';
const app = createApp(App);
app.component('MyComponent', MyComponent);
app.mount('#app');
const AsyncComp = defineAsyncComponent(() => import('./MyComp.vue'));
props: {
title: { type: String, required: true },
count: { type: Number, default: 0 }
}
props: {
age: {
type: Number,
validator: v => v > 0
}
}
<div :class="[isActive ? 'active' : 'inactive']"></div>
<div :class="['box', isError ? 'red' : 'green']"></div>
<div :style="{ backgroundColor: bgColor, fontSize: size + 'px' }"></div>
<template>
<div>
<input v-model="newTask" @keyup.enter="addTask" />
<ul><li v-for="t in tasks" :key="t">{{ t }}</li></ul>
</div>
</template>
<script>
export default {
data: () => ({ newTask: '', tasks: [] }),
methods: { addTask() { this.tasks.push(this.newTask); this.newTask = '' } }
}
</script>
<script lang="ts">
import { defineComponent } from 'vue';
export default defineComponent({
data() { return { count: 0 } }
});
</script>
import { mount } from '@vue/test-utils';
import Counter from './Counter.vue';
test('increments on click', () => {
const wrapper = mount(Counter);
wrapper.find('button').trigger('click');
expect(wrapper.text()).toContain('1');
});
npm run build
Deploy the generated dist/ folder to a hosting service (Netlify, Vercel, GitHub Pages, etc.).
<Suspense>
<template #default>
<AsyncComponent />
</template>
<template #fallback>
Loading...
</template>
</Suspense>
reactive() - creates a reactive object, suitable for multiple properties.
ref() - creates a reactive single value, accessed via .value.
Example:
const state = reactive({ count: 0 });
const count = ref(0);
provide('theme', 'dark');
inject('theme');
errorCaptured(err, instance, info) {
console.log(err, info);
return false; // stop propagation
}
<component :is="currentComponent"></component>
currentComponent = 'HomeComponent';
<template>
<h1>Title</h1>
<p>Paragraph</p>
</template>
app.directive('focus', {
mounted(el) { el.focus(); }
});
<input v-focus />
this.message = 'Hello';
this.$nextTick(() => { console.log('DOM updated'); });
watch([prop1, prop2], ([newVal1, newVal2], [oldVal1, oldVal2]) => {
console.log(newVal1, newVal2);
});
app.mixin({
created() { console.log('Mixin called in all components'); }
});
const AsyncComp = defineAsyncComponent(() => import('./MyComp.vue'));
export default {
install(app, options) {
app.config.globalProperties.$myPlugin = () => console.log('Plugin used');
}
};
<transition name="fade">
<div v-if="show">Hello</div>
</transition>
.fade-enter-active, .fade-leave-active { transition: opacity .5s; }
.fade-enter-from, .fade-leave-to { opacity: 0; }
<input ref="myInput" />
this.$refs.myInput.focus();
import { reactive } from 'vue';
const state = reactive({ count: 0, message: 'Hello' });
import { shallowReactive } from 'vue';
const state = shallowReactive({ nested: { count: 0 } });
import { ref } from 'vue';
const count = ref(0);
const message = ref('Hello');
<li v-for="item in items" :key="item.id">{{ item.name }}</li>
<slot>Default content</slot>
defineEmits(['update']);
emit('update', value);
const routes = [
{ path: '/about', component: () => import('./About.vue') }
];
<MyComponent :title="dynamicTitle" />
import { computed, ref } from 'vue';
const first = ref('John');
const last = ref('Doe');
const fullName = computed(() => first.value + ' ' + last.value);
const AsyncComp = defineAsyncComponent(() => import('./Comp.vue'));
<div :style="{ color: isError ? 'red' : 'green' }">Message</div>
<component :is="currentView"></component>
import { ref, onMounted } from 'vue';
const myInput = ref(null);
onMounted(() => { myInput.value.focus(); });
<input ref="myInput" />
import { reactive, ref } from 'vue';
const count = ref(0);
const message = ref('Hello');
const state = reactive({ count, message });
errorCaptured(err, vm, info) {
console.error(err);
return false; // stop propagation
}
defineEmits(['update', 'delete']);
emit('update', data);
emit('delete', id);
<Suspense>
<template #default>
<AsyncComp />
</template>
<template #fallback>
Loading...
</template>
</Suspense>
mounted() {
fetch('https://api.example.com/data')
.then(res => res.json())
.then(data => this.items = data);
}
<template>
<div v-if="show">
<div class="modal">
<slot>Modal Content</slot>
</div>
</div>
</template>
<script>
export default {
props: ['show']
}
</script>
app.directive('focus', {
mounted(el) { el.focus(); }
});
<input v-focus />
import mitt from 'mitt';
export const bus = mitt();
bus.on('eventName', payload => console.log(payload));
bus.emit('eventName', { message: 'Hello' });
<table>
<tr v-for="row in tableData" :key="row.id">
<td v-for="(value, key) in row" :key="key">{{ value }}</td>
</tr>
</table>
<button @click="isActive = !isActive">Toggle</button>
<p>Status: {{ isActive ? 'On' : 'Off' }}</p>
data() {
return { isActive: false }
}
<div>
<button v-for="tab in tabs" :key="tab" @click="currentTab = tab">{{ tab }}</button>
<div v-for="tab in tabs" v-show="currentTab === tab">Content for {{ tab }}</div>
</div>
data() {
return { tabs: ['Tab1','Tab2'], currentTab: 'Tab1' }
}
<div>
<button @click="showMenu = !showMenu">Menu</button>
<ul v-if="showMenu">
<li v-for="item in items" :key="item">{{ item }}</li>
</ul>
</div>
data() { return { showMenu: false, items: ['One','Two','Three'] } }
<form @submit.prevent="submitForm">
<input v-model="email" />
<span v-if="!isValid">Invalid email</span>
<button type="submit">Submit</button>
</form>
data() { return { email: '' } },
computed: {
isValid() { return this.email.includes('@') }
},
methods: { submitForm() { alert('Form submitted') } }
<div @mouseenter="show=true" @mouseleave="show=false">
Hover me
<span v-if="show">Tooltip text</span>
</div>
data() { return { show: false } }
<template>
<input :value="modelValue" @input="$emit('update:modelValue',$event.target.value)" />
</template>
props: ['modelValue']
import { reactive } from 'vue';
const state = reactive({ count: 0, message: 'Hello' });
state.count++;
state.message = 'Hi';
import { ref, watchEffect } from 'vue';
const count = ref(0);
watchEffect(() => { console.log(`Count is: ${count.value}`); });
count.value++;
async mounted() {
const res = await fetch('https://api.example.com');
const data = await res.json();
this.items = data;
}
<Child :modelValue="parentData" @update:modelValue="parentData = $event" />
data() { return { parentData: '' } }
<Suspense>
<template #default><component :is="currentView" /></template>
<template #fallback>Loading...</template>
</Suspense>
<li v-for="(item, index) in items" :key="index">{{ index }} - {{ item }}</li>
<button @click="handleEvent" @mouseover="handleEvent">Click or Hover</button>
methods: { handleEvent() { console.log('Event triggered') } }
import { reactive } from 'vue';
const map = reactive(new Map());
map.set('key', 'value');
console.log(map.get('key'));
<div v-if="loading">Loading...</div>
<div v-else>Content Loaded</div>
data() { return { loading: true } },
mounted() { setTimeout(() => this.loading = false, 2000) }
<div v-show="isVisible">Visible Content</div>
data() { return { isVisible: true } }
data() { return { time: 10 } },
mounted() {
const timer = setInterval(() => {
if(this.time > 0) this.time--;
else clearInterval(timer);
}, 1000);
}
<div>{{ time }} seconds left</div>
<th @click="sortBy('name')">Name</th>
<tr v-for="item in sortedItems" :key="item.id">
<td>{{ item.name }}</td>
</tr>
computed: {
sortedItems() {
return this.items.sort((a,b) => a.name.localeCompare(b.name));
}
},
methods: { sortBy(key) { /* change sort key logic */ } }
<div v-for="(item, index) in items" :key="index">
<h3 @click="active = index">{{ item.title }}</h3>
<div v-show="active === index">{{ item.content }}</div>
</div>
data() { return { active: null, items: [...] } }
<div v-if="step === 1">Step 1 Content</div>
<div v-else-if="step === 2">Step 2 Content</div>
<button @click="step++">Next</button>
data() { return { step: 1 } }
<input v-model="search" placeholder="Search">
<li v-for="item in filteredItems" :key="item">{{ item }}</li>
computed: {
filteredItems() {
return this.items.filter(i => i.includes(this.search));
}
}
<li v-for="item in pagedItems" :key="item">{{ item }}</li>
<button @click="page--">Prev</button>
<button @click="page++">Next</button>
computed: {
pagedItems() {
const start = (this.page-1)*this.pageSize;
return this.items.slice(start, start + this.pageSize);
}
},
data() { return { page: 1, pageSize: 5 } }
<div :style="{ backgroundColor: color }">Colored Div</div>
data() { return { color: 'red' } }
<div v-for="n in notifications" :key="n.id">{{ n.message }}</div>
methods: { addNotification(msg) { this.notifications.push({ id: Date.now(), message: msg }) } },
data() { return { notifications: [] } }
<div :class="{ sticky: isSticky }">Header</div>
data() { return { isSticky: false } },
mounted() {
window.addEventListener('scroll', () => { this.isSticky = window.scrollY > 100 })
}
<div :style="{ width: progress + '%', backgroundColor: 'green' }"></div>
data() { return { progress: 0 } },
mounted() { setInterval(() => { if(progress<100) this.progress += 1 }, 100) }
<div :class="{ dark: isDark }">Content</div>
<button @click="isDark = !isDark">Toggle Dark</button>
data() { return { isDark: false } }
<Modal v-if="showModal" @close="showModal = false">Content</Modal>
data() { return { showModal: false } },
methods: { openModal() { this.showModal = true } }
<div draggable="true" @dragstart="dragStart(item)">Drag Me</div>
methods: {
dragStart(item) { this.dragged = item }
},
data() { return { dragged: null } }
window.addEventListener('scroll', () => {
if(window.innerHeight + window.scrollY >= document.body.offsetHeight) {
this.loadMore();
}
});
methods: { loadMore() { this.items.push(...newItems) } }
<input v-model.lazy="search">
data() { return { search: '' } }
<button v-for="(tab,index) in tabs" @click="active=index">{{ tab }}</button>
<div v-for="(tab,index) in tabs" v-show="active===index">Content {{ tab }}</div>
data() { return { tabs:['A','B'], active:0 } }
<div v-cloak>{{ message }}</div>
<style> [v-cloak] { display:none; } </style>
<div @mouseenter="show=true" @mouseleave="show=false">Hover Me
<span v-if="show">Tooltip</span>
</div>
data() { return { show:false } }
<transition name="fade">
<div v-if="show">Content</div>
</transition>
data() { return { show: true } }
<style>
.fade-enter-active, .fade-leave-active { transition: opacity 0.5s; }
.fade-enter, .fade-leave-to { opacity:0; }
</style>
<div v-for="(field,index) in fields" :key="index">
<input v-model="field.value" />
</div>
data() { return { fields:[{value:''},{value:''}] } }
import { ref, onMounted } from 'vue';
const time = ref(10);
onMounted(() => {
const timer = setInterval(() => {
if(time.value>0) time.value--;
else clearInterval(timer);
},1000);
});
<img v-lazy="imageUrl" alt="Lazy Image">
import VueLazyload from 'vue-lazyload';
app.use(VueLazyload);
app.directive('tooltip', {
mounted(el, binding) {
el.setAttribute('title', binding.value);
}
});
<button v-tooltip="'Tooltip text'">Hover Me</button>
router.beforeEach((to, from, next) => {
if(to.meta.requiresAuth && !isLoggedIn()) next('/login');
else next();
});
import draggable from 'vuedraggable';
<draggable v-model="list">
<div v-for="item in list" :key="item">{{ item }}</div>
</draggable>
data() { return { list: ['A','B','C'] } }
<div v-for="(img,index) in images" v-show="current===index"><img :src="img"></div>
<button @click="prev">Prev</button>
<button @click="next">Next</button>
data() { return { images:['a.jpg','b.jpg'], current:0 } },
methods: { prev(){ this.current = (this.current-1+this.images.length)%this.images.length }, next(){ this.current=(this.current+1)%this.images.length } }
<input v-model="email">
<span v-if="!isValidEmail">Invalid Email</span>
computed: { isValidEmail(){ return /^[^@]+@[^@]+\.[^@]+$/.test(this.email) } },
data() { return { email:'' } }
data() { return { time: 10000 } },
mounted() {
const timer = setInterval(() => {
if(this.time>0) this.time-=100;
else clearInterval(timer);
},100);
}
<div>{{ (time/1000).toFixed(1) }}s</div>
<svg viewBox="0 0 36 36">
<circle cx="18" cy="18" r="16" stroke-width="4" :stroke-dasharray="progress+',100'" />
</svg>
data() { return { progress: 50 } }
import jsPDF from 'jspdf';
methods: { downloadPDF() {
const doc = new jsPDF();
doc.text("Hello Vue PDF", 10, 10);
doc.save("file.pdf");
} }
<button @click="downloadPDF">Download PDF</button>
<header :class="{ sticky: isSticky }">Header</header>
data() { return { isSticky:false } },
mounted() {
window.addEventListener('scroll', () => this.isSticky = window.scrollY>100)
}
<label>
<input type="checkbox" v-model="isOn">
<span>Toggle</span>
</label>
data() { return { isOn:false } }
<span v-for="n in 5" :key="n" @click="rating=n">{{ n <= rating ? '★':'☆' }}</span>
data() { return { rating:0 } }
<input type="file" @change="onFileChange">
methods: { onFileChange(e){ this.file = e.target.files[0] } },
data() { return { file:null } }
data() { return { time:3600 } },
computed: {
hours() { return Math.floor(this.time/3600) },
minutes() { return Math.floor((this.time%3600)/60) },
seconds() { return this.time%60 }
},
mounted() {
setInterval(() => { if(this.time>0)this.time-- },1000)
}
<div v-if="showToast">Notification</div>
methods: { show(){ this.showToast=true; setTimeout(()=>this.showToast=false,3000) } },
data() { return { showToast:false } }
<input v-model="query">
<li v-for="item in filteredItems">{{ item }}</li>
computed: {
filteredItems() { return this.items.filter(i => i.includes(this.query)) }
},
data() { return { items:['apple','banana'], query:'' } }
<input :type="show?'text':'password'" v-model="password">
<button @click="show=!show">Toggle</button>
data() { return { password:'', show:false } }
<aside :class="{ open:isOpen }">Sidebar</aside>
<button @click="isOpen=!isOpen">Toggle</button>
data() { return { isOpen:false } }
<th @click="sort('name')">Name</th>
<tr v-for="item in sorted"><td>{{ item.name }}</td></tr>
data() { return { items:[{name:'A'},{name:'B'}], sortKey:'', asc:true } },
computed: {
sorted() { return this.items.sort((a,b)=>this.asc?(a[this.sortKey]>b[this.sortKey]?1:-1):(a[this.sortKey]
<div v-for="item in items">{{ item }}</div>
window.addEventListener('scroll', () => {
if(window.innerHeight + window.scrollY > document.body.offsetHeight-10) loadMore()
});
methods: { loadMore(){ this.items.push(...moreItems) } },
data() { return { items:[] } }
<div v-if="showModal">Modal Content</div>
<button @click="showModal=true">Open</button>
<button @click="showModal=false">Close</button>
data() { return { showModal:false } }
<div @drop.prevent="onDrop" @dragover.prevent>Drop files here</div>
methods: { onDrop(e){ this.files = e.dataTransfer.files } },
data() { return { files:[] } }
<li v-for="(crumb,index) in breadcrumbs">{{ crumb }}</li>
data() { return { breadcrumbs:['Home','About','Profile'] } }
<div v-if="step===1">Step 1</div>
<div v-else-if="step===2">Step 2</div>
<button @click="nextStep">Next</button>
data() { return { step:1 } },
methods: { nextStep(){ if(this.step<2)this.step++ } }
<div v-for="img in images"><img :src="img"></div>
data() { return { images:['1.jpg','2.jpg','3.jpg'] } }
<button @click="open=!open">Menu</button>
<ul v-if="open"><li>Item 1</li><li>Item 2</li></ul>
data() { return { open:false } }
<button v-for="(t,index) in tabs" @click="active=index">{{ t }}</button>
<div v-for="(t,index) in tabs" v-show="active===index">{{ t }} Content</div>
data() { return { tabs:['Tab1','Tab2'], active:0 } }
<div :style="{ width: progress+'%' }">{{ progress }}%</div>
data() { return { progress:0 } },
methods: { increase(){ if(this.progress<100)this.progress+=10 } }
<div @click="open=!open">Header</div>
<div v-if="open">Content</div>
data() { return { open:false } }
<input type="date" v-model="selectedDate">
data() { return { selectedDate:'' } }
<input type="time" v-model="selectedTime">
data() { return { selectedTime:'' } }
<button @click="count--">-</button>
<span>{{ count }}</span>
<button @click="count++">+</button>
data() { return { count:0 } }
<div class="table-responsive">
<table>
<tr v-for="item in items"><td>{{ item.name }}</td></tr>
</table>
</div>
data() { return { items:[{name:'A'},{name:'B'}] } }
<select v-model="selected" multiple>
<option v-for="opt in options" :value="opt">{{ opt }}</option>
</select>
data() { return { selected:[], options:['A','B','C'] } }
<span @mouseover="show=true" @mouseleave="show=false">Hover me</span>
<div v-if="show">Tooltip Content</div>
data() { return { show:false } }
<div v-if="showModal">{{ content }}</div>
<button @click="open('Hello')">Open</button>
methods: { open(msg){ this.content=msg; this.showModal=true } },
data() { return { showModal:false, content:'' } }
<div :class="{ flipped:isFlipped }" @click="isFlipped=!isFlipped">Front/Back</div>
data() { return { isFlipped:false } }
data() { return { time:10 } },
mounted() {
let timer=setInterval(()=>{
if(this.time>0)this.time--;
else clearInterval(timer)
},1000)
}
<input v-model="newTag" @keyup.enter="addTag">
<span v-for="tag in tags">{{ tag }}</span>
data() { return { newTag:'', tags:[] } },
methods: { addTag(){ this.tags.push(this.newTag); this.newTag='' } }
<div v-for="img in images"><img :src="img"></div>
methods: { next(){ this.index = (this.index+1) % this.images.length } },
data() { return { images:['1.jpg','2.jpg'], index:0 } }
<span v-for="n in 5" @click="rating=n">★</span>
data() { return { rating:0 } }
<input :type="show?'text':'password'" v-model="password">
<button @click="show=!show">Toggle</button>
data() { return { password:'', show:false } }
<div :class="{ collapsed:isCollapsed }">Sidebar</div>
<button @click="isCollapsed=!isCollapsed">Toggle</button>
data() { return { isCollapsed:false } }
<span @mouseover="show=true" @mouseleave="show=false">Hover</span>
<div v-if="show">{{ tooltipText }}</div>
data() { return { show:false, tooltipText:'Dynamic Tooltip' } }
<td v-for="cell in row"><input v-model="cell.value"></td>
data() { return { table:[[{value:'A'},{value:'B'}]] } }
<input v-model="query">
<li v-for="item in items.filter(i => i.includes(query))">{{ item }}</li>
data() { return { query:'', items:['Apple','Banana'] } }
Vue.directive('focus', { inserted(el){ el.focus() } })
<input v-focus>
<component :is="currentComponent"></component>
data() { return { currentComponent:'CompA' } }
<textarea v-model="md"></textarea>
<div v-html="compiledMarkdown"></div>
computed: { compiledMarkdown(){ return marked(this.md) } },
data() { return { md:'' } }
<div v-if="step===1">Step 1</div>
<div v-else-if="step===2">Step 2</div>
<button @click="step++">Next</button>
data() { return { step:1 } }
<button v-tooltip="'Tooltip Text'">Hover</button>
import VTooltip from 'v-tooltip';
app.use(VTooltip)
<draggable v-model="items"><div v-for="i in items">{{ i }}</div></draggable>
data() { return { items:['A','B','C'] } }
<button @click="showToast">Show</button>
<div v-if="toast">Notification</div>
methods:{ showToast(){ this.toast=true; setTimeout(()=>this.toast=false,2000) } },
data(){ return { toast:false } }
<button @click="scrollTop">Top</button>
methods:{ scrollTop(){ window.scrollTo(0,0) } }
<div>{{ time }}</div>
mounted(){
let t=setInterval(()=>{ if(this.time>0)this.time--; else clearInterval(t) },1000)
},
data(){ return { time:10 } }
<header :class="{ sticky:isSticky }">Header</header>
mounted(){ window.addEventListener('scroll',()=>this.isSticky=window.scrollY>50) },
data(){ return { isSticky:false } }
<div @drop.prevent="upload" @dragover.prevent>Drop file</div>
methods:{ upload(e){ this.file=e.dataTransfer.files[0] } },
data(){ return { file:null } }
<button @click="dark=!dark">Toggle</button>
<div :class="{ dark:dark }">Content</div>
data(){ return { dark:false } }
<button @click="theme=theme==='light'?'dark':'light'">Switch</button>
<div :class="theme">Content</div>
data(){ return { theme:'light' } }