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 drawerAlternatively, 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 colorFor 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
| Method | Endpoint | Description |
|---|---|---|
GET | /api/v1/projects | List your projects |
GET | /api/v1/projects/:id | Get a single project |
GET | /api/v1/projects/:id/feedbacks | List feedbacks for a project |
GET | /api/v1/projects/:id/feedbacks/:id | Get a single feedback |
DELETE | /api/v1/projects/:id/feedbacks/:id | Delete a feedback |
Query parameters
The feedbacks endpoint supports pagination, filtering, and search:
| Parameter | Description |
|---|---|
| page | Page number (default: 1) |
| per_page | Items per page (default: 20, max: 100) |
| sentiment | Filter by sentiment (love, neutral, dislike, hate) |
| search | Search 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.