How I Made a Navbar Component in Gatsby (React) with TypeScript, and Twin Macro

How I Made a Navbar Component in Gatsby (React) with TypeScript, and Twin Macro

Featured on Hashnode

If you don't know what Twin Macro is, check it out! It's a great library that blends Tailwind CSS and Styled Component systems, like Emotion. I love using Twin because it lets me use Tailwind while separating my styling from my markup and methods. If you're someone who likes using styled-components for your React projects, let me show you how I did it. By the way, even though I'm using Gatsby, just know that Twin Macro is compatible with create-react-app and Next.js.

Preliminaries:

  • For a beginner's look into Gatsby with TypeScript, please check this article out.
  • If you haven't already, install the Gatsby CLI by running $ npm install -g gatsby-cli.
  • Create a new repository by running $ npx gatsby new {APP_NAME}. You could also do this by running $ npm init gatsby. Doing it this way will bring up some prompts that you could follow through to set your project up. Select None when it asks you for your preferred design system.
  • Install Emotion by running $ npm install @emotion/react @emotion/styled gatsby-plugin-emotion.
    • Add gatsby-plugin-emotion to your list of plugins in gatsby-config.js.
    • Add Twin as a babel preset by going to package.json and add this snippet:
"babelMacros": {
    "twin": {
        "preset": "emotion"
    }
},
  • Run $ tsc --init --jsx react to build the tsconfig.json file. You'll see a bunch of commented and uncommented lines.
    • Look for the following properties and uncomment them to get your file to look something like this:
{
  "include": ["./src/**/*"],
  "compilerOptions": {
    "target": "esnext",
    "module": "esnext",
    "lib": ["dom", "es2017"],
    "jsx": "react",
    "strict": true,
    "esModuleInterop": true,
    "experimentalDecorators": true,
    "emitDecoratorMetadata": true,
    "noEmit": true,
    "skipLibCheck": true
  }
}
  • Create an empty Tailwind config file by running $ npx tailwindcss-cli@latest init.

First Steps:

  • Because we're going to use the styled module from Twin Macro, we're going to have to make some adjustments so that the compiler doesn't scream at us. Let's add some types for React by running $ npm install --save-dev @types/react.
  • Make a declaration file in the root called twin.d.ts and paste the following snippet in it:
import 'twin.macro'
import styledImport from '@emotion/styled'
import { css as cssImport } from '@emotion/react'

declare module 'twin.macro' {
    const styled: typeof styledImport
    const css: typeof cssImport
}
  • In your tsconfig.json file, add "jsxImportSource": "@emotion/react", to "compilerOptions".
    • Change "jsx": "react" to "jsx": "react-jsx".
    • We'll then import our declaration file by changing the include line, like so: "include": ["./src/**/*", "twin.d.ts"],.

The Work Begins:

  • Add a layout file making layout.tsx in src/components/ and add this snippet to it:
import React from 'react'
import { GlobalStyles } from 'twin.macro'

const Layout = ({ children, ...rest }) => (
    <div {...rest}>
        <GlobalStyles />
        {children}
    </div>
)

export default Layout
  • Go to src/pages/index.js and rename it to src/pages/index.tsx. Add this snippet:
import React from 'react
import Layout from '../components/layout'

const IndexPage = () => {
    return (
        <Layout>
            <p>Hello World!</p>
        </Layout>
    )
}

export default IndexPage
  • Create a Navbar component by making a Navbar folder and make an index.tsx file and a styles.tsx file.
    • We'll be importing tw and styled which will help us make styled-components with Tailwind.

The Work Continues:

  • In the styles.tsx file, add this code snippet:
import tw, {styled} from 'twin.macro'

export const Nav = tw.nav`
    relative flex flex-wrap items-center justify-between px-4 py-5 
    md:px-24 lg:px-8 
`
export const InnerNav = tw.div`
    container px-4 mx-auto flex flex-wrap items-center justify-between
`
export const InnerMost = tw.div`
    w-full relative flex justify-between lg:w-auto lg:static lg:block 
    lg:justify-start
`
export const DropDown = styled.div((props: any) => [
    tw`lg:flex flex-grow items-center`,
    props.open ? tw`flex` : tw`hidden`
]) 

