The Document Object Model (DOM) is a widely used programming interface for web documents. It represents the page so that programs can change the document structure, style, and content. The DOM represents the document as nodes and objects; this way, programming languages like JavaScript can interact with the page. DOM updates are handled by Vue's reactivity system.
-
Reactivity: Vue's reactivity system automatically updates the DOM when the underlying data changes, eliminating the need for manual DOM manipulation.
-
Props and Events: Components communicate through props (parent-to-child data flow) and events (child-to-parent communication), creating a clear data flow throughout the application.
-
Slots: Vue's slot system allows components to receive and render content from their parent components, enabling flexible component composition.
-
Directives: Special attributes with the
v-prefix (likev-if,v-for,v-model) that apply special reactive behavior to rendered DOM elements. -
Transitions and Animations: Vue provides built-in transition components that make it easy to apply animations when elements are inserted, updated, or removed from the DOM.
Components are the building blocks of the user interface and Vue application.
A Vue component encapsulates a template, its logic and the style:
- A Template: The HTML structure that defines what the component looks like, enclosed in
<template></template>tags. - Its Logic: JavaScript code that controls the component's behavior between
<script setup lang="ts"></script>. - The Style: CSS that defines the component's appearance. Typically omitted in favor of global styling, but can be added between
<style lang="postcss"></style>.
Components can be nested within each other, creating a tree-like structure that represents your application's UI. This modular approach promotes reusability, maintainability, and separation of concerns.
Slots are a Vue feature that allows you to create reusable components with customizable content areas. They act as placeholders where parent components can inject their own content, enabling flexible component composition.
A basic slot creates a "hole" in your component where parent content can be inserted:
<template>
<div class="container">
<h1>Fixed Header</h1>
<slot></slot> <!-- Parent content goes here -->
<footer>Fixed Footer</footer>
</div>
</template><template>
<ChildComponent>
<p>This content goes into the slot!</p>
</ChildComponent>
</template>Named slots allow you to create multiple content areas within a single component. This is particularly useful for complex layouts like navigation components.
Defining named slots in a component:
<template>
<NSidebar>
<NSidebarHeader>
<slot name="header">
<!-- Default header content -->
<h2>Default Header</h2>
</slot>
</NSidebarHeader>
<NSidebarContent>
<slot name="content">
<!-- Default content -->
<p>Default content area</p>
</slot>
</NSidebarContent>
<NSidebarFooter>
<slot name="footer">
<!-- Default footer -->
<p>Default footer</p>
</slot>
</NSidebarFooter>
</NSidebar>
</template>Using named slots from a parent component:
<template>
<NavigationSidebarMain>
<template #header>
<CustomHeader />
</template>
<template #content>
<CustomContent />
</template>
<template #footer>
<CustomFooter />
</template>
</NavigationSidebarMain>
</template>The #header syntax is shorthand for v-slot:header.
You can have multiple named slots in one component:
<!-- Enhanced NavigationSidebarMain.vue -->
<template>
<NSidebar>
<NSidebarHeader>
<slot name="header">
<!-- Default header -->
</slot>
</NSidebarHeader>
<NSidebarContent>
<slot name="content">
<!-- Default content -->
</slot>
</NSidebarContent>
<NSidebarFooter>
<slot name="footer">
<!-- Default footer -->
</slot>
</NSidebarFooter>
</NSidebar>
</template>Usage:
<NavigationSidebarMain>
<template #header>
<CustomHeader />
</template>
<template #content>
<CustomContent />
</template>
<template #footer>
<CustomFooter />
</template>
</NavigationSidebarMain>You can also make slot content conditional:
<template>
<NavigationSidebarMain>
<template #secondary-sidebar-header>
<component :is="currentSidebarHeader" />
</template>
</NavigationSidebarMain>
</template>
<script setup>
const route = useRoute()
// Dynamically choose component based on route
const currentSidebarHeader = computed(() => {
switch (route.name) {
case 'profile':
return defineAsyncComponent(() => import('~/components/profile/ProfileSidebarHeader.vue'))
case 'settings':
return defineAsyncComponent(() => import('~/components/settings/SettingsSidebarHeader.vue'))
default:
return null // Use default slot content
}
})
</script>In the directory tree, components are found in app/components.
Nuxt automatically imports any components in this directory alongside with components that are registered by modules - like those of our UI library.
Suppose we have the following nested directory structure inside the components folder:
app
├── components
│ └── frontpage
│ ├── Footer.vue
│ └── Header.vueIn our app, we reference the components in PascalCase based on their path directory and filename:
<template>
<div>
<FrontpageHeader />
<NuxtPage />
<FrontpageFooter />
</div>
</template>If you incorrectly reference a component, you will get a corresponding warning in the console.
You can optionally prefix a component name with Lazy, e.g. <LazyFrontpageFooter hydrate-on-visible />, which improves performance by deferring hydration of components until they're needed.
Because duplicate segments are removed, both of these components would resolve to <SubdirMenuButton />:
app
├── components
│ └── subdir
│ └── menu
│ ├── Button.vue
│ └── SubdirMenuButton.vueWhen launching the application, you would thus see a warning:
⚠️ WARN Two component files resolving to the same name SubdirMenuButton
For clarity, the Nuxt developers recommend that the component's filename matches its full name. So, in the example above, SubdirMenuButton.vue would be the appropriate name.
In our Nuxt application, we leverage these Vue concepts along with additional UI libraries like UnaUI and Reka UI to create a cohesive and responsive user interface.
Users can choose the primary and secondary (gray) colors in Settings. Built-in themes come from Una UI (useUnaThemes()). The app adds a few custom palettes (e.g. the "ngi" primary) that are defined in app/config/theme.ts.
The composable useFirnThemes extends Una’s theme lists with these palettes and keeps a separate theme override (persisted). When a user selects a custom theme, only the override is set; the library’s settings.primary stays at a built-in value. The client plugin app/plugins/theme-custom.client.ts runs after Una’s theme plugin and writes the custom palette’s CSS variables to :root, so the UI shows the custom colors.
In app/config/theme.ts, define objects with keys 50 through 950 and hex strings (same as Una UI extended-colors). Use the exported ThemeShades type and the paletteToPrimaryCssVars() helper to build the CSS variable map.
Use the same theme key in all four places below.
app/config/theme.ts— Add the palette (e.g.export const ngi: ThemeShades = { ... }) and add the name toCUSTOM_PRIMARY_THEMES(e.g.['ngi']).uno.config.ts— InextendTheme, addtheme.colors.<name> = { ...palette }so utilities likebg-<name>-500work.app/composables/useFirnThemes.ts— Build the CSS var map withpaletteToPrimaryCssVars(palette)and append[name, cssVars]toprimaryThemes. UpdateeffectivePrimaryThemeHexandgetEffectivePrimaryCssVars()to handle the new name.app/plugins/theme-custom.client.ts— In the condition that checksthemeOverride.value.primary, add a branch for your theme name (e.g.if (themeOverride.value.primary === 'ngi')) and apply the same palette withpaletteToPrimaryCssVars(palette). If you rename a palette, this plugin must be updated to use the new name; otherwise the custom theme will not be applied.- The Settings page uses the extended list; no change needed there.
The following list highlights a few features of our UI libraries that you may use quite frequently.
<NSidebarMenuButton
:tooltip="h('div', { hidden: false }, item.title)"
:is-active=" // some condition that evaluates to Boolean"
@click="() => { // a function triggered when clicking on the button}"
>Named slots allow you to create multiple content areas within a single component. This is particularly useful for complex layouts like navigation components.
Defining named slots in a component:
<template>
<NSidebar>
<NSidebarHeader>
<slot name="header">
<!-- Default header content -->
<h2>Default Header</h2>
</slot>
</NSidebarHeader>
<NSidebarContent>
<slot name="content">
<!-- Default content -->
<p>Default content area</p>
</slot>
</NSidebarContent>
<NSidebarFooter>
<slot name="footer">
<!-- Default footer -->
<p>Default footer</p>
</slot>
</NSidebarFooter>
</NSidebar>
</template>Using named slots from a parent component:
<template>
<NavigationSidebarMain>
<template #header>
<CustomHeader />
</template>
<template #content>
<CustomContent />
</template>
<template #footer>
<CustomFooter />
</template>
</NavigationSidebarMain>
</template>The #header syntax is shorthand for v-slot:header.
For different pages, create specific components for slot content:
<template>
<div class="flex items-center gap-2">
<NIcon name="i-lucide-user" />
<h2>Profile Settings</h2>
<NButton size="sm">Edit</NButton>
</div>
</template><template>
<div class="flex items-center gap-2">
<NIcon name="i-lucide-settings" />
<h2>System Settings</h2>
<NButton size="sm" variant="outline">Reset</NButton>
</div>
</template>