Dec 17, 2022

Add a feedback form with Formspree

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?

Using a template

The feedback form is part of the following templates:

Create a new project based on one of these, and follow the steps below.

Get your Formspree form id

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.

Update your project config

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

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.

Omit the form from certain pages

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
---

Building the component from scratch

If you are not using one of the templates that include the feedback form, here is how you can build it yourself.

Create the Feedback component

Create 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.

Insert the component on your pages

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.