Skip to content

SSR / Next.js / Nuxt

TokenFlight widgets are Web Components that require the browser DOM. They must initialize in a Client Component because they access the DOM, customElements, and browser APIs like window.ethereum. The package can be safely imported in SSR environments — all browser-specific code is guarded with typeof window !== "undefined" checks.

Next.js (App Router)

Use the 'use client' directive and mount the widget in a useEffect:

tsx
'use client';

export function PaymentWidget({ wagmiConfig }) {
  const containerRef = useRef<HTMLDivElement>(null);

  useEffect(() => {
    if (!containerRef.current) return;

    const adapter = new WagmiWalletAdapter(wagmiConfig);
    const widget = new TokenFlightWidget({
      container: containerRef.current,
      config: {
        toToken: { chainId: 8453, address: '0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913' },
        tradeType: 'EXACT_OUTPUT',
        amount: '100',
        theme: 'dark',
      },
      walletAdapter: adapter,
      callbacks: {
        onSwapSuccess: (data) => console.log('Success:', data),
      },
    });

    widget.initialize();
    return () => widget.destroy();
  }, [wagmiConfig]);

  return <div ref={containerRef} style={{ minHeight: 560 }} />;
}

Then use it in any page or layout:

tsx
// app/page.tsx

export default function Page() {
  return <PaymentWidget wagmiConfig={wagmiConfig} />;
}

Next.js (Pages Router)

Use dynamic with ssr: false to skip server rendering entirely:

tsx

const PaymentWidget = dynamic(
  () => import('../components/PaymentWidget').then((mod) => mod.PaymentWidget),
  { ssr: false }
);

export default function Page() {
  return <PaymentWidget />;
}

See Next.js Example for a complete setup.

Nuxt 3

Use the <ClientOnly> component:

vue
<template>
  <ClientOnly>
    <PaymentWidget />
  </ClientOnly>
</template>

<script setup>

</script>

Or use a .client.vue suffix — Nuxt automatically skips SSR for these components:

components/
  PaymentWidget.client.vue   ← Only rendered on the client

See Vue Example for the full component implementation.

Remix

Use remix-utils ClientOnly or lazy-load in a useEffect:

tsx

export default function PaymentPage() {
  const containerRef = useRef<HTMLDivElement>(null);

  useEffect(() => {
    let widget: { destroy(): void } | undefined;

    import('@tokenflight/swap').then(({ TokenFlightWidget }) => {
      if (!containerRef.current) return;
      widget = new TokenFlightWidget({
        container: containerRef.current,
        config: {
          toToken: { chainId: 8453, address: '0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913' },
          tradeType: 'EXACT_OUTPUT',
          amount: '100',
          theme: 'dark',
        },
      });
      widget.initialize();
    });

    return () => widget?.destroy();
  }, []);

  return <div ref={containerRef} style={{ minHeight: 560 }} />;
}

Astro

Astro <script> tags run on the client by default — no special handling needed:

astro
---
// This runs on the server — no browser APIs here
---

<div id="widget" style="min-height: 560px;"></div>

<script>
  import { TokenFlightWidget } from '@tokenflight/swap';
  import { registerWidgetElement } from '@tokenflight/swap/widget';

  registerWidgetElement();

    const widget = new TokenFlightWidget({
      container: '#widget',
      config: {
        toToken: { chainId: 8453, address: '0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913' },
        tradeType: 'EXACT_OUTPUT',
        amount: '100',
        theme: 'dark',
      },
    });
  widget.initialize();
</script>

SvelteKit

Use onMount which only runs on the client:

svelte
<script>
  import { onMount } from 'svelte';

  let container;

  onMount(async () => {
    const { TokenFlightWidget } = await import('@tokenflight/swap');
    const { registerWidgetElement } = await import('@tokenflight/swap/widget');
    registerWidgetElement();

    const widget = new TokenFlightWidget({
      container,
      config: {
        toToken: { chainId: 8453, address: '0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913' },
        tradeType: 'EXACT_OUTPUT',
        amount: '100',
        theme: 'dark',
      },
    });
    widget.initialize();

    return () => widget.destroy();
  });
</script>

<div bind:this={container} style="min-height: 560px;"></div>

Static Site Generation (SSG)

SSG works with all the above patterns. The widget simply hydrates on the client after the static HTML loads. No additional configuration needed.

Key Points

  • Safe to import on the server — no side effects until window is available
  • Always mount in a client-side lifecycle hookuseEffect, onMount, onMounted, etc.
  • Call destroy() in cleanup — prevents memory leaks on navigation
  • Cache adapter instances — avoid recreating on every render (use useMemo, ref, etc.)