Feedback Widget
The @inputbuffer/feedback package is a lightweight widget you can drop into any web page: documentation sites, dashboards, API references, or your own app. It sends feedback directly to InputBuffer without any server-side code.
It collects thumbs-up/down votes and optional written comments, and each submission appears in your InputBuffer inbox.
Before you start
You need a widget-scoped API token. Widget tokens are safe to embed in client-side code: they can create feedback and submit reactions, and nothing else. They cannot read or modify your organization's existing data.
To create one:
- Go to Settings → API Tokens in your organization
- Click Create token and select the Widget scope
- Copy the token, because it's shown only once
Do not use a full-access API token in the widget. Full-access tokens are rejected by the widget API when sent from a browser. Use a widget-scoped token only.
Feedback submitted through the widget is sent to third-party AI services for classification and search. InputBuffer does not yet scrub content before processing, so avoid placing the widget where users are likely to submit personal information, credentials, or production secrets.
Quick start
There are three ways to embed the widget, and all three are below. Pick the one that fits your page:
- Inline thumbs bar: sits in the page flow, wherever you put the element. Uses
bar.js. - Floating thumbs bar: pins to the bottom of the viewport. Uses
bar.js. - Modal opened by your own button: opens over the page when a button is clicked. Uses
modal.js.
Start with the inline bar. It is the quickest to get working, and the troubleshooting steps under it apply to all three.
1. Inline thumbs bar
Paste this into your page where you want the bar to appear:
<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>What you should see: A thumbs up/down bar appears inline where you placed the element. After a user clicks a thumb, a short follow-up form appears so they can add context. Once they submit, the feedback appears in your InputBuffer inbox under Feedback.
If the bar does not appear, check your browser console for errors. A 401 error means your token is
missing or invalid. A 403 error means one of two things: a full-access token was used instead of a
widget-scoped one, or the page's origin isn't on the token's allowlist. Check Settings → API
Tokens for both the token's scope and its allowed origins.
2. Floating thumbs bar
Same script and same element as the inline bar, with placement="fixed" added. Useful for
documentation pages, where the bar should stay reachable as the reader scrolls.
<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?"
placement="fixed">
</inputbuffer-feedback>3. Modal opened by your own button
This one uses modal.js instead, and attaches to a button you already have on the page. There is no
JavaScript to write.
<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>The modal opens when the button is clicked. The feedback appears in your InputBuffer inbox once submitted.
This form cannot attach a target. There is no data-target-* attribute, and data-attach-to opens the
modal with no target set, so feedback from this button reaches your general inbox without counting
against a page or an endpoint. Attaching a target takes a few lines of script, with no build step and no
npm install. See Targets on the modal.
Bundles
Each embed style above needs one script. Pick the smallest bundle that covers what you used:
| Bundle | Size | What it includes |
|---|---|---|
bar.js | 18 KB | The thumbs up/down bar, with an optional follow-up form |
modal.js | 16 KB | The full-text feedback modal |
widget.js | 32 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-scoped token |
api-url | https://inputbuffer.io | The API origin to send to. The widget appends its own paths, so set this to an origin such as http://localhost:8080 |
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 | An identifier for whoever is submitting, such as your own user id. Sent as submitted_by and shown on the feedback item in your inbox |
user-id | None | A stable user identifier used to deduplicate thumb votes. When set, one vote per user is counted per target, for all time. 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-api-url | The API origin to send to. The widget appends its own paths, so set this to an origin such as http://localhost:8080 |
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 | An identifier for whoever is submitting. Sent as submitted_by |
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-scoped token |
apiUrl | 'https://inputbuffer.io' | The API origin to send to. The widget appends its own paths, so set this to an origin such as http://localhost:8080 |
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 | An identifier for whoever is submitting, such as your own user id. Sent as submitted_by, up to 300 characters |
userId | None | A stable user identifier used to deduplicate thumb votes. When set, one vote per user is counted per target, for all time. 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() | Remove the bar and clean 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)
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-scoped token |
apiUrl | 'https://inputbuffer.io' | The API origin to send to. The widget appends its own paths, so set this to an origin such as http://localhost:8080 |
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 | An identifier for whoever is submitting, such as your own user id. Sent as submitted_by, up to 300 characters |
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?) | Open the modal, optionally with context. See below |
modal.close() | Close the modal programmatically |
modal.destroy() | Clean up and remove all 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 in your inbox, 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 goes into your general inbox. 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 inbox, 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.example.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.example.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 an origin that is not allowlisted. 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-scoped ibw_ token |
forbidden-origin | 403 | This page's origin is not on the token's allowlist. 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 | Too many requests from this token or IP |
internal-error | 500 | Something failed on our end |
Theming
Color scheme
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
- API getting started: submit feedback server-side, query feedback, or integrate with your backend
- API Errors: every error the API can return, with what causes it and how to recover