What are Components in Nuxt?

Nuxt components are Vue components with automatic imports, file-based naming, and built-in optimization.

Nuxt Magic: Components in the components/ directory are automatically imported - no need for manual imports!

Component Directory Structure

components/
├── AppHeader.vue          → <AppHeader />
├── AppFooter.vue          → <AppFooter />
├── base/
│   ├── Button.vue         → <BaseButton />
│   └── Input.vue          → <BaseInput />
└── product/
    ├── Card.vue           → <ProductCard />
    └── List.vue           → <ProductList />
Zero Config: Nuxt automatically registers all components in the components/ directory!

Auto Imports

Nuxt automatically imports components, composables, and utilities without explicit imports.

Basic Component Usage

<!-- components/AppHeader.vue -->
<template>
  <header>
    <h1>My Nuxt App</h1>
  </header>
</template>

<!-- pages/index.vue -->
<template>
  <div>
    <!-- No import needed! -->
    <AppHeader />
    <h2>Welcome!</h2>
  </div>
</template>

Nested Directory Components

<!-- components/base/Button.vue -->
<template>
  <button class="btn">
    <slot />
  </button>
</template>

<!-- Usage in any page/component -->
<template>
  <div>
    <!-- Component name = directory + filename -->
    <BaseButton>Click Me</BaseButton>
  </div>
</template>

Component Naming Conventions

File Path Component Name Usage
components/Button.vue Button <Button />
components/base/Button.vue BaseButton <BaseButton />
components/user/Profile.vue UserProfile <UserProfile />
components/UserProfileCard.vue UserProfileCard <UserProfileCard />
Tip: Use PascalCase for component names in directories and kebab-case or PascalCase in templates.

Built-in Nuxt Components

Nuxt provides several built-in components for common use cases.

<NuxtLink> - Navigation

<template>
  <nav>
    <!-- Internal links -->
    <NuxtLink to="/">Home</NuxtLink>
    <NuxtLink to="/about">About</NuxtLink>
    <NuxtLink to="/blog/post-1">Blog Post</NuxtLink>

    <!-- External links -->
    <NuxtLink to="https://nuxt.com" external>Nuxt Docs</NuxtLink>

    <!-- Active link styling -->
    <NuxtLink
      to="/dashboard"
      active-class="active"
      exact-active-class="exact-active"
    >
      Dashboard
    </NuxtLink>
  </nav>
</template>

<NuxtPage> - Page Rendering

<!-- app.vue or layouts -->
<template>
  <div>
    <AppHeader />

    <!-- Renders the current page -->
    <NuxtPage />

    <AppFooter />
  </div>
</template>

<NuxtLayout> - Layout System

<!-- app.vue -->
<template>
  <NuxtLayout>
    <NuxtPage />
  </NuxtLayout>
</template>

<!-- pages/index.vue -->
<template>
  <div>
    <h1>Home Page</h1>
  </div>
</template>

<script setup>
// Use a specific layout
definePageMeta({
  layout: 'dashboard'
})
</script>

<NuxtLoadingIndicator> - Progress Bar

<!-- app.vue -->
<template>
  <div>
    <!-- Shows progress bar during page transitions -->
    <NuxtLoadingIndicator color="#00DC82" />

    <NuxtLayout>
      <NuxtPage />
    </NuxtLayout>
  </div>
</template>

<NuxtImg> - Image Optimization

<template>
  <div>
    <!-- Optimized image with @nuxt/image module -->
    <NuxtImg
      src="/images/hero.jpg"
      alt="Hero image"
      width="800"
      height="600"
      loading="lazy"
    />

    <!-- With transformations -->
    <NuxtImg
      src="/images/avatar.jpg"
      width="100"
      height="100"
      format="webp"
      quality="80"
    />
  </div>
</template>
Note: <NuxtImg> requires the @nuxt/image module to be installed.

Client-Only Rendering

Render components only on the client-side using <ClientOnly>.

Basic Usage