export const Logo = tw.a`
    leading-relaxed inline-block mr-4 whitespace-nowrap 
`
export const NavButton = tw.button`
    text-black cursor-pointer text-xl leading-none px-3 py-1 border border-solid 
    border-transparent rounded bg-transparent block lg:hidden outline-none 
    focus:outline-none
`
export const DropMenu = tw.ul`
    flex flex-col lg:flex-row list-none lg:ml-auto md:w-full md:items-center
    lg:justify-end
`
export const ListItem = tw.li`
    block
`
export const InnerItem = tw(ThisLink)`
    px-3 py-2 flex items-center font-sans font-semibold text-lg tracking-wide 
    text-gray-700 hover:text-gray-900 
`
export const ExtraButton = tw.a`
    text-black bg-transparent border border-solid border-gray-900 
    hover:bg-black hover:text-white active:bg-gray-600 font-sans font-semibold 
    uppercase text-sm px-4 py-2 rounded outline-none focus:outline-none mr-1 
    mb-1 mt-4 lg:mt-0 lg:ml-24
`
export const Bars = tw.div`
    mt-2
`
export const Bar = tw.div`
    w-8 h-1 bg-black mb-1 align-middle
`
  • If you look at the code above, you'll see one styled module. This enables us to use props, which we'll need to pass our open data to.
    • NOTE: Don't pay too much to the names of these components. You can name them however you wish, as long as you can follow them and the people that will read your code can follow them.
  • Now we need to get our components into index.tsx. This is where our markup will be housed.
    • If you look at the code below, you'll realize that we're using StaticImage. This component is used when we're not loading dynamic images.
import React from 'react'
import {Nav, InnerNav, InnerMost, DropDown, Logo, NavButton,
    DropMenu, ListItem, InnerItem, ExtraButton, Bars, Bar
} from './styles'
import {StaticImage} from 'gatsby-plugin-image'


const Navbar = () => {

    return (
        <>
            <Nav>
                <InnerNav>
                    <InnerMost>
                        <Logo href="/">
                        <StaticImage 
                            src="../../images/logo.png"
                            alt="logo"
                            width={180}
                            placeholder="none"
                        />
                        </Logo>
                        <NavButton
                            type="button"
                        >
                            <Bars>
                                <Bar />
                                <Bar />
                                <Bar />
                            </Bars>
                        </NavButton>
                    </InnerMost>
                    <DropDown>
                        <DropMenu>
                        <ListItem>
                                <InnerItem
                                    href="/products" 
                                    aria-label="Products"
                                >
                                    Products
                                </InnerItem>
                            </ListItem>
                            <ListItem>
                                <InnerItem
                                    href="/pricing" 
                                    aria-label="Product pricing"
                                >
                                    Pricing
                                </InnerItem>
                            </ListItem>
                            <ListItem>
                                <InnerItem
                                    href="/about" 
                                    aria-label="About us"
                                >
                                    About Us
                                </InnerItem>
                            </ListItem>
                            <ListItem>
                                <InnerItem
                                    href="/faq" 
                                    aria-label="FAQs"
                                >
                                    FAQs
                                </InnerItem>
                            </ListItem>
                            <ExtraButton 
                                href="/contact-us"
                            >
                                CONTACT US
                            </ExtraButton>
                        </DropMenu>
                    </DropDown>
                </InnerNav>
            </Nav>
        </>
    )
}

export default Navbar
  • There are a few things that we need to do now. Currently, our navbar is responsive because of our CSS (Tailwind) styles. However, if we were to click on the hamburger button, nothing will happen. Hence, we'll need to tell React to listen for this click, and we'll need to define a method to expand our menu when clicked.
    • We'll add React's onClick event listener which will fire off our event handler, handleClick().
    • We'll then need to need to define our state that will say whether our navbar is open or not. The handleClick() method will then be in charge of facilitating our state change.
    • Finally, we'll pass our state to the extended component in our styles that has conditional styles depending on whether our navbar is open or not. We'll do this with a prop called open. Our markup will now look like this:
import React, {useState} from 'react'
import {Nav, InnerNav, InnerMost, DropDown, Logo, NavButton,
    DropMenu, ListItem, InnerItem, ExtraButton, Bars, Bar
} from './styles'
import {StaticImage} from 'gatsby-plugin-image'


