Sign in
Feedback docs

Frameworks

The element in plain HTML, React, Next.js, Vue, Svelte and Astro, with types for JSX.

The form is a standard custom element, <onestar-feedback>, defined by one script. Any framework that renders HTML can show it: load the script once, render the element, and tell the framework the tag is a custom element when it asks. This page covers plain HTML, plain JavaScript, React, Next.js, Vue, Svelte and Astro.

Every example uses the form id f_hemsz66yupbd76d2. Use your own, from the form's Install tab, and add your page's origin under Allowed sites first (see the Quickstart).

What every framework needs#

  • The loader, https://embed.onestar.sh/v1/loader.js, loaded once per page. It is safe to include twice: the second copy sees the element is already defined and does nothing.
  • The element, with at least a form attribute. The other attributes are on Modes and triggers and Context and placements.
  • Attributes, not properties. The element reads its attributes when it is added to the page, so set them before it goes in. To change the form or the mode, render a new element (in React, change its key).

The element draws itself in a shadow root, so your CSS does not reach inside and the form's CSS does not leak out. On the server it is plain, empty markup; nothing happens until the loader runs in the browser.

Plain HTML#

index.html
<script src="https://embed.onestar.sh/v1/loader.js" async></script>
<onestar-feedback form="f_hemsz66yupbd76d2"></onestar-feedback>

The script can go in the head or anywhere in the body, before or after the element. With async it never blocks the page. An inline form loads the rest of the widget only when it comes near the screen.

Plain JavaScript#

To add a form from code, create the element, set its attributes, then append it:

feedback.js
function addLoader() {
  if (document.querySelector('script[src="https://embed.onestar.sh/v1/loader.js"]')) return;
  const s = document.createElement("script");
  s.src = "https://embed.onestar.sh/v1/loader.js";
  s.async = true;
  document.head.appendChild(s);
}

addLoader();
const form = document.createElement("onestar-feedback");
form.setAttribute("form", "f_hemsz66yupbd76d2");
form.setAttribute("mode", "popover");
form.setAttribute("placement", "settings");
form.addEventListener("onestar:submit", (e) => {
  if (e.detail.final) console.log("Answered", e.detail.form);
});
document.body.appendChild(form);

Remove the element to take the form away. To open, close or send answers from code, see the JavaScript API.

React#

There is no npm package to install yet. A small component adds the loader once in the browser and renders the element:

Feedback.tsx
"use client";
import { useEffect } from "react";

const LOADER = "https://embed.onestar.sh/v1/loader.js";

export function Feedback() {
  useEffect(() => {
    if (document.querySelector(`script[src="${LOADER}"]`)) return;
    const s = document.createElement("script");
    s.src = LOADER;
    s.async = true;
    document.head.appendChild(s);
  }, []);
  return <onestar-feedback form="f_hemsz66yupbd76d2" mode="inline" />;
}

The "use client" line matters only in Next.js and other frameworks with server components; elsewhere React ignores it. This is the same component the form's Install tab gives you when you pick React.

TypeScript#

JSX does not know the tag until you declare it. Add this once to any .d.ts file in your project:

onestar.d.ts
import type { DetailedHTMLProps, HTMLAttributes } from "react";

declare module "react" {
  namespace JSX {
    interface IntrinsicElements {
      "onestar-feedback": DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement> & {
        form: string;
        mode?: "inline" | "popover" | "slide-in" | "modal" | "page" | "thread";
        api?: string;
        trigger?: string;
        "trigger-for"?: string;
        label?: string;
        placement?: string;
        "user-token"?: string;
      };
      "onestar-inbox": DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement> & {
        form: string;
        "user-token"?: string;
      };
    }
  }
}

The import line keeps the file a module, so the block adds to React's types instead of replacing them. It works with the types of React 18 and 19.

Listening to events#

The element's events have a colon in their names (onestar:submit), which JSX props cannot express. Listen with a ref:

Feedback.tsx
"use client";
import { useEffect, useRef } from "react";

export function Feedback({ onDone }: { onDone: () => void }) {
  const ref = useRef<HTMLElement>(null);
  useEffect(() => {
    const el = ref.current;
    if (!el) return;
    const done = (e: Event) => {
      if ((e as CustomEvent<{ form: string; final: boolean }>).detail.final) onDone();
    };
    el.addEventListener("onestar:submit", done);
    return () => el.removeEventListener("onestar:submit", done);
  }, [onDone]);
  return <onestar-feedback ref={ref} form="f_hemsz66yupbd76d2" mode="inline" placement="checkout" />;
}

