Creating an Accordion Design: Part II

20m

Intermediate

Getting started

We’ll be making this project with Framer. If you don’t have it already, download a free 14-day trial.

Recap

Welcome back! In Part I we left off with a code component containing a design component, a card, which we can tap continuously to go back and forth between open and closed.

In Part II, we’ll see how we can create multiple cards and have a single-select logic where opening any new card will both close the previous open card and open the new card. You can download the finished project file for part I here.

We finished with our code looking something like this:

import * as React from "react"
import { Card } from "./canvas"
export function Accordion() {
const [isOpen, setIsOpen] = React.useState(null)
const cardVariants = {
open: {
height: "174px",
},
closed: {
height: "98px",
},
}
return (
<Card
variants={cardVariants}
onTap={() => {
isOpen ? setIsOpen(false) : setIsOpen(true)
}}
animate={isOpen ? "open" : "closed"}
initial={false}
/>
)
}
COPY

Custom animations

In Part I, we wrote out the majority of the more complex logic behind our interactive card. We can now start enriching our variants by adding custom styling and transitions.

Within our variants, we can add a transition property that will contain the specifics about how we want the animation to transition.

Framer will use a default transition if you do not specify one. This explains why, in Part I, the card animates (the default transition) instead of instantly changing the size without a smooth animation.

Let’s say we’d like the duration of the animation to be slightly faster when we close the card, compared to opening it. To our variant open, we add the transition prop and we set the type to "spring" with a duration of 0.6 seconds.

We do the same for the variant closed, only we give it a shorter duration and we give it a type of "tween".

In my card, I’ve added multiple extra transition properties such as stiffness, damping, and mass to fully tweak it to my needs.

Explore the API docs to find out about all the different types of properties (or props) you can set with transitions.

const cardVariants = {
open: {
transition: {
type: "spring",
delay: 0,
stiffness: 250,
damping: 48,
mass: 3,
duration: 0.8,
},
height: "174px",
},
closed: {
transition: {
type: "tween",
duration: 0.3,
},
height: "98px",
},
}
COPY

Adding a Stack

Remember how we imported the React to our component file in part I? We will not also need to be using the Stack component from Framer’s library. To use a Stack in our component, we first need to import it at the top of our file. After doing this our imports at the top will look something like this:

import * as React from "react"
import { Stack } from "framer"
COPY

We can use the stack component straight from the canvas as one of the Layout tools, but we can thus also use it in code.

If we add a <Stack></Stack> to our component, any nested elements (or “children”) within it distribute automatically. This takes care of most of the layout work we’d otherwise do manually.

Tip: when you draw a stack on the canvas and add any element to it, we do the same. Only now we do it from code!

With the help of the API docs, you can then decide which other properties you can give your stack to make sure its layout is exactly how you want it. In our example, we’ll go with the following:

export function Accordion(props) {
const [isOpen, setIsOpen] = React.useState(null)
const cardVariants = {
// variant specifications
}
return (
<Stack gap={10} alignment="start" distribution="start">
<Card
// card properties
/>
</Stack>
)
}
COPY

Adding cards as properties

Accordion.defaultProps = {
width: 366,
height: 174,
cards: ["cardOne", "cardTwo", "cardThree"],
}
COPY

An accordion design will often have more than just one card. Theoretically, we could copy the entire section of <Card …/> and paste it as many times as we want, but then we simply have copies of our design component with the same default data we added to our design component.

Framer is all about making high-fidelity prototypes, so we’ll do something much faster and more effective.

In React, we can assign default properties to our components so they will for instance have fallback values in case a property is missing.

Framer will make any code component 200px x 200px by default, but with defaultProps, we can tell it to have different default properties. Let’s tell it to match our card’s opened state, which is 366px x174px, by writing it at the bottom outside of our component.

We’re also telling our component to contain an array of cards. This is how we’ll be able to magically create multiple cards for every item in our array.

Generating multiple cards

Setting the overflow property

<Card
overflow="hidden"
variants={cardVariants}
onTap={() => {
isOpen ? setIsOpen(false) : setIsOpen(true)
}}
animate={isOpen ? "open" : "closed"}
initial={false}
/>
COPY

Before we create multiple cards within our component, we need to make sure that the overflow property on our design component is set to hidden so that additional cards will not overlap over other cards.

We can set this property directly on the design component from the canvas, but we can also pass overflow="hidden" as a property to the design component from code.

Mapping over cards

We’ll use a method that’s built into Javascript (and thus also React), called map. This method hops over an array and for every item in it, it will do whatever you tell it to do. We have an array with 3 items, so we’ll tell it to map over it and each time it finds something, it will create a new card for us.

{props.cards.map(card => {
return
<Card key={card} />
})}
COPY

Let’s walk through what the map method does exactly. In Javascript, we can use dot notation to dig into the levels of an object.

Consider the code example above. On the first line, we start by saying we are looking at the props of our current component, on which we then consider the cards property, which became part of the component’s properties the moment we added these to defaultProps.

We can use any properties of our component by, at the beginning of your function, adding props between the brackets right where it says export function Accordion(). Our component will thus say Accordion(props) from here on out. This allows us to e.g. use the array of cards we passed to our component with defaultProps.

Returning to the example above, we finally call the map method on our cards object, which will go over our array of cards and return one instance of <Card/> for each item it finds in our array. As we told our array to contain three items, we receive three instances neatly organized in the stack.

