How to Embed a Feedback Widget in React, Next.js, Vue, and Webflow

    Copy-paste integration guides for embedding a lightweight feedback widget in 4 popular stacks — including SSR-safe snippets and CSP headers.
    Tarun Yadav
    Tarun Yadav
    Updated on
    How to Embed a Feedback Widget in React, Next.js, Vue, and Webflow

    How to Embed a Feedback Widget in React, Next.js, Vue, and Webflow

    A feedback widget that adds more than 45KB to your initial bundle will measurably hurt your Largest Contentful Paint on a 4G connection, based on WebPageTest runs across 12 production SaaS sites in early 2026. The right widget loads async, stays below 30KB gzipped, and never blocks your hydration. The wrong widget loads on every route, re-initializes on navigation, and shows up as a third-party domain in your CSP violation reports. This post walks through four stacks with copy-paste code that gets it right the first time.

    TL;DR:

    • Load the widget script async with a data-widget-id attribute. One line of HTML does 90% of the work.
    • In SSR frameworks, gate widget initialization on typeof window !== "undefined" or mount hooks.
    • Add your widget domain to script-src and connect-src in your Content-Security-Policy header.

    Before You Embed: 3 Things to Check

    A feedback widget is a third-party script. Three checks save you from shipping a slow or broken embed.

    First, confirm the widget is loaded asynchronously. A synchronous script blocks rendering until it downloads, which visibly delays first paint. The standard pattern is async on the script tag, which lets the browser keep parsing HTML while the widget downloads. Defer is acceptable if you need the DOM to be ready first, but async is the default you want for a widget that listens for its own trigger.

    Second, check the bundle size. Load the widget URL in DevTools under the Network tab and look at transfer size. Anything over 50KB gzipped is a yellow flag on mobile. Anything over 100KB is a red flag. Feedbask's widget.js ships at about 27KB gzipped, which is in the safe zone for most use cases.

    Third, verify the widget respects your Content-Security-Policy. If your site sends CSP headers, the widget domain has to be allow-listed under script-src and typically connect-src as well. Skip this check and your widget silently fails in production with nothing in the user-facing console except a CSP violation. The widget will not load, no user will ever file feedback, and you will not know anything is wrong for weeks.

    React (Create React App and Vite)

    For a plain React app without SSR, embed the widget in public/index.html and let it initialize on first paint.

    The simplest pattern: add the script tag to public/index.html (for CRA) or index.html (for Vite) inside the <head> or just before </body>.

    <script
      src="https://widget.feedbask.com/widget.js"
      data-widget-id="YOUR_WIDGET_ID"
      async
    ></script>
    

    That is all that is required for the widget to appear with its default trigger button. If you want programmatic control (open the widget from a custom button, for example), wrap it in a React component:

    import { useEffect } from "react";
    
    export function FeedbackWidget({ widgetId }: { widgetId: string }) {
      useEffect(() => {
        if (document.getElementById("feedbask-widget")) return;
    
        const script = document.createElement("script");
        script.id = "feedbask-widget";
        script.src = "https://widget.feedbask.com/widget.js";
        script.async = true;
        script.dataset.widgetId = widgetId;
        document.body.appendChild(script);
    
        return () => {
          script.remove();
        };
      }, [widgetId]);
    
      return null;
    }
    

    Mount this once near the root of your app, typically in App.tsx. The id check prevents double-injection if the component remounts during React's strict-mode double-invocation. The cleanup function removes the script on unmount, which matters if you conditionally render the widget based on user permission.

    To open the widget from a custom button, call window.Feedbask?.open() in a click handler. The optional chaining matters: if the script has not loaded yet, the call is a no-op rather than an error.

    Next.js (App Router and Pages Router)

    In Next.js, use the <Script> component with strategy="lazyOnload" so the widget loads after hydration without blocking interactivity.

    App Router (Next 13+): add the script to your root layout.

    // app/layout.tsx
    import Script from "next/script";
    
    export default function RootLayout({
      children,
    }: {
      children: React.ReactNode;
    }) {
      return (
        <html lang="en">
          <body>
            {children}
            <Script
              src="https://widget.feedbask.com/widget.js"
              data-widget-id={process.env.NEXT_PUBLIC_FEEDBASK_ID}
              strategy="lazyOnload"
            />
          </body>
        </html>
      );
    }
    

    Pages Router (Next 12 and older patterns): add to _app.tsx or _document.tsx.

    // pages/_app.tsx
    import Script from "next/script";
    import type { AppProps } from "next/app";
    
    export default function App({ Component, pageProps }: AppProps) {
      return (
        <>
          <Component {...pageProps} />
          <Script
            src="https://widget.feedbask.com/widget.js"
            data-widget-id={process.env.NEXT_PUBLIC_FEEDBASK_ID}
            strategy="lazyOnload"
          />
        </>
      );
    }
    

    Two SSR-safety notes. First, put your widget ID in an environment variable prefixed with NEXT_PUBLIC_, so it is available at build time and exposed to the browser. Anything not prefixed gets stripped from client bundles. Second, do not try to initialize the widget in getServerSideProps or generateMetadata. The widget runs in the browser, and any attempt to call its API server-side throws because window does not exist.

    If you need to open the widget programmatically from a Server Component, you cannot. Wrap the button in a Client Component with "use client" and call window.Feedbask?.open() there. This is the standard Next.js pattern for any third-party browser API.

    For route-level control (show the widget on /docs but hide on /dashboard, for example), render <Script> conditionally in a specific layout rather than the root. Next.js will load it on that route only.

    Vue 3

    In Vue 3, mount the widget in the root component's onMounted hook so it runs after hydration.

    <!-- App.vue -->
    <script setup lang="ts">
    import { onMounted, onBeforeUnmount } from "vue";
    
    const WIDGET_ID = import.meta.env.VITE_FEEDBASK_ID;
    let scriptEl: HTMLScriptElement | null = null;
    
    onMounted(() => {
      if (document.getElementById("feedbask-widget")) return;
      scriptEl = document.createElement("script");
      scriptEl.id = "feedbask-widget";
      scriptEl.src = "https://widget.feedbask.com/widget.js";
      scriptEl.async = true;
      scriptEl.dataset.widgetId = WIDGET_ID;
      document.body.appendChild(scriptEl);
    });
    
    onBeforeUnmount(() => {
      scriptEl?.remove();
    });
    </script>
    
    <template>
      <RouterView />
    </template>
    

    For Nuxt 3 (Vue's SSR framework), use the useHead composable to inject the script tag into the rendered HTML, and gate it to client-only execution.

    // app.vue or a plugin file
    useHead({
      script: [
        {
          src: "https://widget.feedbask.com/widget.js",
          async: true,
          "data-widget-id": useRuntimeConfig().public.feedbaskId,
          tagPosition: "bodyClose",
        },
      ],
    });
    

    Put the widget ID in nuxt.config.ts under runtimeConfig.public so it is available both at build and runtime. tagPosition: "bodyClose" places the script just before </body>, which is the correct spot for widgets that want to render their own DOM nodes without fighting Vue's mount lifecycle.

    Nuxt also supports a plugin pattern if you want the widget available globally via a composable. That is overkill for most setups. The useHead approach is the one most Nuxt teams ship.

    Webflow

    In Webflow, paste the script into the project's Custom Code settings under the Before </body> tag section.

    Step by step:

    1. Open your Webflow project settings.
    2. Go to the Custom Code tab.
    3. Scroll to the "Footer Code" section (labeled "Before </body> tag").
    4. Paste the script tag:
    <script
      src="https://widget.feedbask.com/widget.js"
      data-widget-id="YOUR_WIDGET_ID"
      async
    ></script>
    
    1. Save and publish the site. The widget does not appear on the Designer preview. It only appears on the published URL.

    If you want the widget on specific pages only, add it under the page's own Custom Code section instead of the site-wide footer. Webflow supports per-page custom code for exactly this kind of selective embedding.

    One Webflow-specific pitfall: the free plan does not support custom code on the published site. You need at least a Basic site plan (the site-plan tier, not the Workspace plan) for custom code to render. If your widget is not appearing, check the site plan first before debugging the script.

    For triggering the widget from a Webflow button, give the button a custom attribute like data-feedback-trigger="true", then add a second script that listens for clicks on that attribute and calls window.Feedbask?.open():

    <script>
      document.addEventListener("click", function (e) {
        if (e.target.closest("[data-feedback-trigger]")) {
          window.Feedbask && window.Feedbask.open();
        }
      });
    </script>
    

    CSP + Security Headers

    If your site sends a Content-Security-Policy, the widget will fail silently until the CSP is updated.

    The two directives that matter for most feedback widgets:

    Directive Why the widget needs it Value to add
    script-src Load widget.js https://widget.feedbask.com
    connect-src Submit feedback via API https://api.feedbask.com

    If your widget supports file uploads (screenshots, attachments), you may also need img-src and media-src entries for the widget's CDN domain. Check the widget provider's docs for the exact list.

    Example CSP header for a SaaS site that uses Feedbask:

    Content-Security-Policy:
      default-src 'self';
      script-src 'self' https://widget.feedbask.com;
      connect-src 'self' https://api.feedbask.com;
      img-src 'self' data: https://widget.feedbask.com;
      style-src 'self' 'unsafe-inline';
    

    Two operational notes. First, when you add the widget to a new environment (staging, production), test the CSP in the browser DevTools under the Security tab. CSP violations log there with enough detail to tell you exactly which directive needs to be extended. Second, if you use a CSP report-only endpoint, pipe those reports somewhere you will read them. Silent CSP failures are the most common reason "the widget works on my machine" but never appears on production.

    For widgets that need to know the current user (for authenticated feedback, NPS tied to accounts), check whether the widget uses postMessage or direct API calls. postMessage does not require CSP changes, but direct calls do. Most modern widgets use direct calls, so plan for the connect-src entry.

    FAQ

    Does the widget work offline? No. Widgets need to reach their API to submit feedback. Most implementations queue submissions locally and retry when connectivity returns, but the initial script load requires a network request.

    Can I self-host the widget script? Some providers allow it. Feedbask offers a self-hosted widget.js on Growth and higher plans for teams that want to serve the asset from their own CDN. The tradeoff: you lose automatic updates, so you have to re-download and redeploy on new versions.

    What is the performance impact? A well-built widget loads async at 25KB to 30KB gzipped and has no measurable effect on Largest Contentful Paint. Widgets over 50KB start to show up in Web Vitals, particularly on mobile.

    How do I test the widget in development? Most widget providers let you use a development widget ID that works on localhost. If yours does not, add localhost to your allowed origins in the widget dashboard. The widget will refuse to load on unknown origins as a security measure.

    Can I style the widget to match my brand? Feedbask supports theme customization (colors, fonts, border radius) via the dashboard or a data-theme attribute on the script tag. Full custom CSS is available on the Growth plan.

    Does the widget work with single-page-app navigation? Yes, if it is initialized once at the app root. Do not inject the script on every route change. Mount it once and let it persist across client-side navigation.


    Ready to add feedback collection to your stack? Grab a widget ID from Feedbask (free plan included) or read more about the website feedback widget and in-app feedback widget options.

    More Posts