Responsive Elements with Tailwind CSS

.

02-01-2023

In this article I will demonstrate how I built the navigation features for this website. First, I will walkthrough the two states in which it is displayed and how I achieved it using Tailwind CSS. Second, I will demonstrate how I acheived the theme switching function found on the far right of the navigation bar. Listed below are the main tools I used to accomplish these functions.

  • Tailwind CSS
  • Next-Themes
  • React useState and useContext hooks
  • Next.js Link Component

First, we need to import three components into our navigation bar file.

// components/navbar.tsx
 
import Link from "next/link"
import ThemeSwitch from './Themes'
import { useState } from "react"

The second thing we want to do is set a value for the useState hook that we just imported from react.

// components/navbar.tsx
 
const NavBar = () => {
  const [showMenu, setShowMenu] = useState(false)
    return (
      // insert code here
    )
  }

Now that we have defined our state, we can begin building out our NavBar component. The first 'section' element will contain the navigation bar displayed on large screens.

// components/navbar.tsx
  <section id='widescreen' className='text-white max-w-4xl mx-auto p-4 flex 
  justify-between items-center'>
    <div className=' py-2 m-2 absolute right-6'>
      <ThemeSwitch />
    </div>
    <div>
      <button id="hamburger" className='text-3xl
       md:hidden cursor-pointer' onClick={() => setShowMenu(!show)}> 
            &#9776;
        </button>
        <nav id='Widescreen Navigation' className='hidden md:block 
        space-x-8 text-xl' aria-label="widescreen-menu">
          <Link className='hover:opacity-90' href="/">Home</Link>
          <Link className='hover:opacity-90' href="/posts">Blog</Link>
          <Link className='hover:opacity-90' href="/about">About</Link>
          <Link className='hover:opacity-90' href="/guestbook">Guestbook</Link>
          <Link className='hover:opacity-90' href="/contact">Contact Me</Link>
          <Link className='hover:opacity-90' href="/tech">Tech</Link>
        </nav>
    </div>
  </section>

In the codeblock above we have the hamburger button, which is only displayed if the window is medium or smaller. It recieves an onClick attribute which will set our 'show' state to true when it is clicked. Contrarily, we have a similar action here on the cross button. if 'X' is clicked, show is set to false, and the mobile menu drawer is hidden.

// components/navbar.tsx
  {show? 
    <section id='mobile-menu' className='absolute py-4 pb-12 
    bg-white min-h-full dark:bg-black w-full text-5xl flex-col 
    justify-content-center origin-top-left animate-open-menu lg:hidden' >
      <button id='cross' className='text-6xl absolute right-2 px-6 text-slate-700 
      dark:text-white animate-close-menu ' onClick={() => setShowMenu(!show)}> 
        &times;
      </button>
      <nav className='flex flex-col min-h-screen items-left py-8  
       text-black dark:text-white bg-white dark:bg-black' aria-label='mobile-menu'>
        <MobileNav />
      </nav>
    </section> : null}
    )
}

You might also notice the hidden attributes assigned to these buttons and the sections that contain them. For example, the lg:hidden attribute on the mobile menu means that it can only be seen if the window size is smaller than what we have assigned for large. This is possible because Tailwind CSS allows us control how something is rendered based on the screen size.

if you're curious how this is set up in our tailwind.config file it will look something like this:

// tailwind.config.js
theme: {
    extend: {
     screens: {
        'xs': '250px',
        // => @media (min-width: 250px) { ... }
        'sm': '340px',
        // => @media (min-width: 340px) { ... }
        'smt': '500px',
        // => @media (min-width: 500px) { ... }
        'md': '750px',
        // => @media (min-width: 750px) { ... }
        'lg': '1024px',
        // => @media (min-width: 1024px) { ... }   
        'xl': '1280px',
        // => @media (min-width: 1280px) { ... }   
        '2xl': '1536px',
        // => @media (min-width: 1536px) { ... }  
        },
      },
    }    
       

Inside of the tailwind.config file we can add as many custom classes and sizes as we would like. For example, the animation when the mobile menu is opened is also configured here.

// tailwind.config.js
theme: {
  extend: {
    keyframes: {  
      'open-menu': {
        '0%': { transform: 'scaleX(0)' },
        '80%': { transform: 'scaleX(1.2)' },
        '100%': { transform: 'scaleX(1)' },
      },
    },
    animation: {
      'open-menu': 'open-menu 0.5s ease-in-out forwards',
    },
  },
}    

Themes with Tailwind CSS

In order to make our theme available to the entire application, we have to provide it as context to our _app.js file, if you're working in react this would go inside of the index.js file instead. Keep in mind, you would have to create your own context if you were using react because next-themes is only available to next.js.

// _app.tsx
 
import '@/styles/globals.css'
import type { AppProps } from 'next/app'
import { ThemeProvider } from 'next-themes'
import { SessionProvider } from 'next-auth/react'
 
export default function App({  Component,
  pageProps: { session, ...pageProps } }: AppProps) {
  return (
    <SessionProvider session={session}>
      <ThemeProvider attribute='class'>
        <Component {...pageProps} />
      </ThemeProvider>
    </SessionProvider>  
  )
}

Now that we have provided context to our application, the last thing we need to do is create a button which will use the context that we have provided. Lets begin by importing a few built- in react hooks along with our custom useContext hook provided by next-themes.

// components/Themes.tsx
import { useState, useEffect } from 'react'
import { useTheme } from 'next-themes'
 
const ThemeSwitch = () => {
  const [mounted, setMounted] = useState(false)
  const { theme, setTheme } = useTheme()
 
    useEffect(() => {
    setMounted(true)
  }, [])
 
  if (!mounted) {
    return null
  }

Now that is set up we can build out our button. We will provide two images depending on the state of the theme. if dark mode is true than we will see a sun icon, and if light mode is true than we will see a moon icon.

// components/Themes.tsx
return (
  <>
    <button
      type="button"
      className="w-9 h-9 bg-indigo-400 rounded-lg border border-solid border-slate-600 dark:bg-indigo-600 flex items-center justify-center  hover:ring-2 ring-gray-300  transition-all"
      onClick={() =>
      setTheme(theme === 'dark' ? 'light' : 'dark')
      }>
        {mounted && (<svg
          xmlns="http://www.w3.org/2000/svg"
          viewBox="0 0 24 24"
          fill="none"
          stroke="currentColor"
          className="w-5 h-5 text-gray-800 dark:text-gray-200"
        >
          {theme === 'dark' ? 
          ( <path
              strokeLinecap="round"
              strokeLinejoin="round"
              strokeWidth={2}
              d="M12 3v1m0 16v1m9-9h-1M4 12H3m15.364 6.364l-.707-.707M6.343 6.343l-.707-.707m12.728 0l-.707.707M6.343 17.657l-.707.707M16 12a4 4 0 11-8 0 4 4 0 018 0z" 
              />) : 
              ( <path
                  strokeLinecap="round"
                  strokeLinejoin="round"
                  strokeWidth={2}
                  d="M20.354 15.354A9 9 0 018.646 3.646 9.003 9.003 0 0012 21a9.003 9.003 0 008.354-5.646z"
                />
          )}
        </svg>)}
    </button>
  </>  
  )
}
export default ThemeSwitch

That's it! Now that our application has a way to use and set our theme, we can tell tailwind how to render our pages based on our current theme. for example:

<main className='bg-white dark:bg-black'>
<h1 className='text-black dark:text-white'>Hello World!</h1>
</main>