Skip to content
UpVoiced Docs

UpVoiced Docs · Widget

Frameworks and site builders

Every option below adds the same script tag once per page. Replace your-slugwith your workspace slug, then list your site underallowed origins. Calling the widget from your code? See the JavaScript API and its TypeScript declaration.

Plain HTML and single-page apps

On a server-rendered site, add the tag to your shared layout or footer template. A single-page app (React, Vue, Svelte or Angular built with Vite, Create React App or the Angular CLI) has one index.html: the tag goes there and stays loaded while people navigate inside the app.

<!-- index.html (Vite, Create React App, Vue, Svelte, Angular): before </body> -->
<script async src="https://cdn.upvoiced.com/w.js" data-workspace="your-slug"></script>

React

index.html is simplest. To keep the slug in code (for example one per environment), add the tag from a component:

// FeedbackWidget.tsx: render it once, e.g. in your root component.
import { useEffect } from 'react';

const SRC = 'https://cdn.upvoiced.com/w.js';

export function FeedbackWidget({ workspace }: { workspace: string }) {
  useEffect(() => {
    // Once per page, also under React StrictMode (effects run twice in development).
    if (document.querySelector(`script[src="${SRC}"]`)) return;
    const script = document.createElement('script');
    script.async = true;
    script.src = SRC;
    script.dataset.workspace = workspace;
    document.body.appendChild(script);
  }, [workspace]);
  return null;
}

// <FeedbackWidget workspace="your-slug" />
// Open it from your own button: <button onClick={() => window.UpVoiced?.open()}>Feedback</button>

Next.js

Use next/script in the root layout so the widget loads once and survives client navigation. next/script passes data-* attributes to the tag.

// app/layout.tsx (App Router)
import Script from 'next/script';
import type { ReactNode } from 'react';

export default function RootLayout({ children }: { children: ReactNode }) {
  return (
    <html lang="en">
      <body>
        {children}
        <Script
          src="https://cdn.upvoiced.com/w.js"
          data-workspace="your-slug"
          strategy="afterInteractive"
        />
      </body>
    </html>
  );
}
// pages/_app.tsx (Pages Router)
import type { AppProps } from 'next/app';
import Script from 'next/script';

export default function App({ Component, pageProps }: AppProps) {
  return (
    <>
      <Component {...pageProps} />
      <Script
        src="https://cdn.upvoiced.com/w.js"
        data-workspace="your-slug"
        strategy="afterInteractive"
      />
    </>
  );
}

With a strict CSP in middleware, also pass the request’s nonce: <Script nonce={nonce} data-nonce={nonce} … /> (seestrict CSP).

Vue

// src/upvoiced.ts: a Vue 3 plugin that adds the widget once.
import type { App } from 'vue';

const SRC = 'https://cdn.upvoiced.com/w.js';

export const upvoiced = {
  install(_app: App, options: { workspace: string }) {
    if (typeof document === 'undefined') return; // server-side rendering
    if (document.querySelector(`script[src="${SRC}"]`)) return;
    const script = document.createElement('script');
    script.async = true;
    script.src = SRC;
    script.dataset.workspace = options.workspace;
    document.body.appendChild(script);
  },
};

// src/main.ts
// createApp(App).use(upvoiced, { workspace: 'your-slug' }).mount('#app');

Nuxt

// nuxt.config.ts (Nuxt 3)
export default defineNuxtConfig({
  app: {
    head: {
      script: [
        {
          src: 'https://cdn.upvoiced.com/w.js',
          async: true,
          'data-workspace': 'your-slug',
          tagPosition: 'bodyClose',
        },
      ],
    },
  },
});

Svelte

A Svelte app built with Vite can use index.html, or this component:

<!-- FeedbackWidget.svelte (Svelte 5, without SvelteKit) -->
<script lang="ts">
  import { onMount } from 'svelte';

  let { workspace }: { workspace: string } = $props();
  const SRC = 'https://cdn.upvoiced.com/w.js';

  onMount(() => {
    if (document.querySelector(`script[src="${SRC}"]`)) return;
    const script = document.createElement('script');
    script.async = true;
    script.src = SRC;
    script.dataset.workspace = workspace;
    document.body.appendChild(script);
  });
</script>

SvelteKit

Add the tag to src/app.html. With kit.csp configured, SvelteKit replaces %sveltekit.nonce% with each response’s nonce; without it, leave out both nonce attributes.

<!-- src/app.html (SvelteKit): after %sveltekit.body% -->
<body data-sveltekit-preload-data="hover">
  <div style="display: contents">%sveltekit.body%</div>
  <script
    async
    nonce="%sveltekit.nonce%"
    src="https://cdn.upvoiced.com/w.js"
    data-workspace="your-slug"
    data-nonce="%sveltekit.nonce%"
  ></script>
</body>

Angular

Add the tag to src/index.html before </body>. To open or close the widget from components, wrap the global in a service:

// src/app/feedback.service.ts (Angular 17+): the script tag goes in src/index.html.
import { Injectable } from '@angular/core';

@Injectable({ providedIn: 'root' })
export class FeedbackService {
  open(tab?: 'feedback' | 'roadmap' | 'whats-new'): void {
    window.UpVoiced?.open(tab);
  }

  close(): void {
    window.UpVoiced?.close();
  }
}
// window.UpVoiced is typed by the declaration file in the API reference (upvoiced.d.ts).

Webflow

  1. Open Site settings → Custom code and paste the snippet from Settings → Widget into Footer code (custom code needs a paid Webflow site plan). For one page only, use that page’s settings →Before </body> tag instead.
  2. Publish the site.
  3. Allow both origins you use, for example https://your-site.webflow.io and https://www.example.com. The Designer preview is a different origin: test on the published site.

WordPress

Without code: install a header-and-footer plugin (for example WPCode) and paste the snippet as a footer script on the whole site. WordPress.com sites need a plan that allows plugins or custom code. With code, enqueue the script from your theme:

<?php
// In your (child) theme's functions.php, or a small site plugin. WordPress 6.3+.
add_action('wp_enqueue_scripts', function () {
    wp_enqueue_script(
        'upvoiced-widget',
        'https://cdn.upvoiced.com/w.js',
        [],
        null,
        ['in_footer' => true, 'strategy' => 'async']
    );
});

add_filter('script_loader_tag', function ($tag, $handle) {
    if ($handle !== 'upvoiced-widget') {
        return $tag;
    }
    return str_replace(' src=', ' data-workspace="your-slug" src=', $tag);
}, 10, 2);

Caching and optimisation plugins that combine or defer scripts can break the data-workspace attribute: exclude cdn.upvoiced.com/w.js from them.