Identifying each unique card

export function Accordion(props) {
const [isOpen, setIsOpen] = React.useState(null)
const cardVariants = {
// variant specifications
}
return (
<Stack gap={10} alignment="start" distribution="start">
{props.cards.map(card => {
return <Card key={card} />
})}
</Stack>
)
}
COPY

Methods sometimes require or accept certain parameters, which we would then add between the brackets. The map method takes in three possible parameters: .map((currentValue, index, arr).

These are parameters that we tell the method to report back to us with a certain value every time it finishes mapping over an item. The currentValue parameter is the only required one for the map method, and the only one we’re using.

When currentValue maps over a single item, it will report back the exact value of that item. You can name currentValue anything, so we’re going with card.

Setting a key

When we tap a card, we’ll need to identify which exact card gets tapped to allow our accordion design to work as expected. We do this with the key property, for which React strongly recommends us to point it to a unique identifying value of each element we create with our map method.

As we have different names for all values in our hypothetical array of cards (cardOne, cardTwo, and cardThree), these are unique and we can thus use card as our key as card will contain one of those three names per card.

Changing the behavior of our card

We are now dealing with three cards. However, every card we tap will open all of the cards as the map method duplicated each card, so tapping one will trigger the state to open for every card.

This is not what we want, so we’re going to add some logic to ensure that tapping one card will only open the tapped card and close any other open cards. Consider the following code and look closely at the differences.

onTap={() =>
isOpen ? setIsOpen(false) : setIsOpen(true)
}
onTap={() =>
setIsOpen(isOpen === card ? null : card)
}
COPY

The first snippet shows the behavioral logic behind our card that we used up to this point. The second is a new approach to ensure that no two cards can be active at the same time.

With our previous onTap logic, we said that when we tap the card, we check whether the card is active. If so, it would then close the card. If the card was closed, however, it would open the card.

This works when we have just one card, but not with multiple. Our new code enables us to open only the card we tap.

Using the key to decide which card to open

onTap={() =>
setIsOpen(isOpen === card ? null : card)
}
COPY

We do this by using card, as we just learned that it will always return the value of the (array) item we just mapped over. As each card contains a different value (i.e. cardOne, cardTwo, cardThree), the result of card is always one of those values and thus unique.

This can be used to decide which card should be open or closed, and therefore ensuring that only one card opens and simultaneously that any other open card closes.

Within our tap event, all we say is that when we tap our card, we want to change the state of isOpen. Just like before, we do this with setIsOpen to tell React to change something in our useState hook.

setIsOpen( // anything within these brackets will be the new state )
COPY

Within the brackets of setIsOpen, we decide what we want our state to change to. We are evaluating another question here and the outcome of this will straight away be what we tell isOpen to be.

setIsOpen(isOpen === card ? null : card)
COPY

The question we evaluate is that if our current open card (or: isOpen) equals the card we just tapped, then we will setIsOpen: (null) so that our card is not considered active anymore and closes.

However, if our current isOpen doesn’t equal the card we just tapped, we instead want it to and so we assign our unique key to our new isOpen state. This means that the newly tapped card is now considered the active card, so it opens.

Updating our animate prop

Our new and more advanced logic requires a tiny update in our animate property from Part I. They currently look like this:

animate={isOpen ? "open" : "closed"}
COPY

However, this would still cause all cards to open at once by tapping any single card. When we tell Framer how our animate behavior should be, we should also take into account which card is active.

Luckily for us, all we need to do is add === card to the equation. Now, when deciding which variants to use, we will first evaluate not just whether isOpen is true or false but instead whether it matches the unique ID of the card we just tapped.

animate={isOpen === card ? "open" : "closed"}
COPY

Finished work

With our code, we now created multiple cards based on an array we mapped over. These cards are all neatly organized within a stack and they have a custom transition when opening versus closing. Finally, we added logic that allows us to tap one card and ensure only this card will be open and others will close or stay closed.

Our final code at the end of this second part:

import * as React from "react"
import { Stack } from "framer"
import { Card } from "./canvas"
export function Accordion(props) {
const [isOpen, setIsOpen] = React.useState(null)
const cardVariants = {
open: {
transition: {
type: "spring",
delay: 0,
stiffness: 250,
damping: 48,
mass: 3,
duration: 0.8,
},
height: "174px",
},
closed: {
transition: {
type: "tween",
duration: 0.3,
},
height: "98px",
},
}
return (
<Stack gap={10} alignment="start" distribution="start">
{props.cards.map(card => {
return (
<Card
key={card}
overflow="hidden"
variants={cardVariants}
onTap={() => setIsOpen(isOpen === card ? null : card)}
animate={isOpen === card ? "open" : "closed"}
initial={false}
/>
)
})}
</Stack>
)
}
Accordion.defaultProps = {
width: 366,
height: 174,
cards: ["cardOne", "cardTwo", "cardThree"],
}
COPY

Review

Download the finished project for part II ›

In Part III, we’ll focus on implementing real data in our accordion design in various ways.

We will finish by learning how to make the number of cards we have depend on (real) data from a JSON file instead of an arbitrary ‘hard-coded’ array. We’ll also learn how to override the text values within our Card design component and replace it with more realistic data from our JSON file.

Got stuck before the finish line somewhere? Feel free to reach out by sending me a tweet. If you’d rather start a discussion head on over to our communities at Spectrum or Slack. Of course, you can also contact support directly.