const Navbar = () => {
    const [navbarOpen, setNavbarOpen] = useState(false)

    function handleClick(){
        setNavbarOpen(!navbarOpen)
    }

    return (
        <>
            <Nav>
                <InnerNav>
                    <InnerMost>
                        <Logo href="/">
                        <StaticImage 
                            src="../../images/logo.png"
                            alt="logo"
                            width={180}
                            placeholder="none"
                        />
                        </Logo>
                        <NavButton
                            type="button"
                            onClick={handleClick}
                        >
                            <Bars>
                                <Bar />
                                <Bar />
                                <Bar />
                            </Bars>
                        </NavButton>
                    </InnerMost>
                    <DropDown open={navbarOpen}>
                        <DropMenu>
                        <ListItem>
                                <InnerItem
                                    href="/products" 
                                    aria-label="Products"
                                >
                                    Products
                                </InnerItem>
                            </ListItem>
                            <ListItem>
                                <InnerItem
                                    href="/pricing" 
                                    aria-label="Product pricing"
                                >
                                    Pricing
                                </InnerItem>
                            </ListItem>
                            <ListItem>
                                <InnerItem
                                    href="/about" 
                                    aria-label="About us"
                                >
                                    About Us
                                </InnerItem>
                            </ListItem>
                            <ListItem>
                                <InnerItem
                                    href="/faq" 
                                    aria-label="FAQs"
                                >
                                    FAQs
                                </InnerItem>
                            </ListItem>
                            <ExtraButton 
                                href="/contact-us"
                            >
                                CONTACT US
                            </ExtraButton>
                        </DropMenu>
                    </DropDown>
                </InnerNav>
            </Nav>
        </>
    )
}

export default Navbar

The Work Ends:

  • Let's go back to our styles.tsx file. This is where we'll get deeper into TypeScript. In React, we have the FC (Function Component) generic type which can be used on arrow functions to define and extend arrow function components. FC allows us to pass props into our component and helps us define default types for it as well. It also includes the children prop type by default, but we will be explicitly defining this in our component.
    • Next, we make a type called ButtonProps where we'll define the props that we know will be passed to our button component (type, children, onClick). For onClick, we'll define it with a function that uses React's default MouseEvent interface. TypeScript pros have a saying that you should never use the any type, but for brevity, we'll just give children an any type.
    • With the help of FC, we can then pass in our defined props to define our extended component.
import React, {FC, MouseEvent} from 'react'
import tw, {styled} from 'twin.macro'

type ButtonProps = {
    type: string,
    children?: any,
    onClick?: (event?: MouseEvent<HTMLButtonElement>) => void,
}

const ThisButton: FC<ButtonProps> = ({onClick, type, children, ...rest}) => 
    <button onClick={onClick} {...rest}>{children}</button>

...

export const NavButton = tw(ThisButton)`
    text-black cursor-pointer text-xl leading-none px-3 py-1 border border-solid 
    border-transparent rounded bg-transparent block lg:hidden outline-none 
    focus:outline-none
`
  • Now in your layout.tsx file, import the Navbar component and place it right below the GlobalStyles component.
import React from 'react'
import { GlobalStyles } from 'twin.macro'
import Navbar from './Navbar'

const Layout = ({ children, ...rest }) => (
    <div {...rest}>
        <GlobalStyles />
        <Navbar />
        {children}
    </div>
)

export default Layout

Conclusion:

  • If you try it out, you will notice that clicking on one of the links in the menu will bring you to a new page with the menu still open. To remedy this, in our Navbar/index.tsx file, we could pass an onClick event listener to each link component, which in our case is <InnerItem />.
    • If we need to, we can define our links like so:
import React, {FC, MouseEvent} from 'react'
import tw, {styled} from 'twin.macro'

type LinkProps = {
    href: string,
    "aria-label"?: string,
    onClick?: (event?: MouseEvent<HTMLAnchorElement>) => void,
}

const ThisLink: FC<LinkProps> = props => <a {...props} />
...

export const InnerItem = tw(ThisLink)`
    px-3 py-2 flex items-center font-sans font-semibold text-lg tracking-wide 
    text-gray-700 hover:text-gray-900 
`
  • Don't feel distressed if you don't get these concepts right away. It just takes some practice, which to be honest. There are so many things that I still need to learn about TypeScript, and so far I'm liking the journey.