Feedback
Receive feedback from your users
Overview
Feedback is crucial for knowing what your reader thinks, and help you to further improve documentation content.
Installation
Copy the component.
'use client';
import { cn } from '@/lib/cn';
import { buttonVariants } from 'fumadocs-ui/components/ui/button';
import { ThumbsDown, ThumbsUp } from 'lucide-react';
import { type SyntheticEvent, useEffect, useState } from 'react';
import {
Collapsible,
CollapsibleContent,
} from 'fumadocs-ui/components/ui/collapsible';
import { cva } from 'class-variance-authority';
import { OpenPanel } from '@openpanel/web';
const rateButtonVariants = cva(
'inline-flex items-center gap-2 px-3 py-2 rounded-full font-medium border text-sm [&_svg]:size-4 disabled:cursor-not-allowed',
{
variants: {
active: {
true: 'bg-fd-accent text-fd-accent-foreground [&_svg]:fill-current',
false: 'text-fd-muted-foreground',
},
},
},
);
interface Feedback {
opinion: 'good' | 'bad';
message: string;
}
function get(): Feedback | null {
const url = window.location.pathname;
const item = localStorage.getItem(`docs-feedback-${url}`);
if (item === null) return null;
return JSON.parse(item) as Feedback;
}
function set(feedback: Feedback | null) {
const url = window.location.pathname;
const key = `docs-feedback-${url}`;
if (feedback) localStorage.setItem(key, JSON.stringify(feedback));
else localStorage.removeItem(key);
}
const clientId = process.env.NEXT_PUBLIC_OPENPANEL_CLIENT_ID;
if (!clientId)
throw new Error(
'environment variable missing: `NEXT_PUBLIC_OPENPANEL_CLIENT_ID`',
);
const op = new OpenPanel({
clientId,
});
export function Rate() {
const [previous, setPrevious] = useState<Feedback | null>(null);
const [opinion, setOpinion] = useState<'good' | 'bad' | null>(null);
const [message, setMessage] = useState('');
useEffect(() => {
setPrevious(get());
}, []);
function submit(e?: SyntheticEvent) {
e?.preventDefault();
if (opinion == null) return;
const feedback: Feedback = {
opinion,
message,
};
void op.track(
'on_rate_docs',
feedback as unknown as Record<string, string>,
);
set(feedback);
setPrevious(feedback);
setMessage('');
setOpinion(null);
}
return (
<Collapsible
open={opinion !== null || previous !== null}
onOpenChange={(v) => {
if (!v) setOpinion(null);
}}
className="border-y py-3"
>
<div className="flex flex-row items-center gap-2">
<p className="text-sm font-medium pe-2">How is this guide?</p>
<button
disabled={previous !== null}
className={cn(
rateButtonVariants({
active: (previous?.opinion ?? opinion) === 'good',
}),
)}
onClick={() => {
setOpinion('good');
}}
>
<ThumbsUp />
Good
</button>
<button
disabled={previous !== null}
className={cn(
rateButtonVariants({
active: (previous?.opinion ?? opinion) === 'bad',
}),
)}
onClick={() => {
setOpinion('bad');
}}
>
<ThumbsDown />
Bad
</button>
</div>
<CollapsibleContent className="mt-3">
{previous ? (
<div className="px-3 py-6 flex flex-col items-center gap-3 bg-fd-card text-fd-card-foreground text-sm text-center rounded-xl text-fd-muted-foreground">
<p>Thank you for your feedback!</p>
<button
className={cn(
buttonVariants({
color: 'secondary',
}),
'text-xs',
)}
onClick={() => {
setOpinion(previous?.opinion);
set(null);
setPrevious(null);
}}
>
Submit Again?
</button>
</div>
) : (
<form className="flex flex-col gap-3" onSubmit={submit}>
<textarea
autoFocus
value={message}
onChange={(e) => setMessage(e.target.value)}
className="border rounded-lg bg-fd-secondary text-fd-secondary-foreground p-3 text-sm resize-none focus-visible:outline-none placeholder:text-fd-muted-foreground"
placeholder="Leave your feedback..."
onKeyDown={(e) => {
if (!e.shiftKey && e.key === 'Enter') {
submit(e);
}
}}
/>
<button
type="submit"
className={cn(buttonVariants({ color: 'outline' }), 'w-fit px-3')}
>
Submit
</button>
</form>
)}
</CollapsibleContent>
</Collapsible>
);
}
The above example uses OpenPanel, you can change the code in submit()
to the platform you're using (e.g. PostHog).
import posthog from 'posthog-js';
const feedback: Feedback = {
opinion,
message,
};
posthog.capture('on_rate_docs', feedback);
Add to Page
Now add the <Rate />
component to your docs page:
import defaultMdxComponents from 'fumadocs-ui/mdx';
import {
DocsPage,
DocsTitle,
DocsDescription,
DocsBody,
} from 'fumadocs-ui/page';
import { Rate } from '@/components/rate';
export default async function Page() {
// other logic
return (
<DocsPage toc={toc} full={page.data.full}>
<DocsTitle>{page.data.title}</DocsTitle>
<DocsDescription>{page.data.description}</DocsDescription>
<DocsBody>
<Mdx
components={{
...defaultMdxComponents,
}}
/>
</DocsBody>
<Rate />
</DocsPage>
);
}
How is this guide?