Family Drawer

Crossfade

I’ve updated the UI of the drawer between this lesson and the last one. This is a course about animations, so we won’t focus too much on the styling, but let’s go through it so you know what changed.

Family Drawer Crossfade

Overview

I’ve updated the UI of the drawer between this lesson and the last one. This is a course about animations, so we won’t focus too much on the styling, but let’s go through it so you know what changed.

I’ve split the component into a few files so that it’s easier to work on the exercises later. We now have a components file that contains all the views.

These views are then used in our useMemo hook. useMemo is usually my preferred approach for conditional rendering with multiple possible outcomes. If it were just two components, I would use a ternary.

const content = useMemo(() => {
  switch (view) {
    case "default":
      return <DefaultView setView={setView} />;
    case "remove":
      return <RemoveWallet setView={setView} />;
    case "phrase":
      return <Phrase setView={setView} />;
    case "key":
      return <Key setView={setView} />;
  }
}, [view]);
const content = useMemo(() => {
  switch (view) {
    case "default":
      return <DefaultView setView={setView} />;
    case "remove":
      return <RemoveWallet setView={setView} />;
    case "phrase":
      return <Phrase setView={setView} />;
    case "key":
      return <Key setView={setView} />;
  }
}, [view]);

I use a lot of arbitrary values in Tailwind. For example, instead of mt-5, I use mt-[21px]. I usually try to avoid this, but in this case, Benji shared Family’s Figma file with me, and I wanted to ensure the spacing is as close as possible to the original.

clsx is a library I use pretty often, it is used to conditionally add classes to an element. You could also create your own utility function, but this one also takes care of objects and doesn’t weigh much.

<button
  className={clsx(
    "grid place-items-center",
    className,
  )}
/>
<button
  className={clsx(
    "grid place-items-center",
    className,
  )}
/>

Or when I need to add a class conditionally when the state of the component changes for example.

<div
  className={clsx("size-16", {
    "bg-blue-400": isActive,
    "bg-gray-400": isDisabled,
  })}
/>
<div
  className={clsx("size-16", {
    "bg-blue-400": isActive,
    "bg-gray-400": isDisabled,
  })}
/>

The buttons now have a small scale transition on press, we go from 1 to 0.95 with the default transition settings from tailwind.

Last thing is the font. Family uses a custom font in their app, it’s a sans-serif font that is a bit rounded. The closest, free font I could find was Open Runde. It’s a rounded variant of Inter. This is important as in recreations like this one it’s not only about the animations, it has to look like the original as well.

In the code editor here I assign the font inline, that’s a limitation of Sandpack, which I use under the hood. I don’t usually inline this type of styles.

Crossfade

By updating the styles we get a lot closer to the actual Family iOS drawer. Let’s now add the crossfade animation to it. This is how the end result should look like::

"use client";

import { useMemo, useState } from "react";
import { Drawer } from "vaul";
import useMeasure from "react-use-measure";
import { motion, AnimatePresence } from "motion/react";
import { DefaultView, Key, Phrase, RemoveWallet } from "./components";
import { CloseIcon } from "./icons";

export default function FamilyDrawer() {
const [isOpen, setIsOpen] = useState(false);
const [view, setView] = useState("default");
const [elementRef, bounds] = useMeasure();

const content = useMemo(() => {
  switch (view) {
    case "default":
      return <DefaultView setView={setView} />;
    case "remove":
      return <RemoveWallet setView={setView} />;
    case "phrase":
      return <Phrase setView={setView} />;
    case "key":
      return <Key setView={setView} />;
  }
}, [view]);

return (
  <>
    <button
      className="fixed top-1/2 left-1/2 -translate-y-1/2 -translate-x-1/2 h-[44px] rounded-full border border-gray-200 bg-white px-4 py-2 font-medium text-black transition-colors hover:bg-[#F9F9F8] focus-visible:shadow-focus-ring-button md:font-medium"
      onClick={() => setIsOpen(true)}
	style={{ fontFamily: "Open Runde" }}
    >
      Try it out
    </button>
    <Drawer.Root open={isOpen} onOpenChange={setIsOpen}>
      <Drawer.Portal>
        <Drawer.Overlay
          className="fixed inset-0 z-10 bg-black/30"
          onClick={() => setIsOpen(false)}
        />
        <Drawer.Content
          asChild
          className="fixed inset-x-4 bottom-4 z-10 mx-auto max-w-[361px] overflow-hidden rounded-[36px] bg-[#FEFFFE] outline-hidden md:mx-auto md:w-full"
        >
          <motion.div animate={{ height: bounds.height }}>
            <Drawer.Close asChild>
              <button
                data-vaul-no-drag=""
                className="absolute right-8 top-7 z-10 flex h-8 w-8 items-center justify-center rounded-full bg-[#F7F8F9] text-[#949595] transition-transform focus:scale-95 focus-visible:shadow-focus-ring-button active:scale-75"
              >
                <CloseIcon />
              </button>
            </Drawer.Close>
            <div ref={elementRef} className="px-6 pb-6 pt-2.5 antialiased" style={{ fontFamily: "Open Runde" }}>
              {content}
            </div>
          </motion.div>
        </Drawer.Content>
      </Drawer.Portal>
    </Drawer.Root>
  </>
);
}