# 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](https://onestar.sh/docs/feedback/quickstart.md)).

## 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](https://onestar.sh/docs/feedback/modes-and-triggers.md) and [Context and placements](https://onestar.sh/docs/feedback/context-and-placements.md).
- 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

```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:

```js 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](https://onestar.sh/docs/feedback/javascript-api.md).

## React

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

```tsx 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:

```ts 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:

```tsx 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](https://onestar.sh/docs/feedback/events.md) 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:

```tsx 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](https://onestar.sh/docs/feedback/security.md).

## 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:

```js 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:

```ts 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:

```html index.html
<script src="https://embed.onestar.sh/v1/loader.js" async></script>
```

```html 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:

```html src/app.html
<script src="https://embed.onestar.sh/v1/loader.js" async></script>
```

```html 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:

```ts 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:

```html 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>
```

```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](https://onestar.sh/docs/feedback/events.md) and the [JavaScript API](https://onestar.sh/docs/feedback/javascript-api.md).
