A lightweight, standalone JavaScript library for adding user feedback collection to any AI output. The widget integrates seamlessly with a single API endpoint which you can implement yourself or set up a quick, free account on Coolhand to capture & analyze in realtime.
- 🎯 Ultra Simple: Add
coolhand-feedbackattribute to any element - no custom JavaScript required! - 🛡️ Isolated Styling: Uses Shadow DOM (when available) to prevent CSS conflicts
- 🎨 Clean UI: Minimal, non-intrusive design with smooth animations
- 📦 Zero Dependencies: Pure JavaScript, no external libraries required
- 📱 Customizable: Easy to customize with your own styling or icons.
- ⚡ Lightweight: ~16KB minified
- 📘 TypeScript Support: Full type definitions included
- ♿ Accessible: WCAG 2.1 AA compliant with full keyboard navigation and screen reader support
- 🔄 Smart Updates: Automatically tracks and updates feedback when users change their response
- ✏️ Revised Output Tracking: Automatically captures edits to textarea/input content
- 💬 Explanation Prompts: Optionally ask users to explain their feedback with configurable sampling
- 🍪 User Fingerprinting (Experimental): Automatic cookie-based session tracking for cross-session feedback correlation
- 📍 Configurable Placement: Position widgets at any corner (top-right, top-left, bottom-right, bottom-left)
The widget is designed with accessibility in mind:
- Keyboard Navigation: Full keyboard support including Tab, Enter/Space, Escape, and Arrow keys
- Screen Reader Support: ARIA labels, roles, and live regions for real-time announcements
- Focus Management: Visible focus indicators on all interactive elements
- Reduced Motion: Respects
prefers-reduced-motionuser preference - Semantic HTML: Proper button elements with
role="radiogroup"androle="radio"for feedback options
- Node.js: coolhand-node package - Coolhand monitoring for Node.js applications
- Ruby: coolhand gem - Coolhand monitoring for Ruby applications
- Python: coolhand package - Coolhand monitoring for Python applications
Include via jsDelivr - these automatically mirror the npm package:
<!-- jsDelivr (recommended) -->
<script src="https://cdn.jsdelivr.net/npm/coolhand@0.2.0/dist/coolhand.min.js"></script>
<!-- Latest version (auto-updates) -->
<script src="https://cdn.jsdelivr.net/npm/coolhand/dist/coolhand.min.js"></script>Or via GitHub releases:
<script src="https://github.com/Coolhand-Labs/coolhand-js/releases/download/v0.2.0/coolhand.min.js"></script>npm install coolhand# Clone the repository
git clone https://github.com/Coolhand-Labs/coolhand-js.git
cd coolhand-js
# Install dependencies
npm install
# Build the library
npm run build
# Output will be in dist/coolhand.min.js<!DOCTYPE html>
<html>
<head>
<script src="coolhand.min.js"></script>
</head>
<body>
<!-- Just add coolhand-feedback attribute -->
<div coolhand-feedback>
This content will automatically get a feedback widget!
The feedback icon will appear in the upper-right corner.
</div>
<script>
// One line initialization - widgets auto-attach!
CoolhandJS.init('your-api-key-here');
</script>
</body>
</html>Initialize the library with your Coolhand API key. Automatically attaches to all elements with coolhand-feedback attribute.
Parameters:
apiKey(string, required): Your Coolhand API keyoptions(object, optional): Configuration optionsautoAttach(boolean): Enable auto-attachment (default: true)clientUniqueId(string): Optional client identifier sent with all feedback (e.g., user ID, session ID)widgetStyle(string): Default widget style for all widgets -"overlay"(default),"pixel", or"hidden"explanationSample(number): Probability (0-1) of showing explanation prompt after feedback.0= never ask,1= always ask (default),0.2= ask 20% of the timeenableFingerprint(boolean): (Experimental) Enable automatic cookie-based user fingerprinting (default: true). Set tofalseto disable.autoHighlight(boolean): (Experimental) Enable automatic highlight on first visit (default: true). When enabled, all feedback widgets show a pulsating highlight until the user interacts with any widget. State is persisted in a cookie.placementVertical(string): Widget vertical position -"top"(default) or"bottom"placementHorizontal(string): Widget horizontal position -"right"(default) or"left"
Returns:
boolean: True if initialization succeeded, false otherwise
Example:
// Auto-attach enabled (default)
CoolhandJS.init('ch_api_abc123...');
// With client tracking
CoolhandJS.init('ch_api_abc123...', { clientUniqueId: 'user-123' });
// Use minimal pixel style for all widgets
CoolhandJS.init('ch_api_abc123...', { widgetStyle: 'pixel' });
// Ask for explanation only 30% of the time
CoolhandJS.init('ch_api_abc123...', { explanationSample: 0.3 });
// Never ask for explanation (just collect ratings)
CoolhandJS.init('ch_api_abc123...', { explanationSample: 0 });
// Disable auto-attachment
CoolhandJS.init('ch_api_abc123...', { autoAttach: false });
// Disable fingerprint cookie (experimental feature)
CoolhandJS.init('ch_api_abc123...', { enableFingerprint: false });
// Disable auto-highlight on first visit (experimental feature)
CoolhandJS.init('ch_api_abc123...', { autoHighlight: false });
// Position all widgets at bottom-left
CoolhandJS.init('ch_api_abc123...', { placementVertical: 'bottom', placementHorizontal: 'left' });Manually attach a feedback widget to an HTML element. Usually not needed since auto-attachment handles this.
Parameters:
element(HTMLElement, required): The DOM element to attach the widget tooptions(object, optional): Configuration options
Options:
clientUniqueId(string): Optional client identifier (overrides global setting from init)workloadId(string): Optional workload hash ID to associate feedback with a specific workload. Improves fuzzy matching accuracy.widgetStyle(string): Widget display style (overrides global setting) -"overlay","pixel", or"hidden"explanationSample(number): Probability (0-1) of showing explanation prompt (overrides global setting). Default:1(always ask)onSuccess(function): Callback when feedback is successfully submittedonError(function): Callback when an error occursonRevisedOutput(function): Callback when revised output is sent (for textarea/input elements)placementVertical(string): Widget vertical position -"top"(default) or"bottom"placementHorizontal(string): Widget horizontal position -"right"(default) or"left"
Returns:
FeedbackWidget: The widget instance, or null if attachment failed
Example:
// Manual attachment (usually not needed)
const widget = CoolhandJS.attach(document.getElementById('content'), {
workloadId: 'abc123def456',
onSuccess: (feedback, response) => {
console.log('Feedback submitted:', feedback); // true, false, or null
},
onError: (error) => {
console.error('Error submitting feedback:', error);
}
});Remove a feedback widget from an element.
Parameters:
element(HTMLElement): The element with an attached widget
Example:
CoolhandJS.detach(document.getElementById('content'));CoolhandJS makes it incredibly easy to capture human feedback on AI outputs. Just add the coolhand-feedback attribute on HTML div containing the feedback:
<!-- Simple feedback widget (overlay style - default) -->
<div coolhand-feedback>
Your content here
</div>
<!-- Pixel style - minimal 8px dot that expands on hover -->
<div coolhand-feedback data-coolhand-widget-style="pixel">
AI response with minimal feedback indicator
</div>
<!-- Hidden style - no UI, but still tracks input changes -->
<textarea coolhand-feedback data-coolhand-widget-style="hidden">
Content that tracks edits without showing the feedback widget
</textarea>
<!-- With workload association -->
<div coolhand-feedback data-coolhand-workload-id="abc123def456">
AI response associated with a specific workload
</div>
<!-- Widget positioned at bottom-left -->
<div coolhand-feedback
data-coolhand-placement-vertical="bottom"
data-coolhand-placement-horizontal="left">
AI response with widget in bottom-left corner
</div>When attached to a <textarea> or <input> element, the widget automatically:
- Captures the initial value as
original_output - Monitors for changes after feedback is submitted
- Sends
revised_outputvia PATCH when the user edits the content (debounced 1 second)
<!-- Editable AI response with revision tracking -->
<textarea coolhand-feedback data-coolhand-workload-id="abc123">
The AI generated this response which the user can edit.
</textarea>coolhand-feedback: Enables automatic widget attachmentdata-coolhand-widget-style: Widget display style -"overlay"(default),"pixel"(minimal 8px dot that expands on hover), or"hidden"(no UI, still tracks input changes)data-coolhand-workload-id: Optional workload hash ID to associate feedback with a specific workload. When provided, improves fuzzy matching accuracy for connecting feedback to the original LLM request.data-coolhand-explanation-sample-rate: Override explanation sample rate for this element (float"0"to"1")."1"= always show,"0"= never show,"0.5"= 50% chance. Takes priority over the globalexplanationSamplesetting.data-coolhand-feedback-id: Set automatically after successful feedback submission. Contains the feedback ID returned from the API. When present, subsequent feedback changes automatically update the existing feedback instead of creating duplicates.data-coolhand-explanation: Set automatically after user submits an explanation. Contains the explanation text for reference.data-coolhand-highlight: Enables a pulsating gradient border around the widget trigger to draw user attention. Great for onboarding or encouraging feedback.data-coolhand-placement-vertical: Widget vertical position -"top"(default) or"bottom". Overrides globalplacementVerticalsetting.data-coolhand-placement-horizontal: Widget horizontal position -"right"(default) or"left". Overrides globalplacementHorizontalsetting.
The widget sends three types of feedback to the API endpoint:
- 👍 Thumbs Up:
like: true - 😐 Neutral:
like: null - 👎 Thumbs Down:
like: false
After a user selects a feedback rating, the widget can prompt them to provide additional context about their feedback. This helps you understand why users rated content the way they did.
- User clicks the feedback trigger and selects a rating (thumbs up/neutral/thumbs down)
- The rating is immediately submitted to the API
- The widget transforms into a text input asking "How could this result be better?"
- User can optionally type an explanation
- The explanation is auto-saved (debounced) or when the user clicks Submit/closes the widget
- If the user returns to the widget later, they see a summary view showing their rating and explanation, which they can edit
You can control how often the explanation prompt appears using the explanationSample option:
// Always ask for explanation (default)
CoolhandJS.init('your-api-key', { explanationSample: 1 });
// Never ask for explanation (ratings only)
CoolhandJS.init('your-api-key', { explanationSample: 0 });
// Ask 25% of the time (random sampling)
CoolhandJS.init('your-api-key', { explanationSample: 0.25 });You can override the global setting for specific elements using the data-coolhand-explanation-sample-rate attribute (float 0-1):
<!-- Always ask for explanation on this element, regardless of global setting -->
<div coolhand-feedback data-coolhand-explanation-sample-rate="1">
Important AI response where we always want detailed feedback
</div>
<!-- Never ask for explanation on this element -->
<div coolhand-feedback data-coolhand-explanation-sample-rate="0">
Simple response where a quick rating is sufficient
</div>
<!-- Ask 50% of the time on this element -->
<div coolhand-feedback data-coolhand-explanation-sample-rate="0.5">
Response with custom sampling rate
</div>The attribute takes priority over the global explanationSample setting, allowing fine-grained control over which outputs get detailed feedback.
CoolhandJS can automatically generate and persist a unique user fingerprint ID via a first-party cookie. This enables cross-session feedback correlation without requiring you to implement user tracking yourself.
- On
init(), a UUID v4 fingerprint is generated (or retrieved if already exists) - The fingerprint is stored in a secure, first-party cookie (
coolhand_fingerprint) - The
coolhand_fingerprint_idis automatically included in all API requests - The cookie is refreshed on each visit to extend its lifetime
The fingerprint cookie uses these security settings:
SameSite=None- Supports third-party iframe embeddingSecure- Requires HTTPS (fingerprinting is disabled on HTTP sites)Path=/- Available site-wideMax-Age=365 days- Persists for one year (refreshed on each visit)
Both identifiers serve different purposes and are sent together:
clientUniqueId: Developer-provided identifier (e.g., your user ID or session ID)coolhand_fingerprint_id: Automatic browser-level identifier
This allows you to correlate feedback both with your own user system and across anonymous sessions.
Fingerprinting is enabled by default. To disable it:
CoolhandJS.init('your-api-key', { enableFingerprint: false });- Requires HTTPS (fingerprinting silently disabled on HTTP)
- Works in Chrome, Firefox, Safari, Edge (modern versions)
- Gracefully degrades if cookies are blocked by the browser or extensions
- Safari ITP: Cookie is refreshed on each visit to work around the 7-day limit for client-set cookies
The fingerprint is a randomly generated UUID with no personal information. It cannot be used to identify individuals, only to correlate feedback from the same browser. Consider disclosing this cookie in your privacy policy if required by your jurisdiction.
The element must contain text content or a value (for input/textarea). The widget will not attach to elements without readable text and will log an error to the console. For <textarea> and <input> elements, the widget uses the value property instead of textContent.
A valid Coolhand API key is required. Get one from your Coolhand Dashboard.
Elements with the coolhand-feedback attribute will automatically get feedback widgets when CoolhandJS.init() is called. The library uses a MutationObserver to detect dynamically added elements.
- Chrome 60+
- Firefox 63+
- Safari 10.1+
- Edge 79+
- Mobile browsers (iOS Safari 10.3+, Chrome Mobile)
The Coolhand API supports CORS for browser-based requests. If you encounter CORS issues:
- Ensure your domain is whitelisted in your Coolhand dashboard
- Check that you're using HTTPS in production
- Verify your API key has the correct permissions
The widget uses CSS custom properties (variables) for easy customization while maintaining style isolation.
Override these variables to match your design:
/* Apply to elements with the widget */
[coolhand-feedback] {
--coolhand-bg: #ffffff; /* Background color */
--coolhand-bg-hover: #f8f9fa; /* Hover state background */
--coolhand-border: #e5e7eb; /* Border color */
--coolhand-border-radius: 6px; /* Corner radius */
--coolhand-text: #374151; /* Primary text/icon color */
--coolhand-text-muted: #6b7280; /* Secondary text/icon color */
--coolhand-shadow: 0 4px 12px rgba(0, 0, 0, 0.08); /* Default shadow */
--coolhand-shadow-hover: 0 6px 16px rgba(0, 0, 0, 0.12); /* Hover shadow */
--coolhand-accent: #2563eb; /* Accent color */
--coolhand-success: #10b981; /* Success/positive color */
--coolhand-icon-size: 18px; /* Icon dimensions */
--coolhand-font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
--coolhand-font-size: 12px; /* Prompt text size */
}/* Dark mode customization */
[coolhand-feedback] {
--coolhand-bg: #1f2937;
--coolhand-bg-hover: #374151;
--coolhand-border: #4b5563;
--coolhand-text: #f9fafb;
--coolhand-text-muted: #9ca3af;
--coolhand-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
--coolhand-shadow-hover: 0 6px 16px rgba(0, 0, 0, 0.4);
}/* Match your brand colors */
[coolhand-feedback] {
--coolhand-accent: #7c3aed; /* Purple accent */
--coolhand-success: #22c55e; /* Green for positive */
--coolhand-border-radius: 12px; /* More rounded corners */
}The widget is designed to avoid conflicts with your existing styles:
- Shadow DOM: When supported, styles are completely isolated
- Scoped Classes: All classes use
coolhand-prefix - High Specificity: Z-index of 99999 ensures visibility
- No Global Styles: Widget styles don't affect your page
- Check that the element has text content
- Verify the API key is initialized
- Look for console errors
- Ensure the element has
position: relativeorposition: absolute
- Verify your API key is valid
- Check network tab for CORS errors
- Ensure you're using HTTPS in production
- Check your Coolhand dashboard for domain whitelisting
- The widget uses Shadow DOM when available
- Try increasing parent element's z-index
- Check for
overflow: hiddenon parent elements
Apache-2.0 License - see LICENSE file for details.
- Create a Free Coolhand Account: coolhandlabs.com
- Coolhand API Documentation: coolhandlabs.com/docs
- Issues: GitHub Issues
- Email: team@coolhandlabs.com