<template>
  <div>
    <h1>This renders on both server and client</h1>

    <!-- Only renders on client-side -->
    <ClientOnly>
      <BrowserOnlyComponent />
    </ClientOnly>
  </div>
</template>

With Fallback

<template>
  <div>
    <ClientOnly>
      <!-- Client-side content -->
      <InteractiveMap />

      <!-- Server-side fallback -->
      <template #fallback>
        <div>Loading map...</div>
      </template>
    </ClientOnly>
  </div>
</template>

When to Use Client-Only

  • Components that use browser-only APIs (window, document, localStorage)
  • Third-party libraries without SSR support
  • Components with different server/client hydration
  • Heavy visualizations or animations

Example: Browser Storage

<template>
  <ClientOnly>
    <div>
      <p>Last visit: {{ lastVisit }}</p>
    </div>
  </ClientOnly>
</template>

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

const lastVisit = ref('')

onMounted(() => {
  // Safe to use localStorage in mounted
  lastVisit.value = localStorage.getItem('lastVisit') || 'First visit!'
  localStorage.setItem('lastVisit', new Date().toISOString())
})
</script>

Dynamic & Lazy Components

Improve performance by lazy loading components when needed.

Lazy Component Prefix

<template>
  <div>
    <!-- Regular import (bundled immediately) -->
    <AppHeader />

    <!-- Lazy loaded (separate chunk) -->
    <LazyAppFooter />

    <!-- Only loaded when visible -->
    <LazyHeavyChart />
  </div>
</template>

Manual Dynamic Import

<template>
  <div>
    <button @click="showModal = true">Open Modal</button>

    <component
      v-if="showModal"
      :is="Modal"
      @close="showModal = false"
    />
  </div>
</template>

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

const showModal = ref(false)

// Only loaded when needed
const Modal = defineAsyncComponent(() =>
  import('~/components/Modal.vue')
)
</script>

Performance Benefits

Approach Bundle Use Case
<Component /> Main bundle Critical, above-the-fold components
<LazyComponent /> Separate chunk Below-the-fold, modal, tabs
defineAsyncComponent On-demand Conditional rendering, user interactions
Best Practice: Use Lazy prefix for components below the fold or loaded conditionally.

Component Best Practices

1. Use Auto Imports

<!-- ✅ Good - Auto imported -->
<template>
  <BaseButton>Click Me</BaseButton>
</template>

<!-- ❌ Bad - Manual import (unnecessary) -->
<template>
  <BaseButton>Click Me</BaseButton>
</template>

<script setup>
import BaseButton from '~/components/base/Button.vue' // Not needed!
</script>

2. Component Organization

components/
├── base/           # Reusable UI components
│   ├── Button.vue
│   └── Input.vue
├── layout/         # Layout components
│   ├── Header.vue
│   └── Footer.vue
├── feature/        # Feature-specific components
│   ├── UserProfile.vue
│   └── ProductCard.vue
└── App*.vue       # App-level components

3. Use Composables for Logic

<!-- composables/useAuth.ts -->
export const useAuth = () => {
  const user = useState('user', () => null)

  const login = async (credentials) => {
    // Login logic
  }

  const logout = () => {
    user.value = null
  }

  return { user, login, logout }
}

<!-- components/UserMenu.vue -->
<script setup>
const { user, logout } = useAuth() // Auto imported!
</script>

4. TypeScript Props

<!-- components/ProductCard.vue -->
<script setup lang="ts">
interface Props {
  title: string
  price: number
  image?: string
  featured?: boolean
}

const props = withDefaults(defineProps<Props>(), {
  image: '/placeholder.jpg',
  featured: false
})
</script>

<template>
  <div :class="{ featured: props.featured }">
    <img :src="props.image" :alt="props.title" />
    <h3>{{ props.title }}</h3>
    <p>${{ props.price }}</p>
  </div>
</template>
Pro Tip: Use the Nuxt DevTools to visualize your component tree and analyze bundle sizes!