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
- 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.
- Publish the site.
- Allow both origins you use, for example
https://your-site.webflow.ioandhttps://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.