Walkthrough of a typed carousel with Formik and Styled Components integration

Ross Bulat
Aug 12 · 16 min read

A Real-world TypeScript & React Example Project

TypeScript has great support with React and the surrounding packages within the ecosystem — this piece will demonstrate how these technologies can be used hand-in-hand to create a reliable component: A custom-built carousel that houses and integrates with forms.

We will be walking through this project step-by-step — it is also available to clone on Github here. The carousel is designed to be integrated with form state and there are links for each stage of the carousel that turns green when that section of the form is completed. Each stage has a Formik form embedded within. The carousel is a component itself, FormCarousel, that can take custom components as its stages.

Here is a screencast of the project in action, showing how stages of the carousel are animated and updated to a completed state once a form has been validated (via Formik’s validation function):

TypeScript and React Carousel with Form integration

This solution entails visiting some clever and stylish ways to use TypeScript, React, and JSX together. It is bare-bones, envisaging that developers will want to expand upon this initial setup or amend it to integrate into existing projects.

The project has been developed to be easy to follow and reference:

  • Alphabetical ordering: All CSS properties, component props, and type/interface properties are alphabetically ordered
  • Styled components have been separated from component logic when appropriate for easier referencing and readability
  • Single type source: All FormCarousel specific types are all defined inside types.ts file, that sits in the FormCarousel component folder. We will briefly visit the project structure next, highlighting how the solution is compartmentalized into a range of components for easy customisability
  • Open source: The project can be pulled from GitHub and run locally (and amendments are welcome!). I’ll be briefly covering the installation steps and dependencies later

We will focus more on TypeScript, props and component structure rather than CSS and styling in this talk, although all the styles used can be studied or expanded for individual project needs.

Project Installation and Dependencies

The easiest way to get started is to run the project locally, but we will also cover the setup steps for a bare-bones project too.

Installing the project

The demo is based on Create React App and will be packaged in a familiar fashion. Clone it from GitHub and install the dependencies:

# local setupgit clone https://github.com/rossbulat/ts-react-formik-carousel.gitcd ts-react-formik-carousel
yarn && yarn start

The project is in a minimal state and doesn’t require too many dependencies. If you require a fresh setup, perhaps wanting to follow along to create your own version completely, install a base project using create-react-app with TypeScript included:

# generate typescript CRA
npx create-react-app my-carousel-app --typescript

