# Nuxt

> Use the Vue component in a Nuxt app — client-only rendering, the stylesheet, and avoiding layout shift.

The Vue component works in Nuxt with no module and no plugin. It is SSR-safe — every
browser API it touches (`window`, `navigator`, WebGL, `AudioContext`) is behind `onMounted`
— so importing it never breaks a server render. But it draws nothing on the server, so it
belongs inside [`<ClientOnly>`](https://nuxt.com/docs/api/components/client-only).

Prefer to see it running first? The [Nuxt playground](https://demo.fftvisualizer.com/nuxt/)
is this exact setup on a prerendered page.

## Install

```bash [pnpm]
pnpm add @fft-visualizer/vue
```

Then register the stylesheet in `nuxt.config.ts` — it sizes the canvas and styles the stats
overlay, and the component renders unstyled without it:

```ts [nuxt.config.ts]
export default defineNuxtConfig({
  css: ['@fft-visualizer/vue/style.css'],
})
```

## Quick start

```vue [app.vue]
<script setup>
import { FFTVisualizer } from '@fft-visualizer/vue'
</script>

<template>
  <div class="viz">
    <ClientOnly>
      <FFTVisualizer mode="local" :bands="40" gradient="aurora" />
    </ClientOnly>
  </div>
</template>

<style>
/* The component fills its container — give it a height */
.viz { width: 100%; height: 240px; }
</style>
```

`mode="local"` captures the microphone and computes the FFT in the browser, so this needs no
backend. The browser prompts for permission on mount.

<note>

Give the wrapper a height *outside* `<ClientOnly>`, as above. If the sized element only
exists after hydration, the page reflows the moment the visualizer appears.

</note>

## Avoiding layout shift

`<ClientOnly>` renders nothing on the server, which leaves a gap until hydration. Its
`#fallback` slot fills that gap with server-rendered markup:

```vue
<ClientOnly>
  <FFTVisualizer mode="local" :bands="40" />
  <template #fallback>
    <div class="viz-placeholder" />
  </template>
</ClientOnly>
```

Hydration replaces the entire `<ClientOnly>` subtree, so anything you want to survive that
swap — a heading, a toolbar — belongs outside it, or in *both* branches.

## Using the core directly

If you skip the component and drive [`@fft-visualizer/core`](/core/api) yourself, construct it
in `onMounted` and guard any module-scope browser access with `import.meta.client`:

```vue
<script setup>
import { FFTVisualizer } from '@fft-visualizer/core'

const canvas = useTemplateRef('canvas')
let viz

onMounted(() => {
  viz = new FFTVisualizer(canvas.value, { mode: 'local', bands: 40 })
})

onBeforeUnmount(() => viz?.destroy())
</script>

<template>
  <canvas ref="canvas" />
</template>
```

<warning>

The canvas is a **constructor argument**, not a later `init()` call — the class has no such
method, and `connect()` runs automatically on construction. See the
[API reference](/core/api) for the full surface.

</warning>
