Vue Components
Components in Vue
Components are reusable, self-contained pieces of UI. In Vue 3, components are defined as Single File Components (.vue files) containing template, script, and style sections.
<!-- components/AppButton.vue -->
<template>
<button
:class="['btn', `btn-${variant}`, `btn-${size}`, { loading }]"
:disabled="disabled || loading"
@click="$emit('click', $event)"
>
<span v-if="loading" class="spinner"></span>
<slot>{{ label }}</slot>
</button>
</template>
<script setup>
// defineProps — declare accepted props
const props = defineProps({
label: { type: String, default: 'Click me' },
variant: { type: String, default: 'primary', validator: v => ['primary','secondary','danger'].includes(v) },
size: { type: String, default: 'md' },
disabled: { type: Boolean, default: false },
loading: { type: Boolean, default: false },
})
// defineEmits — declare emitted events
const emit = defineEmits(['click'])
</script>
<style scoped>
.btn { padding: 8px 16px; border-radius: 4px; cursor: pointer; }
.btn-primary { background: #42b883; color: white; }
.btn-secondary { background: #6c757d; color: white; }
.btn-danger { background: #dc3545; color: white; }
.btn-sm { padding: 4px 8px; font-size: 0.875rem; }
.btn-lg { padding: 12px 24px; font-size: 1.125rem; }
.loading { opacity: 0.7; cursor: not-allowed; }
</style>
<!-- components/AppCard.vue — using slots -->
<template>
<div class="card">
<!-- Named slot: header -->
<div v-if="$slots.header" class="card-header">
<slot name="header" />
</div>
<!-- Default slot: body content -->
<div class="card-body">
<slot />
</div>
<!-- Named slot: footer -->
<div v-if="$slots.footer" class="card-footer">
<slot name="footer" />
</div>
</div>
</template>
<!-- Scoped slot — pass data from child to parent -->
<!-- components/DataList.vue -->
<template>
<ul>
<li v-for="item in items" :key="item.id">
<!-- Pass item data to parent via scoped slot -->
<slot :item="item" :index="items.indexOf(item)" />
</li>
</ul>
</template>
<script setup>
defineProps({ items: Array })
</script>
<!-- Parent.vue — using components -->
<template>
<div>
<!-- Using AppButton -->
<AppButton label="Save" variant="primary" @click="save" />
<AppButton variant="danger" :loading="isDeleting" @click="deleteItem">
Delete <!-- slot content overrides label -->
</AppButton>
<!-- Using AppCard with named slots -->
<AppCard>
<template #header>
<h3>User Profile</h3>
</template>
<!-- Default slot -->
<p>Name: Alice</p>
<p>Email: alice@example.com</p>
<template #footer>
<AppButton label="Edit" size="sm" />
</template>
</AppCard>
<!-- Using scoped slot -->
<DataList :items="users">
<template #default="{ item, index }">
<span>{{ index + 1 }}. {{ item.name }}</span>
</template>
</DataList>
</div>
</template>
<script setup>
import { ref } from 'vue'
import AppButton from './AppButton.vue'
import AppCard from './AppCard.vue'
import DataList from './DataList.vue'
const isDeleting = ref(false)
const users = ref([{ id: 1, name: 'Alice' }, { id: 2, name: 'Bob' }])
function save() { console.log('Saved!') }
async function deleteItem() {
isDeleting.value = true
await new Promise(r => setTimeout(r, 1000))
isDeleting.value = false
}
</script>
Ready to Level Up Your Skills?
Explore 500+ free tutorials across 20+ languages and frameworks.