Add the loader as in the first example. Events bubble and cross the shadow root, so a listener on window hears every form on the page too. Events and errors lists them all.

Next.js#

In the App Router, use the React component above as it is. It is a client component: the server renders the empty element, and the effect adds the loader once in the browser, so nothing runs on the server and nothing breaks during static rendering. Import it from any page or layout, including server components.

If you would rather load the script once for the whole site, put it in the root layout with next/script and render the element wherever you need it:

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://embed.onestar.sh/v1/loader.js" strategy="lazyOnload" />
      </body>
    </html>
  );
}

The element itself needs no client component unless you listen to its events or call the JavaScript API. Add the TypeScript declaration above in either case.

If your site sends a Content Security Policy, add the two lines from Allowed sites and security.

Vue#

Vue tries to resolve every unknown tag as a Vue component and warns when it cannot. Tell its compiler that tags starting with onestar- are custom elements. With Vite:

vite.config.js
import vue from "@vitejs/plugin-vue";

export default {
  plugins: [
    vue({
      template: {
        compilerOptions: {
          isCustomElement: (tag) => tag.startsWith("onestar-"),
        },
      },
    }),
  ],
};

With Nuxt, the same option goes under vue in the config:

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

Then load the script once (in index.html, or in Nuxt with useHead or the app.head config) and use the element in any template:

index.html
<script src="https://embed.onestar.sh/v1/loader.js" async></script>
Feedback.vue
<template>
  <onestar-feedback form="f_hemsz66yupbd76d2" mode="inline" placement="docs" @onestar:submit="onSubmit"></onestar-feedback>
</template>

<script setup>
function onSubmit(e) {
  if (e.detail.final) console.log("Answered");
}
</script>

Vue passes the attributes through as written and listens to the element's DOM events with @.

Svelte#

Svelte treats any tag with a dash as a custom element, so no setting is needed. In SvelteKit, put the script in src/app.html (or in <svelte:head> on the pages that use it) and render the element:

src/app.html
<script src="https://embed.onestar.sh/v1/loader.js" async></script>
Feedback.svelte
<script>
  let el;
  $effect(() => {
    const done = (e) => e.detail.final && console.log("Answered");
    el.addEventListener("onestar:submit", done);
    return () => el.removeEventListener("onestar:submit", done);
  });
</script>

<onestar-feedback bind:this={el} form="f_hemsz66yupbd76d2" mode="inline"></onestar-feedback>

That example uses Svelte 5 runes; in Svelte 4, add the listener in onMount instead.

TypeScript#

With TypeScript, tell Svelte the tag's attributes by adding to svelte/elements in a .d.ts file your tsconfig.json includes:

src/onestar.d.ts
import type { HTMLAttributes } from "svelte/elements";

declare module "svelte/elements" {
  export interface SvelteHTMLElements {
    "onestar-feedback": HTMLAttributes<HTMLElement> & {
      form: string;
      mode?: "inline" | "popover" | "slide-in" | "modal" | "page" | "thread";
      api?: string;
      trigger?: string;
      "trigger-for"?: string;
      label?: string;
      placement?: string;
      "user-token"?: string;
    };
  }
}

export {};

The empty export keeps the file a module, so it adds to Svelte's types instead of replacing them.

Astro#

Astro renders the element as plain HTML and the loader brings it to life, so it needs no client: directive. Put the script in your layout with is:inline, so Astro leaves the outside URL alone instead of trying to bundle it:

src/layouts/Layout.astro
<html lang="en">
  <head>
    <script is:inline src="https://embed.onestar.sh/v1/loader.js" async></script>
  </head>
  <body>
    <slot />
  </body>
</html>
src/pages/index.astro
---
import Layout from "../layouts/Layout.astro";
---
<Layout>
  <onestar-feedback form="f_hemsz66yupbd76d2" mode="inline" placement="home"></onestar-feedback>
</Layout>

To react to events, add a <script> to the page that listens on the element or on window, as in the plain JavaScript example.

Types for events and the API#

To type the element's events and window.onestar in any TypeScript project, see the declarations on Events and errors and the JavaScript API.