Documentation

Add a feedback widget to your site with two lines of code. Works with any framework or plain HTML.

HTML

The fastest way to get started. Paste this anywhere in your HTML, right before the closing </body> tag.

<script src="https://cdn.palmframe.com/embed.js"></script>
<palmframe-widget project="your-project-id" />

The script registers a custom element using Shadow DOM. No build step, no dependencies, no conflicts with your styles.

Add mode="test" to try the widget without affecting your live data. Feedback will only appear in test mode in your dashboard. Remove the attribute when you’re ready to go live.

React / Next.js

Load the script once, then use the custom element in any component. Next.js, Vite, CRA — all work the same.

import Script from "next/script"

export default function Layout({ children }) {
  return (
    <>
      {children}
      <Script src="https://cdn.palmframe.com/embed.js" strategy="lazyOnload" />
      <palmframe-widget project="your-project-id" />
    </>
  )
}

For plain React (Vite / CRA), use a useEffect to load the script, or add the two HTML lines to your index.html.

Add mode="test" to try the widget without affecting your live data. Feedback will only appear in test mode in your dashboard. Remove the attribute when you’re ready to go live.

TypeScript

TypeScript will show Property 'palmframe-widget' does not exist on type 'JSX.IntrinsicElements'. Fix it by adding a type declaration file anywhere in your project:

// palmframe.d.ts
import "react"

declare module "react" {
  namespace JSX {
    interface IntrinsicElements {
      "palmframe-widget": React.DetailedHTMLProps<
        React.HTMLAttributes<HTMLElement> & {
          project?: string
          "api-url"?: string
          position?: "bottom-right" | "bottom-left" | "top-right" | "top-left"
          theme?: "light" | "dark" | "auto"
        },
        HTMLElement
      >
    }
  }
}

Vue.js

Add the script to your index.html or load it in your root component. Tell Vue to ignore the custom element.

// vite.config.js
export default defineConfig({
  plugins: [
    vue({
      template: {
        compilerOptions: {
          isCustomElement: (tag) => tag === "palmframe-widget"
        }
      }
    })
  ]
})

Then in your App.vue:

<template>
  <router-view />
  <palmframe-widget project="your-project-id" />
</template>

Add mode="test" to try the widget without affecting your live data. Feedback will only appear in test mode in your dashboard. Remove the attribute when you’re ready to go live.

TypeScript

Add a type declaration so Volar recognizes the custom element:

// palmframe.d.ts
declare module "vue" {
  interface GlobalComponents {
    "palmframe-widget": {
      project?: string
      "api-url"?: string
      position?: "bottom-right" | "bottom-left" | "top-right" | "top-left"
      theme?: "light" | "dark" | "auto"
    }
  }
}

Svelte

Add the script tag to your app.html and use the custom element in your layout.

<!-- src/app.html -->
<script src="https://cdn.palmframe.com/embed.js"></script>
<!-- src/routes/+layout.svelte -->
<slot />
<palmframe-widget project="your-project-id" />

Add mode="test" to try the widget without affecting your live data. Feedback will only appear in test mode in your dashboard. Remove the attribute when you’re ready to go live.

TypeScript

Svelte may show a warning about an unknown HTML element. Silence it by adding <!-- svelte-ignore a11y_unknown_element --> above the element, or add type declarations:

// palmframe.d.ts
declare namespace svelteHTML {
  interface IntrinsicElements {
    "palmframe-widget": {
      project?: string
      "api-url"?: string
      position?: "bottom-right" | "bottom-left" | "top-right" | "top-left"
      theme?: "light" | "dark" | "auto"
    }
  }
}

Angular

Add CUSTOM_ELEMENTS_SCHEMA to your module or standalone component, then drop the widget into any template.

// app.component.ts
import { Component, CUSTOM_ELEMENTS_SCHEMA } from "@angular/core"

@Component({
  selector: "app-root",
  standalone: true,
  schemas: [CUSTOM_ELEMENTS_SCHEMA],
  template: `
    <router-outlet />
    <palmframe-widget project="your-project-id" />
  `
})
export class AppComponent {}

Add mode="test" to try the widget without affecting your live data. Feedback will only appear in test mode in your dashboard. Remove the attribute when you’re ready to go live.

Add the script tag to your index.html as shown in the HTML section above.

TypeScript

The CUSTOM_ELEMENTS_SCHEMA is all you need. It tells the compiler to allow any unknown element, so no additional type declarations are required.

SolidJS

Solid supports custom elements natively. Add the script to your index.html and use the element directly.

// src/App.tsx
export default function App() {
  return (
    <>
      <Routes />
      <palmframe-widget project="your-project-id" />
    </>
  )
}

Add mode="test" to try the widget without affecting your live data. Feedback will only appear in test mode in your dashboard. Remove the attribute when you’re ready to go live.

TypeScript

Solid uses a separate JSX namespace. Add this declaration:

// palmframe.d.ts
import "solid-js"

declare module "solid-js" {
  namespace JSX {
    interface IntrinsicElements {
      "palmframe-widget": {
        project?: string
        "api-url"?: string
        position?: "bottom-right" | "bottom-left" | "top-right" | "top-left"
      }
    }
  }
}

