Skip to content

Vue Router and Nuxt โ€‹

The components render plain anchors unless you hand them your router's link component. They hold no global state, so they render on the server as they do in the browser.

View as Markdown

Components that render navigation take a linkComponent prop. Without it, a link is a plain <a> and following it reloads the page. With it, the component renders yours instead and passes the destination as to.

These components take linkComponent:

ComponentRenders links for
I9kButton, I9kIconButtonto
I9kNavigation, I9kNavMenuthe site links
I9kSideNavthe section links
I9kBreadcrumbevery item but the last
I9kFootersite-relative links in its columns
I9kAnnouncementBar, I9kProfileMenu, I9kLanguageSwitchertheir links

Pass the imported component, not its name, so it resolves wherever the component renders:

vue
<script setup lang="ts">
import { RouterLink } from 'vue-router';
import { I9kSideNav } from '@9klabs/design';
</script>

<template>
  <I9kSideNav label="Admin sections" :links="links" :link-component="RouterLink" />
</template>

I9kButton and I9kIconButton also accept the globally registered name, which is the shortest way to write a one-off link:

vue
<I9kButton to="/pricing" link-component="RouterLink">See pricing</I9kButton>

In Nuxt, pass NuxtLink the same way. It is auto-imported in templates, so :link-component="NuxtLink" needs no import in a page or component.

Decide the current page in the caller

I9kSideNav and I9kBreadcrumb never read the router. Set current on a side-nav link, or put the current page last in the breadcrumb, from your route. That way a nested page, such as an editor under Courses, can still mark its section.

Notifications, once per app โ€‹

Notifications go through one store per app. Install it where you create the app, and render one <I9kToaster /> at the root, before the page content:

ts
// main.ts
import { createI9kToaster } from '@9klabs/design';

app.use(createI9kToaster({ labels: { region: 'Notifications', dismiss: 'Dismiss' } }));
ts
// plugins/toaster.ts in Nuxt
import { createI9kToaster } from '@9klabs/design';

export default defineNuxtPlugin((nuxtApp) => {
  nuxtApp.vueApp.use(createI9kToaster());
});

The store is created per app, never as a module singleton, so two server-rendered requests never share notifications. Notifications and dialogs covers the rest.

Server rendering โ€‹

Every component renders on the server. Anything that needs the browser waits until the component is mounted: matchMedia, timers, the dialog API, and Nino's maze seed when none is given. In practice:

  • No hydration surprises from viewport checks. I9kSidebarLayout renders its wide layout on the server and switches to the drawer once mounted on a narrow screen. I9kNavigation lays out with CSS alone.
  • Teleports go to <body>. I9kNavMenu teleports its panel there, which Nuxt and VitePress place correctly. A hand-rolled server renderer must put body teleports at the very start of <body>.
  • The theme class is yours. The components follow the dark or light class on <html>. Set it before the first paint so a dark page never flashes light. See Theming and dark mode.

Made with love ๐Ÿ’š, caffeine, and Claude Code. Built with @9klabs/design itself.