Building a product announcement with Radix thumbnail
Guides

Building a product announcement with Radix

Joe McKenney's headshotJoe McKenney
  • December 12, 2023
  • 3 min read

Dopt is headless.

The term headless gets thrown around quite a bit. Devs like ‘headless’ because it implies flexibility, so it’s been coopted by lots of companies. Often it just means that there’s an escape hatch where you can use APIs that were built to be used in lockstep with a companies UI offering, and are borderline unusable as a result

So, let’s say Dopt is headless first. Headless with emphasis ✨.

Our SDKs make no assumptions about presentation, where presentation means UI in this context.

Concretely, this means our SDKs give you state, actions, and content. How you use those is up to you.

Dopt being headless first, by design, means that we can easily build and open-source great UI components that pair naturally with Dopt. They are just UI components that consume the state, actions, and content.

But you can just as easily pair any UI components with Dopt e.g., component libraries like Chakra and Mantine, headless component libraries like Radix UI and React Aria, or even your company’s component library.

At Dopt, our design system is called Bodo, and it is built with React Aria. We sometimes use Bodo components when building onboarding in Dopt, because, say it with me, Dopt is headless, and you can pair it with anything.

To really hammer this point home, I’m going to build a dark-mode announcement example using Radix UI’s dialog component.

Check out the video below!

It took me a few minutes to build this. The workflow was as follows:

Copy the example dialog code from Radix UI’s website.

export function DarkModeAnnouncement() {  return (    <Dialog.Root>      <Dialog.Trigger />      <Dialog.Portal>        <Dialog.Overlay />        <Dialog.Content>          <Dialog.Title />          <Dialog.Description />          <Dialog.Close />        </Dialog.Content>      </Dialog.Portal>    </Dialog.Root>  )

Tweak the styles and structure.

export function DarkModeAnnouncement() {  return (    <Dialog.Root open={true}>      <Dialog.Portal>        <Dialog.Overlay className="DialogOverlay" />        <Dialog.Content className="DialogContent">          <Dialog.Title className="DialogTitle">Title</Dialog.Title>          <Dialog.Description className="DialogDescription">            Body          </Dialog.Description>          <Dialog.Close asChild>            <button>Close</button>          </Dialog.Close>          <Dialog.Close asChild>            <button>Accept</button>          </Dialog.Close>        </Dialog.Content>      </Dialog.Portal>    </Dialog.Root>  )}

Grab the state, actions, and content from Dopt using our React SDK.

import { useModal } from '@dopt/react-modal';
export function DarkModeAnnouncement() {
  const modal = useModal('radix-dark-mode-toggle.dark-mode-announcement');
  return (    <Dialog.Root open={true}>      <Dialog.Portal>        <Dialog.Overlay className="DialogOverlay" />        <Dialog.Content className="DialogContent">          <Dialog.Title className="DialogTitle">Title</Dialog.Title>          <Dialog.Description className="DialogDescription">            Body          </Dialog.Description>          <Dialog.Close asChild>            <button>Close</button>          </Dialog.Close>          <Dialog.Close asChild>            <button>Accept</button>          </Dialog.Close>        </Dialog.Content>      </Dialog.Portal>    </Dialog.Root>  )}

Wire those to the dialog.

import { useTheme } from '@/hooks';import { useModal } from '@dopt/react-modal';import * as Dialog from '@radix-ui/react-dialog';
export function DarkModeAnnouncement() {  const [theme, setTheme] = useTheme('light');
  const modal = useModal('radix-dark-mode-toggle.dark-mode-announcement');
  return (    <Dialog.Root open={modal.active}>      <Dialog.Portal>        <Dialog.Overlay className="DialogOverlay" />        <Dialog.Content className="DialogContent">          <Dialog.Title className="DialogTitle">{modal.title}</Dialog.Title>          <Dialog.Description className="DialogDescription">            {modal.body}          </Dialog.Description>          <Dialog.Close asChild>            <button onClick={() => modal.dismiss()}>              {modal.dismissLabel}            </button>          </Dialog.Close>          <Dialog.Close asChild>            <button              onClick={() => {                setTheme('dark');                modal.complete();              }}            >              {modal.completeLabel}            </button>          </Dialog.Close>        </Dialog.Content>      </Dialog.Portal>    </Dialog.Root>  );}

And voila!