React, Next.js & other frameworks

Drop a Vuesax element into Next.js, React, Nuxt, Vue, Svelte, Angular or Astro — typed, server-rendered, no glue code.

Getting started 3 min read

TL;DR · one file, every framework

  • Every vs-*.js file is safe to import on the server. Next.js, Nuxt, Astro and SvelteKit render the page without a single guard.
  • React and Next.js get a generated wrapper (VsX.tsx) and TypeScript types (vs-x.d.ts) next to the code.
  • Everything else needs one line of setup at most.

Every item is a native custom element. The element itself is the same in every framework; what changes is how your framework passes arrays, listens to events and type-checks the tag. This page covers each one.

What you get

The code panel of every entry (and the MCP server) hands you:

FileWhat it isWho needs it
vs-x.jsThe component. Zero dependencies, occasionally with a sibling vs-*.js it reuses.Everyone
vs-x.d.tsTypeScript types: the element, its attributes, its events, and the HTMLElementTagNameMap entry.TypeScript projects
VsX.tsxA React wrapper: typed props, on… event props, arrays and objects passed as properties.React and Next.js

Keep them together in one folder — the wrapper imports the .js and the .d.ts by relative path.

Next.js

Works in the App Router and the Pages Router, on React 18 and 19.

  1. Put the files in a folder, for example components/vuesax/:
components/vuesax/
  vs-nav-menu.js
  vs-fx.js            ← sibling it imports (when there is one)
  vs-nav-menu.d.ts
  VsNavMenu.tsx
  1. Render the wrapper — from a Server Component, a Client Component or a page:
// app/page.tsx — a Server Component is fine
import { VsNavMenu } from '@/components/vuesax/VsNavMenu';

const items = [
  { value: 'docs', label: 'Docs', layout: 'list', links: [{ title: 'Install', desc: 'Guides' }] },
  { value: 'blog', label: 'Blog', href: '/blog' },
];

export default function Page() {
  return <VsNavMenu variant="ghost" items={items} />;
}

That’s it. No dynamic(() => import(…), { ssr: false }), no useEffect import, no custom declare global block.

  • The wrapper is a client component (it starts with 'use client'), so event props work: onSelect={(e) => router.push(e.detail.item.href)} inside any client component.
  • The server renders <vs-nav-menu variant="ghost"> with its attributes; the browser upgrades it when the script loads.
  • ref gives you the element itself, typed as VsNavMenuElement.
Why a wrapper at all? React 18 turns an array prop on a custom element into the string "[object Object]" and never listens to custom events. React 19 fixes part of that, but not booleans that default to on. The wrapper writes every prop to the element with its real type and binds on… props to the element's events, so the same code behaves the same on both.

React (Vite, Remix, CRA)

Exactly the same files and the same usage as Next.js — the 'use client' line is ignored outside Next.

import { VsButton } from './vuesax/VsButton';

<VsButton label="Save" variant="primary" onClick={save} />

If you prefer the raw tag, import the file once and use <vs-button> directly; set arrays through a ref (ref.current.items = items) and listen with addEventListener.

Nuxt and Vue

Tell the template compiler that vs-* tags are custom elements, import the file once, and use the tag:

// nuxt.config.ts
export default defineNuxtConfig({
  vue: { compilerOptions: { isCustomElement: (tag) => tag.startsWith('vs-') } },
});

// Vue + Vite: vite.config.ts
vue({ template: { compilerOptions: { isCustomElement: (tag) => tag.startsWith('vs-') } } });
<script setup>
import './vuesax/vs-nav-menu.js'; // SSR-safe, no <ClientOnly> needed
</script>

<template>
  <vs-nav-menu variant="ghost" :items.prop="items" @select="onSelect" />
</template>

.prop sends arrays and objects as properties; @event listens to the element’s events.

Svelte and SvelteKit

<script>
  import './vuesax/vs-nav-menu.js';
</script>

<vs-nav-menu variant="ghost" items={items} onselect={onSelect}></vs-nav-menu>

Svelte 5 sets arrays and objects as properties automatically. On Svelte 4 use on:select.

Angular

@Component({
  selector: 'app-nav',
  schemas: [CUSTOM_ELEMENTS_SCHEMA],
  template: `<vs-nav-menu variant="ghost" [items]="items" (select)="onSelect($event)"></vs-nav-menu>`,
})
export class NavComponent {}

Import the file once, in main.ts or the component.

Astro

<vs-nav-menu variant="ghost"></vs-nav-menu>

<script>
  import '../vuesax/vs-nav-menu.js';
  document.querySelector('vs-nav-menu').items = [/* … */];
</script>

No client:only needed.

Plain HTML

<script type="module" src="./vs-nav-menu.js"></script>
<vs-nav-menu variant="ghost"></vs-nav-menu>

Server-side rendering, in detail

A server has no DOM, so a component file that touched HTMLElement or customElements the moment it was imported would crash the page before it rendered. Every Vuesax file is written so that importing it on a server does nothing at all:

  • the element class falls back to an empty base class when HTMLElement is missing;
  • customElements.define only runs when a registry exists — and never twice, so hot reload and duplicate bundles don’t throw;
  • window listeners and other browser-only setup wait for a browser.

Each file is checked by importing it in bare Node before it ships. The page renders the tag with its attributes; the element comes alive when the browser runs the script.

A few animations draw with WebGL. They render nothing on the server and start the moment the browser upgrades them — there’s nothing to wrap.

Three animations use three Almost every file is zero-dependency. The three that build on three.js say so at the top of the file and import it by name — run npm install three once if you add one of them.