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. SelectNone
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:
- Add
"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"],
.
- Change
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
andstyled
which will help us make styled-components with Tailwind.
- We'll be importing
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 ouropen
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.
- If you look at the code below, you'll realize that we're using
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:
- We'll add React's
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 thechildren
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). ForonClick
, we'll define it with a function that uses React's defaultMouseEvent
interface. TypeScript pros have a saying that you shouldnever
use theany
type, but for brevity, we'll just givechildren
anany
type. - With the help of
FC
, we can then pass in our defined props to define our extended component.
- Next, we make a type called
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 theGlobalStyles
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.