Nuxt Guide
Components
Component naming, file structure, layouts, and icon patterns.
Naming Conventions
| Type | Convention | Example |
|---|---|---|
| Component files | PascalCase.vue | UserNavbar.vue, ResourceCard.vue |
| Shared (layer) | Prefix-free, consumed via auto-import | AlertBanner.vue |
| App-specific | Prefix by domain/feature | AdminUserList.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, nodefineComponent. - Type props with the runtime object form — always provide a concrete
PropType<T>, neverPropType<any>. defineEmitsuses the array form — keep it simple.- Avoid logic in templates — move it to
<script setup>or composables. - Prefer
v-bindshorthand when spreading attributes to child components.
Layouts
Layouts live in app/layouts/. Each app has at minimum a default.vue.
Sidebar Layout (dashboard apps)
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" />