Skip to main content
Components are reusable Vue instances with custom names. They accept the same options as the root Vue instance and have their own template, data, methods, and lifecycle hooks.

Defining a Component

Components can be defined using Single-File Components (SFCs) with the .vue extension:
<!-- ButtonCounter.vue -->
<template>
  <button @click="count++">
    You clicked me {{ count }} times
  </button>
</template>

<script setup>
import { ref } from 'vue'

const count = ref(0)
</script>

<style scoped>
button {
  padding: 10px 20px;
  background: #42b983;
  border: none;
  border-radius: 4px;
  color: white;
  cursor: pointer;
}
</style>

Using a Component

Import and use the component in another component:
<template>
  <div>
    <h1>Here are many child components!</h1>
    <ButtonCounter />
    <ButtonCounter />
    <ButtonCounter />
  </div>
</template>

<script setup>
import ButtonCounter from './ButtonCounter.vue'
</script>
Each component instance maintains its own separate state. Each ButtonCounter has its own independent count.

Component Registration

Global Registration

Register components globally to use them anywhere:
import { createApp } from 'vue'
import App from './App.vue'
import ButtonCounter from './components/ButtonCounter.vue'

const app = createApp(App)

// Register globally
app.component('ButtonCounter', ButtonCounter)

app.mount('#app')
Now ButtonCounter can be used in any component without importing:
<template>
  <ButtonCounter />
</template>

<!-- No import needed -->

Local Registration

With <script setup>, imported components are automatically locally registered:
<template>
  <ButtonCounter />
</template>

<script setup>
import ButtonCounter from './ButtonCounter.vue'
// Automatically registered
</script>
Local registration is recommended as it:
  • Makes dependencies more explicit
  • Enables tree-shaking (unused components won’t be included in bundle)
  • Improves IDE autocomplete

Passing Data with Props

From packages/runtime-core/src/componentProps.ts, props allow parent components to pass data to child components:
<template>
  <div>
    <h3>{{ title }}</h3>
    <p>{{ message }}</p>
  </div>
</template>

<script setup>
// Declare props
defineProps({
  title: String,
  message: String
})
</script>

Prop Types

Define prop types for validation:
<script setup>
defineProps({
  title: String,
  count: Number,
  isActive: Boolean,
  tags: Array,
  user: Object,
  callback: Function
})
</script>

Advanced Prop Definitions

<script setup>
defineProps({
  // Basic type check
  title: String,
  
  // Multiple possible types
  message: [String, Number],
  
  // Required prop
  id: {
    type: Number,
    required: true
  },
  
  // With default value
  status: {
    type: String,
    default: 'pending'
  },
  
  // Object/array defaults need factory function
  user: {
    type: Object,
    default: () => ({ name: 'Guest' })
  },
  
  // Custom validator
  age: {
    type: Number,
    validator: (value) => value >= 0
  }
})
</script>

TypeScript Props

Use TypeScript for better type safety:
<script setup lang="ts">
interface Props {
  title: string
  count?: number
  tags: string[]
  user: {
    name: string
    age: number
  }
}

const props = defineProps<Props>()

// With defaults
const props = withDefaults(defineProps<Props>(), {
  count: 0,
  tags: () => []
})
</script>

Emitting Events

Child components can emit events to communicate with parents:
<template>
  <button @click="handleClick">
    Click me
  </button>
</template>

<script setup>
const emit = defineEmits(['clicked'])

function handleClick() {
  emit('clicked', 'Hello from child')
}
</script>

Declaring Emits

<script setup>
// Array syntax
const emit = defineEmits(['change', 'update'])

// Object syntax with validation
const emit = defineEmits({
  change: (value) => {
    // Return true if valid
    return typeof value === 'string'
  },
  update: null // No validation
})

// TypeScript syntax
const emit = defineEmits<{
  change: [value: string]
  update: [id: number, value: string]
}>()
</script>

v-model on Components

Create two-way bindings on custom components:
<template>
  <input
    :value="modelValue"
    @input="$emit('update:modelValue', $event.target.value)"
  />
</template>

<script setup>
defineProps(['modelValue'])
defineEmits(['update:modelValue'])
</script>

Multiple v-model Bindings

<template>
  <UserName
    v-model:first-name="first"
    v-model:last-name="last"
  />
</template>

<script setup>
import { ref } from 'vue'

