Feedback Widget
The InputBuffer feedback widget is a small script you add to any web page: a docs site, a dashboard, an API reference, or your own app. The widget collects thumbs-up and thumbs-down votes and written comments, and sends them straight to InputBuffer, so you don't need a server of your own. If you scroll to the bottom of this page you'll see an example of the widget.
This page has two parts. The getting started guide gets the widget onto one of your pages. The reference documentation after it lists every option the widget takes.
Getting started guide
By the end of this guide you'll have a widget token, the widget on one of your pages, and a test comment on your Feedback page. Setup takes two steps: create a widget token, then add the widget to a page.
If you use a coding assistant, it can add the widget to your app for you. Here's the prompt:
Before you start
You need...
- An InputBuffer account and organization. An organization is your workspace in InputBuffer. It holds your feedback and your tokens. If you don't have an organization yet, the getting started guide walks through signing up for a free account and creating one.
- A web page you can add a script tag to.
Create a widget token
The widget sends feedback using a token written into your page. Anyone who views the page's source can read the token, so it has to be a token that's safe to publish.
InputBuffer has two kinds of token. A full-access token starts with ib_ and can read and change everything in your organization. A widget token starts with ibw_ and can only submit feedback and votes, so publishing it is safe.
- In InputBuffer, open your organization and go to Settings → API Tokens.
- Click Create token.
- Set Token Type to Widget (safe to embed).
- In Allowed Origins, list the sites the widget will run on, separated by commas, such as
https://docs.yoyodyne.com, https://app.yoyodyne.com. - Click Create token, then copy the token. It's shown only once.
Allowed Origins limits a token to requests sent from web pages on the sites you list. If you leave it blank, the token works from any site. While you test on your own machine, add your local address too, such as http://localhost:3000.
Use a widget token in the widget, never a full-access token. A full-access token in a page's source gives every visitor control of your organization, and InputBuffer rejects a full-access token sent from a browser.
Add the widget to a page
There are two ways to add the widget to a page. Pick the one that fits your page:
- An inline thumbs bar sits in the page flow, wherever you put the element. It uses
bar.js. - A modal opened by your own button opens over the page when someone clicks the button. It uses
modal.js.
Start with the inline bar. It takes the least setup, and the troubleshooting steps under it apply to both.
We don't want anything sensitive sent to us. We send feedback to third-party AI services for classification and search, and we don't yet scrub it first, so keep the widget off pages where people are likely to enter personal information, credentials, or production secrets.
Add an inline thumbs bar
Paste this into your page where you want the bar to appear, and replace YOUR_WIDGET_TOKEN with the widget token you created earlier:
<script src="https://cdn.jsdelivr.net/npm/@inputbuffer/[email protected]/dist/bar.js"></script>
<inputbuffer-feedback
api-key="YOUR_WIDGET_TOKEN"
label="Was this helpful?">
</inputbuffer-feedback>Load the page. A thumbs up/down bar appears where you placed the element. Click a thumb, and a short follow-up form opens. Write a comment and submit it. The comment then appears on the Feedback page of your organization in InputBuffer.
If the form shows an error instead, open your browser console, where the widget logs the reason. A 401 error means the token is missing or invalid. A 403 error means one of two things: the token is a full-access token instead of a widget token, or the page's site isn't listed in the token's Allowed Origins. Check both under Settings → API Tokens.
Open a modal from your own button
The modal uses modal.js instead, and attaches to a button you already have on the page. You don't write any JavaScript.
<button id="feedback-btn">Send feedback</button>
<script
src="https://cdn.jsdelivr.net/npm/@inputbuffer/[email protected]/dist/modal.js"
data-api-key="YOUR_WIDGET_TOKEN"
data-attach-to="#feedback-btn">
</script>Click the button, and the modal opens over the page. Write a comment and submit it. The comment then appears on the Feedback page of your organization in InputBuffer.
Attach feedback to a target
A target records what a piece of feedback is about: a docs page, an API endpoint, or a CLI command. Targets let you see which pages or endpoints draw the most complaints.
The widget you added above sends no target yet. Comments still arrive on your Feedback page, but the bar only records a thumb vote when it has a target to count the vote against.
To give the inline bar a target, add target-type="documentation":
<inputbuffer-feedback
api-key="YOUR_WIDGET_TOKEN"
label="Was this helpful?"
target-type="documentation">
</inputbuffer-feedback>Load the page and click a thumb. The bar fills in the page's URL for you, so the page now appears on the Targets page of your organization in InputBuffer. Open it there to see its votes.
For an API endpoint or a CLI command, the bar needs a few more attributes. Targets on the web component lists them.
A modal opened with data-attach-to can't attach a target. There is no data-target-* attribute, so feedback from that button arrives on your Feedback page without being counted against a page or an endpoint. Attaching a target to the modal takes a few lines of script, with no build step and no npm install. Targets on the modal shows the script.
You now have a widget token and the widget on a page, and you've seen a comment arrive on your Feedback page.
Reference documentation
The rest of this page is reference: the bundles, every attribute and option, targets, errors, and styling.
Bundles
Each way of adding the widget needs one script. Pick the smallest bundle that covers what you used:
| Bundle | Size | What it includes |
|---|---|---|
bar.js | 20 KB | The thumbs up/down bar, with an optional follow-up form |
modal.js | 16 KB | The full-text feedback modal |
widget.js | 35 KB | Both the bar and the modal |
There are two ways to load a bundle: from the CDN, or from npm.
From the CDN
https://cdn.jsdelivr.net/npm/@inputbuffer/[email protected]/dist/bar.js
https://cdn.jsdelivr.net/npm/@inputbuffer/[email protected]/dist/modal.js
https://cdn.jsdelivr.net/npm/@inputbuffer/[email protected]/dist/widget.jsThe version is pinned so a new release can't change your page without you knowing. Bump the number when you want to pick up a newer version.
From npm
npm install @inputbuffer/feedbackThe package has one entry point per bundle: @inputbuffer/feedback/bar, @inputbuffer/feedback/modal, and @inputbuffer/feedback for both.
Web component reference
Use <inputbuffer-feedback> with bar.js or widget.js.
| Attribute | Default | Description |
|---|---|---|
api-key | required | Your widget token |
label | None | The text shown next to the thumbs, such as "Was this helpful?" |
show-label | true | Set to false to hide the text label |
show-thumbs | true | Set to false to hide the thumbs. The whole bar becomes one button that opens the form, with no sentiment and no reaction recorded. Ignores show-label="false" |
placement | inline | inline renders the bar in place; fixed pins it to the bottom of the viewport |
modal-title | None | The heading for the follow-up form |
modal-placeholder | None | The textarea placeholder text in the follow-up form |
show-title-field | false | Show the optional title field in the follow-up form |
submitted-by | None | The id your product has for the person writing the feedback, such as their user id. Sent as submitted_by with each written comment, up to 300 characters, and returned on the feedback item by the API |
user-id | None | The id your product has for the person voting. Sent only with thumb votes, where it limits each person to one vote per target, for all time. Usually the same value as submitted-by. When omitted, InputBuffer falls back to the IP address over a 24 hour window |
target-type | None | Attaches the bar to a docs page, an endpoint, or a CLI command, so thumb votes are counted against it. See Targeting |
theme-primary | None | The primary color, used for buttons and focus rings |
theme-background | None | The bar background color |
theme-text | None | The text color |
theme-selected | None | The background of the selected thumb button |
theme-selected-color | None | The icon color of the selected thumb |
inject-styles | true | Set to false to disable automatic CSS injection |
Script tag auto-init (data-* attributes)
Add data-api-key to any script tag pointing at modal.js or widget.js and the modal initializes automatically:
| Attribute | Description |
|---|---|
data-api-key | Sets up the modal on page load |
data-attach-to | A CSS selector for the element that opens the modal on click |
data-title | The modal heading |
data-placeholder | The textarea placeholder text |
data-show-title-field | Set to true to show the optional title field |
data-show-sentiment | Set to true to show the thumbs up/down selector |
data-submitted-by | The id your product has for the person writing the feedback, such as their user id. Sent as submitted_by with each written comment, up to 300 characters, and returned on the feedback item by the API |
data-color-scheme | light, dark, or auto. Defaults to auto |
data-inject-styles | Set to false to disable automatic CSS injection |
data-theme-primary | The primary color |
data-theme-background | The modal background color |
data-theme-text | The text color |
data-theme-selected | The background of the selected thumb |
data-theme-selected-color | The icon color of the selected thumb |
These are all of the attributes the script tag reads. None of them sets a target, because a modal takes
its target when it opens rather than from its configuration. To attach one, leave data-api-key off the
script tag and create the modal yourself. See Targets on the modal.
Programmatic API
For full control, use the JavaScript API directly.
createBar(config)
createBar() builds a thumbs up/down bar and returns it, so you can place it anywhere on the page. Clicking a thumb records a reaction right away, without waiting for the follow-up form. The selected vote is saved to localStorage for 24 hours, so a returning user still sees their previous choice.
import { createBar } from '@inputbuffer/feedback';
// or: const { createBar } = window.InputBufferIO;
const bar = createBar({
apiKey: 'YOUR_WIDGET_TOKEN',
label: 'Was this helpful?',
placement: 'inline', // or 'fixed'
colorScheme: 'auto', // 'light' | 'dark' | 'auto'
modalTitle: 'Share your feedback',
modalPlaceholder: "What's on your mind?",
target: {
type: 'documentation',
metadata: { page_url: window.location.href },
},
});
document.getElementById('feedback-slot').appendChild(bar.element);createBar config options:
| Option | Default | Description |
|---|---|---|
apiKey | required | Your widget token |
label | None | The text shown next to the thumbs |
showThumbs | true | Set to false to hide the thumbs. The whole bar becomes one button that opens the form with no sentiment, and no reaction is recorded |
placement | 'inline' | 'inline' renders the bar in place; 'fixed' pins it to the bottom of the viewport |
colorScheme | 'auto' | 'light', 'dark', or 'auto' |
target | None | The resource the bar collects votes on. See Targeting |
modalTitle | 'Share your feedback' | The heading for the follow-up form |
modalPlaceholder | "What's on your mind?" | The textarea placeholder text in the follow-up form |
showTitleField | false | Show the optional title field in the follow-up form |
submittedBy | None | The id your product has for the person writing the feedback, such as their user id. Sent as submitted_by with each written comment, up to 300 characters, and returned on the feedback item by the API |
userId | None | The id your product has for the person voting. Sent only with thumb votes, where it limits each person to one vote per target, for all time. Usually the same value as submittedBy. When omitted, InputBuffer falls back to the IP address over a 24 hour window |
injectStyles | true | Set to false to disable automatic CSS injection |
theme | None | A theme object. See Theming |
Bar instance methods:
| Method | Description |
|---|---|
bar.on('vote', ({ sentiment }) => {}) | The user clicked a thumb |
bar.on('open', ({ sentiment }) => {}) | The follow-up form opened |
bar.on('submit', ({ id }) => {}) | The feedback was submitted. id is the InputBuffer feedback id |
bar.on('close', () => {}) | The follow-up form closed |
bar.on('error', (err) => {}) | The submission failed |
bar.open(sentiment?) | Opens the follow-up form from your own UI. Pass 'positive' or 'negative' to select that thumb, or omit it to keep whatever is already selected. This is a display action only: it does not record a reaction or emit vote |
bar.close() | Closes the follow-up form. A no-op if it's already closed |
bar.destroy() | Removes the bar and cleans up its listeners |
Pair showThumbs: false with your own trigger to open the follow-up form without showing the thumbs at all:
const bar = createBar({
apiKey: 'YOUR_WIDGET_TOKEN',
showThumbs: false,
target: {
type: 'documentation',
metadata: { page_url: window.location.href },
},
});
document.getElementById('feedback-slot').appendChild(bar.element);
document.getElementById('my-trigger').addEventListener('click', () => bar.open());createModal(config)
createModal() builds a feedback modal and returns it. The modal stays hidden until you call modal.open().
import { createModal } from '@inputbuffer/feedback';
// or: const { createModal } = window.InputBufferIO;
const modal = createModal({
apiKey: 'YOUR_WIDGET_TOKEN',
title: 'Share your feedback',
placeholder: "What's on your mind?",
showTitleField: false,
showSentiment: false,
colorScheme: 'auto',
});
document.getElementById('my-button').addEventListener('click', () => modal.open());createModal config options:
| Option | Default | Description |
|---|---|---|
apiKey | required | Your widget token |
attachTo | None | A CSS selector for an element that opens the modal on click |
title | 'Share your feedback' | The modal heading |
placeholder | "What's on your mind?" | The textarea placeholder text |
showTitleField | false | Show the optional title field |
showSentiment | false | Show the thumbs up/down sentiment selector. The selection drives the modal's own UI. The feedback API has no sentiment field, so recording a vote needs the bar and a target instead |
submittedBy | None | The id your product has for the person writing the feedback, such as their user id. Sent as submitted_by with each written comment, up to 300 characters, and returned on the feedback item by the API |
colorScheme | 'auto' | 'light', 'dark', or 'auto' |
injectStyles | true | Set to false to disable automatic CSS injection |
theme | None | A theme object. See Theming |
createModal takes no target option, unlike createBar. The modal attaches a target per open call, so
pass one to modal.open(). See Targets on the modal.
Modal instance methods:
| Method | Description |
|---|---|
modal.open(options?) | Opens the modal, optionally with context. See below |
modal.close() | Closes the modal |
modal.destroy() | Removes the modal and cleans up its listeners |
modal.on('submit', ({ id }) => {}) | The feedback was submitted. id is the InputBuffer feedback id |
modal.on('close', () => {}) | The modal closed |
modal.on('error', (err) => {}) | The submission failed |
Use the id from the submit event to match a widget submission to its entry on your Feedback page, or to fetch it from the REST API.
modal.open(options)
Pass options to pre-populate the modal or attach feedback to a specific target:
modal.open({
title: 'Feedback on this page', // Override modal heading
sentiment: 'negative', // Pre-select a thumb ('positive' | 'negative')
submittedBy: 'user_12345', // Override the submittedBy set on createModal
prefill: {
description: 'This section was...', // Pre-fill textarea
},
target: {
type: 'documentation',
metadata: {
page_url: window.location.href,
section_heading: 'Submit your first feedback', // optional
},
},
});Targeting
Targets attach feedback to a specific resource in your product: a documentation page, a REST endpoint, or a CLI command. A target lets you see which pages or endpoints draw the most complaints.
You do not register targets ahead of time. Send the type and its metadata, and InputBuffer either finds the matching target or creates it. Two targets of the same type with the same metadata are treated as the same target, which is how votes and feedback accumulate per resource.
Targets are optional for the modal: without one, feedback still arrives on your Feedback page. They matter more for the bar. A thumb click with no target still opens the follow-up form, and anything the user writes there still reaches your Feedback page, but the vote itself is not recorded anywhere.
When you set a target, metadata is required, and so are the fields marked below as required for that type. A target missing one is rejected with a 422. The web component drops an incomplete target and logs a warning to the console rather than sending it.
Documentation page
modal.open({
target: {
type: 'documentation',
metadata: {
page_url: 'https://docs.yoyodyne.com/getting-started', // required
section_heading: 'Authentication', // optional
doc_version: 'v2', // optional
},
},
});REST API endpoint
modal.open({
target: {
type: 'rest_endpoint',
metadata: {
method: 'POST', // required
path: '/v1/users', // required
host: 'api.yoyodyne.com', // optional
api_version: 'v1', // optional
},
},
});CLI command
modal.open({
target: {
type: 'cli_command',
metadata: {
command: 'deploy', // required
subcommand: 'production', // optional
cli_version: '2.4.0', // optional
args: ['--env', 'prod'], // optional
},
},
});Pass more than one entry in args and each flag becomes its own target, so you can see which flag drew the complaint.
Targets on the modal
The modal takes a target when it opens, not when it is created, so every target reaches it through
modal.open(). The script tag auto-init calls open() with no arguments, which is why data-api-key
and data-attach-to on their own cannot attach one.
A static site can still send targets, with no build step and no npm install. The CDN bundle puts
createModal on window.InputBufferIO, so wiring up your own button takes a few lines:
<button id="feedback-btn">Send feedback</button>
<script src="https://cdn.jsdelivr.net/npm/@inputbuffer/[email protected]/dist/modal.js"></script>
<script>
const modal = window.InputBufferIO.createModal({ apiKey: 'YOUR_WIDGET_TOKEN' });
document.getElementById('feedback-btn').addEventListener('click', () => {
modal.open({
target: { type: 'documentation', metadata: { page_url: window.location.href } },
});
});
</script>Leave data-api-key off the script tag when you do this. With it, the bundle builds a second modal of
its own, and if data-attach-to names the same button, a click opens both.
Reach for attachTo and the data-* attributes when you don't need a target, and for the snippet above
when you do.
Targets on the web component
<inputbuffer-feedback> builds the same target from target-type plus the metadata attributes for that type:
| Attribute | Applies to | Metadata field |
|---|---|---|
target-type | All | documentation, rest_endpoint, or cli_command |
target-page-url | documentation | page_url. Defaults to the current page URL |
target-section-heading | documentation | section_heading |
target-doc-version | documentation | doc_version |
target-method | rest_endpoint | method |
target-path | rest_endpoint | path |
target-host | rest_endpoint | host |
target-api-version | rest_endpoint | api_version |
target-command | cli_command | command |
target-subcommand | cli_command | subcommand |
target-cli-version | cli_command | cli_version |
target-args | cli_command | args, as a comma separated list |
Because target-page-url defaults to the current page, rating the page a reader is on takes one attribute:
<inputbuffer-feedback
api-key="YOUR_WIDGET_TOKEN"
label="Was this helpful?"
target-type="documentation">
</inputbuffer-feedback>An endpoint needs its identifying fields spelled out:
<inputbuffer-feedback
api-key="YOUR_WIDGET_TOKEN"
label="Was this helpful?"
target-type="rest_endpoint"
target-method="POST"
target-path="/v1/users">
</inputbuffer-feedback>Errors
The error event hands you the failure so you can log it or show your own message. API failures arrive as an ApiError with type, title, status, detail, category, and, on a validation failure, the field that was rejected. Network failures and the 10 second request timeout arrive as an ordinary Error instead.
Switch on type rather than status, because several types share a status code. The full list is in the API Errors reference.
The category value tells you who can fix the problem. The widget treats the two differently:
user: the person filling in the form sent something the API rejected, such as text over the length limit. Thedetailvalue is written for them, and the widget shows it in the form as-is.integration: the embed itself is misconfigured, such as a full-access token in the browser or a site that is not listed in the token's Allowed Origins. The person filling in the form sees a generic message, and the widget logs the real reason to the browser console so you can find it.
import { ApiError } from '@inputbuffer/feedback';
bar.on('error', (err) => {
if (err instanceof ApiError && err.category === 'integration') {
console.error('Widget misconfigured:', err.type, err.detail);
}
});ApiError is exported from every entry point: @inputbuffer/feedback, /bar, and /modal.
| Problem type | Status | What went wrong |
|---|---|---|
unauthorized, invalid-token, invalid-token-format | 401 | The token is missing, malformed, or revoked |
widget-token-restricted | 403 | A full-access ib_ token was sent from a browser. Use a widget ibw_ token |
forbidden-origin | 403 | This page's site is not listed in the token's Allowed Origins. Add it under Settings → API Tokens |
usage-limit-reached | 402 | The organization hit its lifetime feedback limit |
missing-required-field, invalid-field-value | 422 | A field is missing or invalid. The field value in the error response names the field that was rejected |
rate-limited | 429 | One IP address sent more than 30 requests in a minute |
internal-error | 500 | Something failed on our end |
Theming
Color scheme
The colorScheme option sets the widget to light or dark:
createModal({ apiKey: '...', colorScheme: 'dark' });
// or 'light' | 'auto' (default: 'auto' follows system preference)Theme object
Override individual colors via the theme config option:
createModal({
apiKey: '...',
theme: {
primary: '#6366f1', // Button and focus ring color
background: '#1e1e2e', // Modal background
surface: '#2a2a3e', // Modal header background
text: '#cdd6f4', // Body text
selected: '#6366f1', // Selected thumb background
selectedColor: '#ffffff', // Selected thumb icon color
},
});The same theme option is available on createBar(). For the web component, use theme-* attributes:
<inputbuffer-feedback
api-key="..."
theme-primary="#6366f1"
theme-background="#1e1e2e"
theme-text="#cdd6f4">
</inputbuffer-feedback>CSS custom properties
When injectStyles is true (default), the widget injects its own stylesheet and exposes CSS custom properties you can override:
/* Modal: set on #ib-modal */
#ib-modal {
--ib-primary: #6366f1;
--ib-primary-hover: #4338ca;
--ib-background: #1e1e2e;
--ib-surface: #2a2a3e;
--ib-text: #cdd6f4;
--ib-muted: #9399b2;
--ib-border: #45475a;
--ib-selected: #6366f1;
--ib-selected-color: #ffffff;
--ib-radius: 8px;
--ib-radius-input: 4px;
}
/* Bar: set on .ib-bar-wrapper */
.ib-bar-wrapper {
--ib-primary: #6366f1;
}CSS customization
When injectStyles: false, you can style everything from scratch using stable selectors.
Modal selectors
| Selector | Element |
|---|---|
#ib-overlay | The full-screen backdrop |
#ib-modal | The modal container, and the scope for the CSS variables |
#ib-modal-header | The header bar |
#ib-modal-body | The body content area |
#ib-title | The modal heading |
#ib-textarea | The feedback textarea |
#ib-title-input | The optional title input |
#ib-submit | The submit button |
#ib-close | The close button |
#ib-success | The success message |
#ib-error | The error message |
Bar selectors
| Selector | Element |
|---|---|
.ib-bar-wrapper | The outer wrapper, and the scope for the CSS variables |
.ib-bar | The visible bar strip |
.ib-bar-label | The label text |
.ib-bar-btn | The thumb buttons |
.ib-bar-popover | The follow-up popover |
.ib-bar-header | The popover header |
.ib-bar-body | The popover body |
.ib-bar-title | The popover heading |
.ib-bar-textarea | The feedback textarea |
.ib-bar-title-input | The optional title input |
.ib-bar-submit | The submit button |
.ib-bar-success | The success message |
.ib-bar-error | The error message |
Next steps
Feedback from the widget now arrives on your Feedback page. Next, set up buffers, so InputBuffer sorts that feedback into topics for you.
- Set up buffers: define the categories InputBuffer sorts feedback into, and write the rules that route it
- Feedback from AI Agents: have AI coding agents report problems with your docs and API, using the same kind of token
- API getting started: send feedback from your own backend, a support tool, or a script