This episode covers reusable components with Single File Components, layouts for shared page scaffolding, nested layouts, slots for flexible component composition, and styling with CSS Modules, Tailwind, and component-scoped styles.

A good page is built from small, reusable pieces. Episode 5 focuses on the two main tools for that: components for reusable UI elements, and layouts for page scaffolding shared across pages.
The problems we're solving: the header and footer should not be rewritten on every page, a product card should be a single component used in many places, and styles must be isolated so they don't override each other. After this episode, you will understand the Vue composition patterns that are the foundation of large-scale applications.
The app/components folder is dedicated to reusable components, and they are all auto-imported. Let's create a product card component:
<script setup lang="ts">
defineProps<{
nama: string
harga: number
}>()
</script>
<template>
<article class="kartu">
<h2>{{ nama }}</h2>
<p>Rp {{ harga }}</p>
</article>
</template>defineProps<{ nama: string }>() declares the props the component accepts with full type checking. This component can be used on any page right away, no import needed.
Use component names of two words or more, for example KartuProduk, not Kartu. A single-word name risks colliding with native HTML elements. Nuxt also understands nested folder structures — components/kartu/Header.vue can be used as <KartuHeader />.
A layout is a scaffold that wraps page content. The default layout holds a header, <NuxtPage />, and a footer:
<template>
<div>
<header>Header Toko</header>
<main>
<NuxtPage />
</main>
<footer>Footer Toko</footer>
</div>
</template><NuxtPage /> renders the active page inside the layout. Every page that doesn't specify another layout automatically uses default.
Create a second layout, e.g. app/layouts/clean.vue, then register it on a specific page:
<script setup lang="ts">
definePageMeta({ layout: "clean" })
</script>definePageMeta({ layout: "clean" }) makes that page render with the clean layout. For nested layouts — a layout inside a layout — simply wrap <NuxtPage /> with another layout layer by layer.
Slots allow a component to accept content from outside. The KotakInfo component can be used with different content each time:
<template>
<div class="kotak-info">
<slot />
</div>
</template><KotakInfo>
<p>Promo gratis ongkir minggu ini.</p>
</KotakInfo><slot /> is the insertion point for the component's child content. This pattern makes components flexible without needing endless props.
For multiple areas within a single component, use named slots. Slots can also send data back to the consumer through slot props. You'll use the full details when building form components in episode 10.
The simplest and safest approach: add the scoped attribute so the CSS only applies to that component:
<style scoped>
.kartu {
border: 1px solid #ddd;
padding: 1rem;
}
</style>scoped makes Nuxt add a unique attribute to the component's elements and write matching CSS selectors, so h2 in this component does not affect h2 elsewhere.
For more explicit styling, enable CSS Modules:
<style module>
.kartu {
background: #f5f5f5;
}
</style><template>
<article :class="$style.kartu">
Konten kartu
</article>
</template>With module, classes are accessed through $style.kartu and hashed so they don't collide across components.
For utility-first styling, install the Tailwind module:
npm install --save-dev @tailwindcss/viteimport tailwindcss from "@tailwindcss/vite"
export default defineNuxtConfig({
vite: {
plugins: [tailwindcss()],
},
})After that you can write utility classes directly in the template, for example class="border p-4". The choice between scoped styles, CSS Modules, or Tailwind should stay consistent within one project.
Episode 5 gives you a complete set of UI composition tools: reusable components with auto-import and props, layouts for shared page scaffolding, slots for flexibility, and three styling approaches you can pick from based on project needs.
Key takeaways:
app/components are auto-imported and should be named with at least two words.default.vue is used by all pages automatically.definePageMeta({ layout: "nama" }) switches a page to another layout.scoped in <style> isolates CSS per component.$style and Tailwind is added through a Vite plugin.In the next episode, episode 6, we will discuss data fetching and server actions — fetching data with useAsyncData and useFetch, writing server actions for forms, understanding the difference between server and client fetches, and caching and revalidating data. Your store will start consuming real data.