Patterns · AhaSlides surfaces
CSAT
The shared thumbs up/down satisfaction prompt — the one CSAT control, not a bespoke rating.
Examples
State
Binary satisfaction · borderless single row — unrated · rated up · rated down · thank-you
Thumbs-down → feedback popover — click the down thumb to open the free-text follow-up
<!-- Paste-and-run: save as .html and open in a browser. No build step.
<aha-csat> is the SAME shared custom element React and Vue consume — here in its native
form. Its thumbs are the shared <aha-icon>, and a thumbs-down opens a feedback popover built
from the shared <aha-popover> + <aha-counted-textarea> + <aha-button> — all loaded by the
element itself. Theming comes only from the --aha-* tokens in tokens.css. -->
<link rel="stylesheet" href="https://cdn.jsdelivr.net/gh/ahaslides-product/ahaslides-design@master/lib/tokens.css">
<script type="module">
import 'https://cdn.jsdelivr.net/gh/ahaslides-product/ahaslides-design@master/lib/aha-csat.js'; // registers <aha-csat> (+ <aha-icon>, <aha-popover>, <aha-counted-textarea>, <aha-button>)
</script>
<!-- The borderless single row (the only layout): prompt + 16px thumbs on one line, no chrome.
Thumbs-up rates instantly; thumbs-down opens the free-text feedback popover.
`thanks` opts into the confirmation line, shown after the up rating / feedback submit. -->
<aha-csat prompt="How's your experience?" source="docs_footer" thanks="Thanks for the feedback!"></aha-csat>
<script>
const csat = document.querySelector('aha-csat');
csat.addEventListener('rate', (e) => {
console.log('CSAT', e.detail.rating, 'from', e.detail.source); // { rating: 'up' | 'down', source }
});
csat.addEventListener('feedback', (e) => {
console.log('CSAT feedback', e.detail.feedback, 'from', e.detail.source); // { rating: 'down', source, feedback }
});
</script>
import { useEffect, useRef } from 'react';
import '@ahaslides-product/design/aha-csat'; // registers <aha-csat> (+ <aha-icon>, <aha-popover>, <aha-counted-textarea>, <aha-button>)
// The element emits composed `rate` and `feedback` events — attach ref listeners (React <19 pattern).
function AhaCsat({ prompt, source, onRate, onFeedback }) {
const ref = useRef(null);
useEffect(() => {
const el = ref.current;
const rate = (e) => onRate?.(e.detail); // { rating: 'up' | 'down', source }
const feedback = (e) => onFeedback?.(e.detail); // { rating: 'down', source, feedback }
el.addEventListener('rate', rate);
el.addEventListener('feedback', feedback);
return () => { el.removeEventListener('rate', rate); el.removeEventListener('feedback', feedback); };
}, [onRate, onFeedback]);
return <aha-csat ref={ref} prompt={prompt} source={source} />;
}
// usage — thumbs-up rates instantly; thumbs-down opens the free-text feedback popover
<AhaCsat
prompt="How's your experience?"
source="editor_help"
onRate={(d) => track('CSAT_RATED', d)}
onFeedback={(d) => track('CSAT_FEEDBACK_SUBMITTED', d)}
/>
// main.ts — register the element + mark aha-* as custom elements
import '@ahaslides-product/design/aha-csat';
app.config.compilerOptions.isCustomElement = (tag) => tag.startsWith('aha-');
// Component.vue — the custom element's `rate` and `feedback` events bind natively with @rate / @feedback
<script setup>
function onRate(e) {
track('CSAT_RATED', e.detail); // { rating: 'up' | 'down', source }
}
function onFeedback(e) {
track('CSAT_FEEDBACK_SUBMITTED', e.detail); // { rating: 'down', source, feedback }
}
</script>
<template>
<!-- The borderless single row (the only layout). Thumbs-up rates instantly; thumbs-down opens the
free-text feedback popover, which emits `feedback` on submit. -->
<aha-csat prompt="How's your experience?" source="editor_help" @rate="onRate" @feedback="onFeedback" />
</template>
API
| Prop | Type | Default | Notes |
|---|---|---|---|
prompt | string | — | Optional question shown before the thumbs |
source | string | — | Stable placement token for analytics — passed in the rate and feedback events |
value | up | down | — | The current rating (also reflects a click); syncs the selected thumb's aria-pressed |
thanks | string | — | Opt-in thank-you line — shown (role=status) after rating (on down, after feedback submit); fades in on a persistent node |
feedback-prompt | string | What could be better? | Heading shown at the top of the thumbs-down feedback popover |
feedback-placeholder | string | Tell us more (optional) | Placeholder for the free-text field in the feedback popover |
Install
# .npmrc — once: point the @ahaslides-product scope at GitHub Packages
@ahaslides-product:registry=https://npm.pkg.github.com
//npm.pkg.github.com/:_authToken=${GITHUB_TOKEN} # a GitHub token with read:packages
npm i @ahaslides-product/design
import '@ahaslides-product/design/tokens.css'; // once, at the app root
import '@ahaslides-product/design/aha-csat'; // registers <aha-csat>
Agent feed for this component (absolute, fetchable anywhere): csat.agent.json · csat.md · csat.llms.txt
When to use
When to use
- CSAT — a lightweight satisfaction check on a feature or a piece of content
- Result — a full-block outcome after an operation
- a Form — you need structured, multi-field feedback with validation
Use the shared control — never build a bespoke rating. Always pass a stable `source` per placement so segmentation stays typo-proof. Keep it to the binary thumbs; free-text is asked for ONLY after a thumbs-down (the built-in feedback popover), and never block rendering on tracking.
Surfaces
editor dashboard audience
Spec
Layout borderless single row (21px high): 14px #4A4A4A prompt + 16px thumb icons, no chrome — the only layout (legacy `inline` is a no-op) · Rest icon #8A8A8A · Hover scale 1.12 · icon primary #6A1EBB (motion tokens) · Selected icon primary #6A1EBB · aria-pressed synced · Thumbs-up rates instantly — reflects value='up', emits `rate` · Thumbs-down registers value='down' + emits `rate`, then opens a feedback popover (shared <aha-popover>, role=dialog · Esc/outside-click close) anchored to the down thumb — a prompt, an <aha-counted-textarea> and a primary Send button; dismissing keeps the down rating · Thank-you opt-in `thanks` — check + success #16C49A line fades in (motion tokens): instantly on up, after feedback submit on down · Events `rate` → { rating: 'up' | 'down', source } · `feedback` → { rating: 'down', source, feedback }