Note: npx allows us to execute binaries from node modules (or installed NPM packages. It is a global package itself and can be installed with yarn global add npx. We are also assuming that CRA is also installed; do so with yarn global add create-react-app.

From here we can jump into the project directory and install the dependencies:

cd my-carousel-app# styled-components with its types
yarn add styled-components @types/styled-components
# formik form management
yarn add formik
# FontAwesome icons are used for stage buttons
yarn add @fortawesome/react-fontawesome \
@fortawesome/fontawesome-svg-core \

These are all the dependencies we need; this can be verified by referring to the project’s package.json file. This highlights one of the big advantages of a custom-built carousel solution for your app — smaller file sizes.

Custom carousels vs all-in-one carousels

The issue with capable carousel packages is exactly this — they are all-in-one solutions. This is useful for quick development turnarounds and prototyping but comes at a cost like a larger app build. It also becomes much harder to add your own customizations since you have to adhere to all the package’s design principles while trying to avoid breaking what is already there.

Note: Often designed for photo galleries or landing page presentations, I wouldn’t want to think about embedding forms and integrating them into an all-in-one carousel!

Undoubtedly, a custom solution is a better long-term solution for the well-being of your app’s user experience and allows you to keep loading times short and only including features you need. In fact, none of our dependencies here are actually carousel-specific, they can all be used throughout the entire app:

  • Styled components will be used for styling your entire app
  • Formik will be your app’s adopted form manager
  • Only the Font Awesome icons used will be bundled into your final build, and are available for import in other areas of your app

Also, nothing is stopping you from packaging up your custom carousel solution into an NPM package to use throughout your projects. I wrote an article on how to do just that with your own private NPM registry here.

Let’s examine the project structure and view how the FormCarousel components are formulated.

Project Structure

FormCarousel is made of multiple components, all housed under a FormCarousel/ folder. The fundamental concept of our carousel is to be able to slot any number of forms we wish into an arbitrary number of carousel stages.

Note: Stage is the term I have adopted to represent one item of the carousel. Each item is a stage, and each stage hosts a form.

The forms we are embedding into the carousel can be defined anywhere in your project and are given to FormCarousel within its props. The resulting folder structure is as follows:

# component structuresrc/
FormCarousel is not aware of Form components
  • Stage.tsx and Wrapper.tsx are styled-components — simply defining the CSS structure of the carousel
  • StageButton.tsx is a clickable button, that navigates to that stage once clicked
  • index.tsx is the FormCarousel component itself, that assembles the styled-components, stage buttons, and stages. It is all that needs to be imported to assemble a FormCarousel (We’ll visit importing and passing props into FormCarousel in the next section)

Even though form components are decoupled from FormCarousel, they still need to adhere to the types that a carousel stage expects to ensure support for the carousel, such as updating the stage to a completed state. If we peak into the interface for a stage now (we’ll dive a lot more into types and TypeScript specific syntax throughout the piece), we can see exactly what each stage expects in terms of props:

# snippet from FormCarousel/types.tsexport interface FormCarousel_Stage {
form: React.ComponentType<FormCarousel_Form>;
icon: React.ReactNode;
label: string;

Notice the form property, being a generic React component that requires its props to adhere to the FormCarousel_Form type. As long as a form component adheres to this, TypeScript will not throw any compiler errors. We’ll discuss FormCarousel_Form props further down.

With this high-level conceptual understanding of how the carousel is structured, let’s now see how FormCarousel is imported and how forms and other configurations are passed as props into the component. We’ll then work our way down, visiting some interesting syntax along the way.

Importing and Using <FormCarousel />

Using FormCarousel is as easy as importing it and giving it a prop: stages. This is done within App.tsx in the demo project, where we provide each stage with a form, an icon, and a label:

FormCarousel only takes one prop , stages. If we refer to the component signature, we can find exactly what that prop needs to consist of:

export class FormCarousel extends React.Component<FormCarouselProps, FormCarouselState>

By jumping to FormCarousel/types.ts, we can see that FormCarousel expects an array of FormCarousel_Stage’s:

export interface FormCarouselProps {
stages: FormCarousel_Stage[];
export interface FormCarousel_Stage {
form: React.ComponentType<FormCarousel_Form>;
icon: React.ReactNode;
label: string;

Note: CMD +click the type name within your editor (I am using VS Code) to jump to the definition file.

We briefly covered FormCarousel_Stage above, but now we can cover the remaining properties:

  • The icon property expects a React node in the form of a Font Awesome icon. This is because the react-fontawesome package provides us SVG’s wrapped in React components
  • The label property is simply a string that will be displayed in the Stage’s corresponding StageButton, along with the icon

The form property has the most mystery to it, and we need not worry about the generic <FormCarousel_Form> type right now — we are simply providing a React component at this stage and will provide it with props within a JSX element when it comes to rendering the component.

With an array of FormCarousel_Stage's given to FormCarousel, we can now take those props and construct the carousel itself.

Inside the FormCarousel Component

The meat of the logic happens within FormCarousel/index.tsx.

FormCarousel has some state to it, defined as FormCarouselState in our types.ts file:

export interface FormCarouselState {
activeStage: number;
stageOut: number;
stageCompleted: Array<boolean>;

Each stage has an index starting from 0. This is the easiest way to index a carousel, being friendly towards mapping or looping through each stage with a corresponding key matching the index.

  • activeStage stores the current stage being displayed — the default index being 0
  • stageOut tracks the stage being transitioned out. This is solely for animation purposes. We need to know which stage to animate out (we already know activeStage is the stage to animate in). Upon the initial render, a value of -1 is given to ensure none will be transitioning out

stageCompleted is simply an array of booleans representing whether each stage is completed, and is given an empty array upon the initial declaration:

state: FormCarouselState = {
activeStage: 0,
stageOut: -1,
stageCompleted: []

This array length is arbitrary — it depends on how many stages we pass into the component via the stages prop we covered earlier. Because of this, we populate this state value within the component’s constructor, dynamically defining the length of the array based on the number of stages we provided via props:

constructor (props: FormCarouselProps) {

const stages = props.stages.map((item: FormCarousel_Stage) => {
return false;
this.state.stageCompleted = stages;

By iterating through each stage with Array.map(), we can construct a new array, indexing each stage with a value of false. The above snippet does this and updates this.state.stageCompleted with the resulting array.

Note: If you wish to initiate a completed form upon the initial render, this is where you would check your form components to determine whether the form is completed — feel free to add additional props to aid in more configurations.

Carousel interaction methods

FormCarousel defines two methods to update its state that are passed to the StageButton and each form component in order to interact with the carousel itself:

  • toggleActiveStage(): A method to update the active stage of the carousel
  • setStageCompleted(): A method to toggle whether a particular stage has been completed. This will be called within Formik’s handleSubmit() method, as we’ll see further down.

There is nothing groundbreaking about these methods, however, setStageCompleted() follows an important convention of duplicating a state object before manipulating its value, and then applying it to the component state with setState(). This function updates a stage’s completed status in a duplicated state object, before applying it to the state itself:

# duplicating state -> amending -> setState patternconst stageCompleted: Array<boolean> =       
stageCompleted[index] = true;this.setState({ stageCompleted: stageCompleted });

These methods, as well as the component’s state, can now be used to configure the props to be passed down into StageButton and each form component. Rendering these components happens within two mappings that are defined inside the JSX itself.

This is the pattern we are aiming for:

render() {
{ loop through stage buttons and render }
{ include arrow icon in-between buttons }
{ loop through forms, display active form and animate in}
{ also display previous form to animate out }

Note: Mapping an object can also be done outside of JSX, but this is commonly seen in other projects and documentation. Sometimes inline JSX mapping can be easier to read for the developer and keeps all your JSX markup in one place.

Let’s go over how this is done next, touching on some interesting syntax that makes up our render() function.

Rendering the <StageButton />

3 StageButton components with FontAwesome separator arrows in-between

Let’s check out how the StageButton’s are generated, iterating through each props.stages to do so:

There are a couple of interesting things standing out here:

  • We have used React.Fragment to wrap each StageButton and possible FontAwesomeIcon if our mapping index is more than 0 (we do not need an arrow to the left of the first StageButton!)

Why use React.Fragment? JSX render methods must have one topmost component, which would not be the case if we were rendering an icon alongside the StageButton. React.Fragment was introduced for scenarios exactly like this. We have also included the compulsory iterable key prop with React.Fragment.

  • The i parameter of our mapping also acts as our stage index and is passed into the StageButton so it knows which stage it is representing
  • toggleActiveStage() is passed into StageButton as a prop, giving the button access to update the carousel

If we visit the FormCarousel_StageButton interface, we can see all supported StageButton props:

export interface FormCarousel_StageButton {
active: boolean;
complete: boolean;
icon: React.ReactNode;
index: number;
label: string;
toggle (index: number): void;

The toggle method’s signature is indeed included, as is each stage index, and other configuration such as whether the stage is active and completed. These props ultimately determine the style of the button, such as the color and active indicator (the bullet point below the active stage button).

<StageButton>’s styled component

The three states of a StageButton: completed, active and inactive

If we look inside StageButton.tsx itself, we can see that it is being wrapped with a StyledButton component, which handles a click event to update the carousel via props.toggle():

const StyledButton = styled.button<FormCarousel_StyledStageButton>`
return (
disabled={props.active ? true : false}
onClick={() => { props.toggle(props.index) }}
{props.icon !== undefined && props.icon}
{props.active && <span>&bull;</span>}

What is interesting here is that styled-components support generic types, giving us the opportunity to strictly type the styled-components we define. The FormCarousel_StyledStageButton type caters for the props that this styled component requires:

// type specifically for a styled componentexport interface FormCarousel_StyledStageButton {
complete: boolean;
disabled: boolean;
onClick (index: number): void;

The completed prop has been accessed multiple times within StyledButton itself to determine whether the default grey or completed green color should be assigned to a property — border being one of these properties:

// accessing props to determine property valuesconst completedColor = '#28a81b';
const inactiveColor = '#888';
const activeColor = '#000';
const StyledButton = styled.button<FormCarousel_StyledStageButton>`
border: 1px solid ${props => props.complete === true ? `${completedColor} !important` : inactiveColor};

The complete implementation of StageButton can be found here.

Rendering Stages (Form components)

Like StageButton’s, we also iterate through FormCarousel’s props.stages and render the appropriate forms, along with the required animation for the form entering and the form exiting.

The loop resembles the following inline JSX:

// rendering forms in-line JSX<div>
{this.props.stages.map((stage: FormCarousel_Stage, i: number) => {
const Form: React.ComponentType<FormCarousel_Form> =
return (
<Form ... />

Notice how we are grabbing stage.form and typing the new object as React.ComponentType<FormCarousel_Form>. This is required to tell the Typescript compiler exactly what stage.form is. We then render Form as a JSX element along with all the required props.

The FormCarousel_Form interface defines the props each form component needs, the 4 bold properties being significant to our form:

export interface FormCarousel_Form {
className: string;
index: number;
key: number;
setCompleted (index: number, completed: boolean): void;
toggleStage (index: number): void;
transition: StageTransition;

Along with the two carousel methods, we are defining the form’s stage index and required key prop from the iterable — both being identical.

transition is the interesting property here — it has a specific type that restricts the values it can accept. Let’s explore this further.

Stage transition keyframe animation

The StageTransition type defines all possible transition values, that determine what keyframe animation actually takes place for a stage:

export type StageTransition = 
'stage_in_left' | 'stage_out_left' |
'stage_in_right' | 'stage_out_right' |

StageTransition is a string literal type that defines the keyframe animation names that transition accepts. A stage can either enter or exit, from the left or the right. We can indeed calculate the correct transition by just referring to the active stage index and previous stage index.

This is exactly what has been done within the transition prop, by simply embedding multiple expression ? true : false statements to determine the resulting stage transition:

This could also have been done as a cluster of if-else statements, but this method allows us to keep syntax to a minimum — and kept as inline JSX.

As your animations become more complex, you may wish to create your own animation controller class instead of embedding this logic as a prop! In any case, the above block is stating:

  • If the activeStage index is less than the stageOut index, transition activeStage from the left, else transition activeStage from the right
  • If the activeStage index is less than the stageOut index, transition stageOut to the right, else transition stageOut to the left
  • Only apply transitions to activeStage and stageOut indexes. All other stages are given a none transition

The other prop we have not mentioned, className, will hold a value of hidden for all stages that are not activeStage or stageOut:

this.state.activeStage !== i && this.state.stageOut !== i
? `hidden`
: ``

With this logic in place, we are ensuring that only the two stages to transition in and out are displayed, with the remaining stages hidden.

Now, the keyframe animation itself is defined within Stage.tsx, a styled component that defines all our form related CSS and wraps our form components.

The transition prop is firstly passed into the styled component:

// from within any Form component (Details, Bio, Submit)<Stage transition={props.transition}>

With the component now aware of the correct transition, it can be embedded within the animation property. Notice also how our StateTransition string literal values match the keyframe animation names:

This concludes the animation solution of the carousel. To recap, only the active stage and exiting stage need to be shown, and have an in and out animation attached to them respectively.

All that is left to visit now is the forms themselves, managed via Formik via the withFormik() HOC.

Carousel Formik Forms

This section utilizes Formik’s HOC, withFormik(), to wrap Formik functionality around a form. I have published an article focussed on Formik and this HOC that may be a useful read before diving into this section, that can be found here.

What we will be focusing on here is how TypeScript is utilized to make Formik type-safe, in terms of our custom carousel props and the “FormikBag” — Formik’s own range of props used to manage the wrapped form.

As we saw in App.tsx, we have three form components that have been imported as props into the carousel; Details, Bio, and Submit. These components have deliberately been kept simple as not to confuse the reader or go too far out of the scope of the talk, sticking to one input per form.

Note: Having a carousel to manage a form would support a design paradigm of only having 2–3 inputs per stage, making the form itself more digestible for the user. Forms are a great use case for carousels that should be used more often in modern app development.

Let’s take a look at Detail to visit the important concepts of typing a Formik form.

Deconstructing the details form

Each Formik form in this project is constructed in the same way:

  • A presentation component defining the form markup, utilizing the components Formik provides are Form, Field, ErrorMessage, etc.
  • The withFormik() HOC wrapping the presentational component, defining Formik specific functions such as validation, submission, and initial form values

Let’s examine the presentational component signature to understand the range of props being injected into it:

const DetailsForm: React.FC<FormCarousel_Form & FormikProps<Form_Details>> = (props) => {

We have defined an intersection type, combining FormCarousel_Form and FormikProps<Form_Details>> that will make up our component props.

FormDetails simply defines the form values we are expecting. In our case, the Details form only consists of an email field:

export interface Form_Details {
email: string

FormikProps<T>, a type that facilitates the range of props withFormik() injects, provides a generic type for passing in our form values types too. Hence we arrive at FormikProps<Form_Details>. FormCarousel_Form, on the other hand, facilitates all the props we passed in FormCarousel/index.tsx.

Once DetailsForm is defined, we move onto wrapping it with withFormik().

withFormik HOC integration

The formikHOC manages validation, as seen here with an empty Bio error message

Referring to the TypeScript Formik documentation, we need to provide the HOC with two generic types — form props and form values. This is how it looks:

const Details = withFormik<FormCarousel_Form & FormProps, Form_Details>({

We have again provided the props we have passed from FormCarousel in combination with Formik props and the form values with Form_Details. With the types taken care of, we can go about our business as we would in Formik, defining the validation, initial values, etc.

What is interesting here is that in handleSubmit() we are grabbing setCompleted() and setActiveStage() from our custom props, now residing in props.formikBag. Once the form is successfully submitted, these methods will execute, consequently updating the carousel state:

handleSubmit: (values, formikBag) => {
formikBag.props.setCompleted(formikBag.props.index, true);
formikBag.props.toggleStage(formikBag.props.index + 1);

This is how our Formik managed forms interact with the carousel component. Upon a change of state, the carousel re-renders, which in turn causes a stage transition.

The carousel in a completed state, as highlighted by each StageButton

Summing Up

This walkthrough has attempted to show a real-world use case using TypeScript, React, and Formik (and styled-components!) to create a useful re-usable component to facilitate form display.

The solution has been fully typed and is ready to be expanded upon to suit projects on an individual basis.

Learn more on the concepts used in this project

Learn more about TypeScript and the use of Generics in my article specifically on the subject:

To brush up on styled-components, check out how to use it in conjunction with a light and dark mode for your apps:

And for Formik specific studies, check out my article focussing on the withFormik() HOC and other components Formik has to offer:

Better Programming

Advice for programmers.

Thanks to Zack Shapiro

Ross Bulat

Written by

Director @ JKRB Investments

Better Programming

Advice for programmers.

Welcome to a place where words matter. On Medium, smart voices and original ideas take center stage - with no ads in sight. Watch
Follow all the topics you care about, and we’ll deliver the best stories for you to your homepage and inbox. Explore
Get unlimited access to the best stories on Medium — and support writers while you’re at it. Just $5/month. Upgrade