Animations

Many Reshaped components come with built-in micro-interactions, ranging from simple Button hover and click transitions to more complex animations like those in DropdownMenu.

One of the main themes of Reshaped is its focus on layout composition and flexibility. We provide you with the building blocks so you can combine them based on your product needs. This approach gives you more control over how you animate component layouts.

The most common way to build such animations today is by using framer-motion. It allows you to wrap Reshaped components with motion elements and animate their composition.

Here is an example where we use layout and presence animations in framer-motion. Whenever you submit the form, it slides to the left and shows a confirmation message. With the compositional approach, we can animate the Card height based on the content. We use View without wrapping to ensure both content items are positioned in a row.

function CardAnimation() {
  const [done, setDone] = React.useState(false);
  const transition = { ease: "easeInOut", duration: 0.5 };

  return (
    <View padding={4} width="360px">
      <Card padding={5}>
        <LayoutGroup>
          <View direction="row" align="start" wrap={false}>
            <AnimatePresence initial={false}>
              {!done && (
                <motion.div
                  initial={{
                    x: "-100%",
                    height: 56,
                    opacity: 0,
                    width: "100%",
                  }}
                  animate={{
                    x: 0,
                    height: "auto",
                    opacity: 1,
                    width: "100%",
                  }}
                  exit={{ x: "-100%", height: 56, opacity: 0 }}
                  transition={transition}
                  layout
                >
                  <View gap={3}>
                    <TextArea
                      name="message"
                      placeholder="Enter your message"
                      variant="faded"
                      resize="none"
                    />
                    <Button color="primary" onClick={() => setDone(true)}>
                      Submit
                    </Button>
                  </View>
                </motion.div>
              )}
            </AnimatePresence>

            <AnimatePresence>
              {done && (
                <motion.div
                  initial={{ x: 0, opacity: 0, width: "100%", height: "auto" }}
                  animate={{ x: "-100%", opacity: 1, height: "auto" }}
                  exit={{ x: 0, opacity: 0, width: "100%", height: "auto" }}
                  layout
                  transition={transition}
                >
                  <View align="center" gap={2}>
                    <Text variant="body-3" weight="medium" align="center">
                      Your message was submitted!
                    </Text>
                    <Button size="small" onClick={() => setDone(false)}>
                      Send more
                    </Button>
                  </View>
                </motion.div>
              )}
            </AnimatePresence>
          </View>
        </LayoutGroup>
      </Card>
    </View>
  );
}

Similarly, it's very easy to handle animations when dynamically adding and removing content by wrapping Reshaped components with motion.li wrappers. framer-motion takes care of everything required for handling layout animations and component lifecycles.

function ListAnimation() {
  const [items, setItems] = React.useState([1]);

  return (
    <View gap={2}>
      <View gap={1} as="ul">
        <AnimatePresence initial={false}>
          {items.map((i) => (
            <motion.li
              key={i}
              layout
              initial={{ height: 0, opacity: 0 }}
              animate={{ height: "auto", opacity: 1 }}
              exit={{ height: 0, opacity: 0 }}
            >
              <Card>Item {i}</Card>
            </motion.li>
          ))}
        </AnimatePresence>
      </View>

      <View direction="row" gap={2}>
        <View.Item grow>
          <Button
            fullWidth
            disabled={items.length === 1}
            onClick={() => {
              setItems((prev) => prev.slice(0, -1));
            }}
          >
            Remove
          </Button>
        </View.Item>
        <View.Item grow>
          <Button
            fullWidth
            onClick={() => {
              setItems(prev => [...items, items.length + 1]);
            }}
            disabled={items.length >= 5}
            color="primary"
          >
            Add
          </Button>
        </View.Item>
      </View>
    </div>
  );
}

Some components, like Tabs, are compound, meaning you can create custom layouts for them and animate their state updates. In the following example, we animate the selected tab panel visibility using the AnimatePresence component from framer-motion and motion.div inside the Tab.Panel.

function TabAnimation() {
  return (
    <Tabs>
      <View gap={4}>
        <Tabs.List>
          <Tabs.Item value="1" icon={IconZap}>
            Item 1
          </Tabs.Item>
          <Tabs.Item value="2">Long item 2</Tabs.Item>
          <Tabs.Item value="3">Very long item 3</Tabs.Item>
        </Tabs.List>

        <AnimatePresence mode="wait">
          {["1", "2", "3"].map((i) => (
            <Tabs.Panel value={i}>
              <motion.div
                key={i}
                initial={{ y: 20, opacity: 0 }}
                animate={{ y: 0, opacity: 1 }}
                transition={{ duration: 0.4 }}
              >
                <View
                  padding={6}
                  textAlign="center"
                  backgroundColor="neutral-faded"
                >
                  <View.Item>Tab {i}</View.Item>
                </View>
              </motion.div>
            </Tabs.Panel>
          ))}
        </AnimatePresence>
      </View>
    </Tabs>
  );
}