const first = ref('')
const last = ref('')
</script>

Slots

Slots allow you to pass template content to child components:
<template>
  <div class="card">
    <div class="card-header">
      <slot name="header">Default Header</slot>
    </div>
    <div class="card-body">
      <slot>Default Content</slot>
    </div>
    <div class="card-footer">
      <slot name="footer"></slot>
    </div>
  </div>
</template>

<style scoped>
.card {
  border: 1px solid #ddd;
  border-radius: 4px;
}
</style>

Scoped Slots

Pass data from child to parent through slots:
<template>
  <ul>
    <li v-for="item in items" :key="item.id">
      <slot :item="item" :index="item.id">
        {{ item.name }}
      </slot>
    </li>
  </ul>
</template>

<script setup>
defineProps({
  items: Array
})
</script>

Dynamic Components

Switch between components dynamically:
<template>
  <div>
    <button
      v-for="tab in tabs"
      :key="tab"
      @click="currentTab = tab"
    >
      {{ tab }}
    </button>
    
    <component :is="currentComponent" />
  </div>
</template>

<script setup>
import { ref, computed } from 'vue'
import Home from './Home.vue'
import Posts from './Posts.vue'
import Archive from './Archive.vue'

const tabs = ['Home', 'Posts', 'Archive']
const currentTab = ref('Home')

const components = { Home, Posts, Archive }

const currentComponent = computed(() => {
  return components[currentTab.value]
})
</script>

Keep-Alive

Cache component instances when switching:
<template>
  <KeepAlive>
    <component :is="currentComponent" />
  </KeepAlive>
</template>
<KeepAlive> preserves component state and avoids re-rendering when components are toggled. Use the include, exclude, and max props to control caching behavior.

Async Components

From packages/runtime-core/src/apiAsyncComponent.ts, load components asynchronously:
<script setup>
import { defineAsyncComponent } from 'vue'

const AsyncComp = defineAsyncComponent(() =>
  import('./components/AsyncComponent.vue')
)

// With options
const AsyncCompWithOptions = defineAsyncComponent({
  loader: () => import('./AsyncComponent.vue'),
  loadingComponent: LoadingComponent,
  errorComponent: ErrorComponent,
  delay: 200,
  timeout: 3000
})
</script>

<template>
  <AsyncComp />
</template>

Provide / Inject

Pass data to descendant components without prop drilling:
<script setup>
import { provide, ref } from 'vue'

const theme = ref('dark')
const user = ref({ name: 'John' })

// Provide to descendants
provide('theme', theme)
provide('user', user)
</script>

Component Naming Conventions

1

PascalCase for files

Name component files using PascalCase:
  • ButtonCounter.vue
  • UserProfile.vue
  • TodoItem.vue
2

PascalCase in templates

Use PascalCase in templates (recommended):
<ButtonCounter />
<UserProfile />
3

kebab-case alternative

kebab-case also works:
<button-counter />
<user-profile />

defineComponent

From packages/runtime-core/src/apiDefineComponent.ts:63, use defineComponent for better TypeScript inference:
import { defineComponent } from 'vue'

export default defineComponent({
  props: {
    message: String
  },
  setup(props) {
    // props are typed!
    console.log(props.message)
  }
})
With <script setup>, you don’t need defineComponent as type inference works automatically.

Component Instance Access

From packages/runtime-core/src/component.ts:106, access the component instance:
<script setup>
import { getCurrentInstance } from 'vue'

const instance = getCurrentInstance()

if (instance) {
  console.log(instance.type.name)
  console.log(instance.proxy) // Component public instance
}
</script>
getCurrentInstance() is an advanced API for library authors. Avoid using it in application code as it exposes internal details.

Best Practices

1

Single Responsibility

Each component should have a single, well-defined purpose.
2

Props Down, Events Up

Pass data down via props, communicate up via events.
3

Validate Props

Always define prop types and add validation when needed.
4

Use Scoped Styles

Add scoped attribute to <style> to prevent style leaks:
<style scoped>
/* Styles only apply to this component */
</style>
5

Extract Reusable Logic

Use composables for reusable logic instead of mixins.

Reactivity Fundamentals

Component state with ref() and reactive()

Template Refs

Access component instances

Lifecycle Hooks

Component lifecycle management

Build docs developers (and LLMs) love