Dec 17, 2022
Gather feedback from visitors with a custom form at the bottom of your pages.
Let your users vote and comment on your content by adding a feedback form:
Was this page helpful?
The feedback form is part of the following templates:
Create a new project based on one of these, and follow the steps below.
Formspree allows you to collect form data from your website. It filters spam and bots, and can integrate with your existing tools, such as Airtable, Google Sheets and Zapier.
Head over to formspree.io, sign in, and create a new form. This will generate an endpoint of the form:
https://formspree.io/f/your-formspree-id
Grab the form id.
Open your Motif project config – that's the file named project-config.js
at the root of your project. Find the section named feedback
, and replace YOUR_FORMSPREE_ID
with the id obtained in the previous step.
feedback: {
formspreeId: "YOUR_FORMSPREE_ID",
}
Publish your site, and see the form appear at the bottom of your pages. When a visitor submits feedback, it will show up in your Formspree dashboard. The feedback consists of a positive/negative flag, optionally a comment, and the path of the page from which the feedback was submitted.
You may want to omit the feedback from some pages. This can be done by adding an includeFeedback
flag in the frontmatter of your page:
---
includeFeedback: false
---
If you are not using one of the templates that include the feedback form, here is how you can build it yourself.
Feedback
componentCreate a component named Feedback.jsx
in your /components
folder, and add the following:
import { useState, useCallback, useRef, useEffect } from "react"
import Tippy from '@tippyjs/react'
import toast, { Toaster, ToastBar } from "react-hot-toast"
export const ThumbUp ({ className }) => <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth={1.5} stroke="currentColor" className={className}>
<path strokeLinecap="round" strokeLinejoin="round" d="M6.633 10.5c.806 0 1.533-.446 2.031-1.08a9.041 9.041 0 012.861-2.4c.723-.384 1.35-.956 1.653-1.715a4.498 4.498 0 00.322-1.672V3a.75.75 0 01.75-.75A2.25 2.25 0 0116.5 4.5c0 1.152-.26 2.243-.723 3.218-.266.558.107 1.282.725 1.282h3.126c1.026 0 1.945.694 2.054 1.715.045.422.068.85.068 1.285a11.95 11.95 0 01-2.649 7.521c-.388.482-.987.729-1.605.729H13.48c-.483 0-.964-.078-1.423-.23l-3.114-1.04a4.501 4.501 0 00-1.423-.23H5.904M14.25 9h2.25M5.904 18.75c.083.205.173.405.27.602.197.4-.078.898-.523.898h-.908c-.889 0-1.713-.518-1.972-1.368a12 12 0 01-.521-3.507c0-1.553.295-3.036.831-4.398C3.387 10.203 4.167 9.75 5 9.75h1.053c.472 0 .745.556.5.96a8.958 8.958 0 00-1.302 4.665c0 1.194.232 2.333.654 3.375z" />
</svg>
export const ThumbDown ({ className }) => <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth={1.5} stroke="currentColor" className={className}>
<path strokeLinecap="round" strokeLinejoin="round" d="M7.5 15h2.25m8.024-9.75c.011.05.028.1.052.148.591 1.2.924 2.55.924 3.977a8.96 8.96 0 01-.999 4.125m.023-8.25c-.076-.365.183-.75.575-.75h.908c.889 0 1.713.518 1.972 1.368.339 1.11.521 2.287.521 3.507 0 1.553-.295 3.036-.831 4.398C20.613 14.547 19.833 15 19 15h-1.053c-.472 0-.745-.556-.5-.96a8.95 8.95 0 00.303-.54m.023-8.25H16.48a4.5 4.5 0 01-1.423-.23l-3.114-1.04a4.5 4.5 0 00-1.423-.23H6.504c-.618 0-1.217.247-1.605.729A11.95 11.95 0 002.25 12c0 .434.023.863.068 1.285C2.427 14.306 3.346 15 4.372 15h3.126c.618 0 .991.724.725 1.282A7.471 7.471 0 007.5 19.5a2.25 2.25 0 002.25 2.25.75.75 0 00.75-.75v-.633c0-.573.11-1.14.322-1.672.304-.76.93-1.33 1.653-1.715a9.04 9.04 0 002.86-2.4c.498-.634 1.226-1.08 2.032-1.08h.384" />
</svg>
export const TooltipContent = ({ path, yes, onDone }) => {
const [loading, setLoading] = useState(false)
const [message, setMessage] = useState("")
const textAreaRef = useRef()
const submitFeedback = useCallback(async () => {
await fetch(`https://formspree.io/f/YOUR_FORMSPREE_ID`, {
method: "POST",
body: JSON.stringify({ page: path, positive: yes, message }),
headers: { 'Accept': 'application/json' }
})
toast.success('Thanks for your feedback!');
onDone?.()
setMessage("")
}, [message, yes, onDone])
return <div className="rounded-md bg-white dark:bg-neutral-800 p-4 shadow-xl flex flex-col gap-2 border dark:border-white/20">
<p className="text-xs">Leave a comment (optional)</p>
<textarea
ref={textAreaRef}
autoFocus={true}
rows={4}
value={message}
disabled={loading}
onChange={(e) => setMessage(e.target.value)}
className="p-2 border rounded-md outline-none w-full resize-none text-sm"
/>
<button
type="submit"
onClick={submitFeedback}
disabled={loading}
className="text-white rounded-md bg-primary-500 hover:bg-primary-600 transition duration-200 w-full text-sm px-2 py-1.5 outline-none">
Submit
</button>
</div>
}
export const TooltipWrapper = ({ children, path, yes, visible, onClickOutside, onDone }) => {
return <Tippy
visible={visible}
onClickOutside={onClickOutside}
content={<TooltipContent
path={path}
yes={!!yes}
onDone={onDone}
/>}
delay={[0, 0]}
duration={[300, 0]}
trigger="click"
interactive
className="w-[300px]"
>
{children}
</Tippy>
}
export const Feedback = ({ path }) => {
const [yesVisible, setYesVisible] = useState(false)
const [noVisible, setNoVisible] = useState(false)
return <div className="rounded-md p-3 flex flex-row gap-2 items-center text-neutral-600 dark:text-white/80 border dark:border-white/20 w-min">
<p className="text-sm truncate mr-2">Was this page helpful?</p>
<TooltipWrapper
path={path}
yes
onDone={() => setYesVisible(false)}
visible={yesVisible}
onClickOutside={() => setYesVisible(false)}>
<button
onClick={() => setYesVisible(!yesVisible)}
className="border rounded-md px-2 py-1 flex flex-row items-center gap-2 hover:bg-neutral-100 dark:hover:bg-white/10 transition duration-200 outline-none dark:border-white/20">
<ThumbUp className="w-4 h-4" />
<p className="font-semibold">Yes</p>
</button>
</TooltipWrapper>
<TooltipWrapper
path={path}
visible={noVisible}
onDone={() => setNoVisible(false)}
onClickOutside={() => setNoVisible(false)}>
<button
onClick={() => setNoVisible(!noVisible)}
className="border rounded-md px-2 py-1 flex flex-row items-center gap-2 hover:bg-neutral-100 dark:hover:bg-white/10 transition duration-200 outline-none dark:border-white/20">
<ThumbDown className="w-4 h-4" />
<p className="font-semibold">No</p>
</button>
</TooltipWrapper>
<Toaster
position="bottom-center"
toastOptions={{
className: 'shadow-md rounded-md text-sm text-white bg-neutral-900',
success: { icon: null },
error: { icon: null },
}}>
{(t) => (
<ToastBar
toast={t}
style={{
...t.style,
animation: t.visible ? 'toast-enter 0.2s ease-out' : 'toast-exit 0.5s ease-in forwards',
}}
/>
)}
</Toaster>
</div>
}
In the code above, find the line that includes YOUR_FORMSPREE_ID
, and replace it with the id obtained from the Formspree dashboard.
You can now insert the Feedback component on your pages. We recommend using a template, for instance:
import { Feedback } from "@components/feedback"
export const Template = ({ children }) => {
return <div>
{children}
<Feedback />
</div>
}
That's it! You now have a feedback form on all the pages using this template.