Nuxt Guide

Components

Component naming, file structure, layouts, and icon patterns.

Naming Conventions

TypeConventionExample
Component filesPascalCase.vueUserNavbar.vue, ResourceCard.vue
Shared (layer)Prefix-free, consumed via auto-importAlertBanner.vue
App-specificPrefix by domain/featureAdminUserList.vue, OwnerCalendar.vue

All components are auto-imported by Nuxt — no explicit import statements needed.

Component Structure

All components use the Composition API with <script setup>. The <script> block always comes before the template:

<script lang="ts" setup>
const props = defineProps({
  item:         { type: Object as PropType<IResource>, required: true },
  showActions:  { type: Boolean, default: true },
})

const emit = defineEmits(['edit', 'delete'])

function handleEdit() {
  emit('edit', props.item._id)
}
</script>

<template>
  <div class="card">
    <div class="card-body">
      <h2 class="card-title">{{ item.name }}</h2>
      <p class="text-sm">{{ item.description }}</p>
      <div v-if="showActions" class="card-actions justify-end">
        <button class="btn btn-sm btn-primary" @click="handleEdit">Edit</button>
      </div>
    </div>
  </div>
</template>

Key Rules

  • Always use <script lang="ts" setup> — no Options API, no defineComponent.
  • Type props with the runtime object form — always provide a concrete PropType<T>, never PropType<any>.
  • defineEmits uses the array form — keep it simple.
  • Avoid logic in templates — move it to <script setup> or composables.
  • Prefer v-bind shorthand when spreading attributes to child components.

Layouts

Layouts live in app/layouts/. Each app has at minimum a default.vue.

Uses DaisyUI drawer for a collapsible sidebar:

<!-- app/layouts/default.vue -->
<template>
  <div class="drawer lg:drawer-open min-h-screen">
    <input id="my-drawer" type="checkbox" class="drawer-toggle" />

    <div class="drawer-content flex flex-col">
      <!-- Mobile top bar -->
      <div class="navbar lg:hidden bg-base-100 border-b">
        <label for="my-drawer" class="btn btn-ghost">
          <PhList class="w-5 h-5" />
        </label>
      </div>

      <main class="bg-base-100 flex-1 px-7 py-6">
        <slot />
      </main>
    </div>

    <!-- Sidebar -->
    <div class="drawer-side z-50">
      <label for="my-drawer" class="drawer-overlay" />
      <aside class="w-64 min-h-screen bg-base-200 p-4">
        <NavigationItems />
      </aside>
    </div>
  </div>
</template>

Top Navbar Layout (public / customer apps)

<!-- app/layouts/default.vue -->
<template>
  <header class="sticky top-0 z-50 bg-white duration-300" :class="{ shadow: isScrolled }">
    <AppNavbar />
  </header>
  <main>
    <slot />
  </main>
</template>

<script lang="ts" setup>
const isScrolled = ref(false)

onMounted(() => window.addEventListener('scroll', handleScroll))
onBeforeUnmount(() => window.removeEventListener('scroll', handleScroll))

function handleScroll() {
  isScrolled.value = window.scrollY > 0
}
</script>

Icon System

Icons use Phosphor Icons (@phosphor-icons/vue). Register them globally in a plugin so individual components don't need to import them:

// app/plugins/phosphor-icons.ts
import {
  PhEye, PhCaretLeft, PhMagnifyingGlass, PhList,
  PhPencilSimple, PhTrash, PhPlus, PhX, PhCheck,
} from '@phosphor-icons/vue'

export default defineNuxtPlugin((nuxtApp) => {
  const icons: Record<string, Component> = {
    PhEye, PhCaretLeft, PhMagnifyingGlass, PhList,
    PhPencilSimple, PhTrash, PhPlus, PhX, PhCheck,
  }
  Object.entries(icons).forEach(([name, component]) => {
    nuxtApp.vueApp.component(name, component)
  })
})

Add new icons here when needed — don't import them locally in individual components.

Usage in templates:

<PhMagnifyingGlass class="w-5 h-5 text-base-content/50" />
<PhTrash class="w-4 h-4" />