User identification

Optionally attach user identity to feedback so you know who sent it. Pass an object with any combination of id, email, name. Identity is sent alongside the feedback payload automatically.

Pre-load config

Set user identity before the widget loads. The widget reads this on initialization.

window.palmframe = {
  user: {
    id: "user_123",
    email: "jane@example.com",
    name: "Jane Doe"
  }
}

Dynamic (after load)

Call identify() at any time — for example after the user logs in.

document
  .querySelector("palmframe-widget")
  .identify({
    id: "user_123",
    email: "jane@example.com",
    name: "Jane Doe"
  })

All fields are optional. Feedback submitted without identification still works as before.

HMAC-verified identity is coming soon. It will let you cryptographically verify user identity server-side, preventing spoofing. Plain-text identification works great for trusted environments today.

Events

The widget fires a custom event when feedback is submitted successfully. Listen for it to trigger your own logic — confetti, analytics, toast notifications, you name it.

const widget = document.querySelector("palmframe-widget")

widget.addEventListener("palmframe:submit", (event) => {
  console.log(event.detail.message)   // "Great product!"
  console.log(event.detail.sentiment) // "love"
})

The event includes message and sentiment in event.detail.

Custom trigger

Want to use your own button instead of the built-in floating bubble? Use the trigger attribute with a CSS selector. Add no-button to hide the default button.

Try it — this button opens the widget on this page:

<button id="my-feedback-btn">Give Feedback</button>

<palmframe-widget
  project="your-project-id"
  trigger="#my-feedback-btn"
  no-button
/>

Programmatic control

You can also open and close the widget from JavaScript using the open() and close()methods.

const widget = document.querySelector("palmframe-widget")

widget.open()  // Open the feedback drawer
widget.close() // Close the feedback drawer

Alternatively, dispatch a palmframe:open event on the widget element to open it.

Dark mode

Add the theme attribute to switch between light, dark, or system-preference themes.

<!-- Light theme (default) -->
<palmframe-widget project="your-project-id" />

<!-- Always dark -->
<palmframe-widget project="your-project-id" theme="dark" />

<!-- Follows the user’s OS preference -->
<palmframe-widget project="your-project-id" theme="auto" />

Customization

Override any CSS custom property on the widget element to match your brand. All styling is driven by these variables:

--pf-background          // Widget background
--pf-foreground          // Text color
--pf-primary             // Send button, tooltips
--pf-primary-foreground  // Text on primary
--pf-primary-hover       // Send button hover
--pf-muted               // Hover backgrounds
--pf-muted-foreground    // Placeholder text
--pf-accent              // Selected sentiment
--pf-border              // Borders
--pf-input               // Input border
--pf-ring                // Focus ring
--pf-radius              // Border radius
--pf-link                // Link color
--pf-link-hover          // Link hover color

For example, to use a blue primary color:

<style>
  palmframe-widget {
    --pf-primary: oklch(0.6 0.2 260);
    --pf-primary-hover: oklch(0.5 0.2 260);
    --pf-primary-foreground: white;
  }
</style>

How it works

The embed script registers a <palmframe-widget> custom element that renders inside a Shadow DOM. This means it won’t conflict with your styles or JavaScript. The widget shows a small floating button. When a user clicks it, they can pick a sentiment, leave a message, and submit. The feedback is sent to Palmframe along with the current page URL.

REST API

Access your feedback data programmatically. The API uses Bearer token authentication and returns JSON responses.

Authentication

Create an API key from your dashboard. Include it in the Authorization header of every request.

curl https://www.palmframe.com/api/v1/projects \
  -H "Authorization: Bearer YOUR_API_KEY"

Endpoints

MethodEndpointDescription
GET/api/v1/projectsList your projects
GET/api/v1/projects/:idGet a single project
GET/api/v1/projects/:id/feedbacksList feedbacks for a project
GET/api/v1/projects/:id/feedbacks/:idGet a single feedback
DELETE/api/v1/projects/:id/feedbacks/:idDelete a feedback

Query parameters

The feedbacks endpoint supports pagination, filtering, and search:

ParameterDescription
pagePage number (default: 1)
per_pageItems per page (default: 20, max: 100)
sentimentFilter by sentiment (love, neutral, dislike, hate)
searchSearch in message, name, and email

Example

curl "https://www.palmframe.com/api/v1/projects/your-project-id/feedbacks?sentiment=love&per_page=10" \
  -H "Authorization: Bearer YOUR_API_KEY"

Response format

All list endpoints return paginated results with a meta object:

{
  "data": [
    {
      "id": "abc-123",
      "projectId": "your-project-id",
      "message": "Great product!",
      "sentiment": "love",
      "pageUrl": "https://example.com/pricing",
      "userEmail": "jane@example.com",
      "userName": "Jane Doe",
      "createdAt": "2026-03-09T12:00:00.000+00:00"
    }
  ],
  "meta": {
    "total": 42,
    "per_page": 20,
    "current_page": 1,
    "last_page": 3
  }
}

The API only returns live feedback. Test mode feedback is not accessible via the API.