Goal

Build ThemeToggle and Navbar: sticky top bar, section links, mobile menu, and light/dark mode that persists in localStorage.

Create the folder src/components/ if it does not exist yet.

1. ThemeToggle.jsx

import { useEffect, useState } from "react";

function ThemeToggle() {
  const [dark, setDark] = useState(false);

  useEffect(() => {
    const saved = localStorage.getItem("theme");
    const prefersDark = window.matchMedia(
      "(prefers-color-scheme: dark)",
    ).matches;
    const enabled = saved === "dark" || (!saved && prefersDark);
    setDark(enabled);
    document.documentElement.classList.toggle("dark", enabled);
  }, []);

  function toggle() {
    const next = !dark;
    setDark(next);
    document.documentElement.classList.toggle("dark", next);
    localStorage.setItem("theme", next ? "dark" : "light");
  }

  return (
    <button
      type="button"
      onClick={toggle}
      className="rounded-md border border-slate-300 px-3 py-2 text-sm font-medium text-slate-700 transition hover:bg-slate-100 dark:border-slate-600 dark:text-slate-200 dark:hover:bg-slate-800"
      aria-label="Toggle dark mode"
    >
      {dark ? "Light" : "Dark"}
    </button>
  );
}

export default ThemeToggle;

Tailwind chunks explained

Class Meaning
rounded-md Medium border radius
border 1px border
border-slate-300 Light gray border
px-3 / py-2 Padding inside the button
text-sm Smaller label text
font-medium Slightly bold label
text-slate-700 Button text color
transition Animate property changes (hover/dark swap)
hover:bg-slate-100 Soft fill on hover
dark:border-slate-600 Darker border in dark mode
dark:text-slate-200 Lighter text in dark mode
dark:hover:bg-slate-800 Hover fill for dark background

React bits (not Tailwind): toggle dark on <html>, remember the choice in localStorage, fall back to system preference.

2. Navbar.jsx

import { useState } from "react";
import ThemeToggle from "./ThemeToggle";

function Navbar() {
  const [open, setOpen] = useState(false);

  const linkClass =
    "block rounded-md px-3 py-2 text-sm font-medium text-slate-700 hover:bg-slate-100 hover:text-blue-600 dark:text-slate-200 dark:hover:bg-slate-800";

  return (
    <header className="sticky top-0 z-10 border-b border-slate-200 bg-white/90 backdrop-blur dark:border-slate-800 dark:bg-slate-950/90">
      <nav className="mx-auto flex max-w-5xl items-center justify-between px-6 py-4">
        <a
          href="#"
          className="text-lg font-bold text-slate-900 dark:text-slate-100"
        >
          YourName.dev
        </a>

        <button
          type="button"
          className="rounded-md border border-slate-300 px-3 py-2 text-sm text-slate-700 transition hover:bg-slate-100 dark:border-slate-600 dark:text-slate-200 dark:hover:bg-slate-800 md:hidden"
          onClick={() => setOpen((value) => !value)}
          aria-expanded={open}
          aria-controls="mobile-menu"
        >
          Menu
        </button>

        <ul className="hidden items-center gap-6 text-sm font-medium text-slate-600 dark:text-slate-300 md:flex">
          <li>
            <a
              href="#about"
              className="hover:text-blue-600 dark:hover:text-blue-400"
            >
              About
            </a>
          </li>
          <li>
            <a
              href="#skills"
              className="hover:text-blue-600 dark:hover:text-blue-400"
            >
              Skills
            </a>
          </li>
          <li>
            <a
              href="#projects"
              className="hover:text-blue-600 dark:hover:text-blue-400"
            >
              Projects
            </a>
          </li>
          <li>
            <a
              href="#contact"
              className="rounded-md bg-brand-600 px-3 py-2 text-white hover:bg-brand-700 dark:bg-brand-500 dark:hover:bg-brand-600"
            >
              Contact
            </a>
          </li>
        </ul>

        <ThemeToggle />
      </nav>

      {open && (
        <ul
          id="mobile-menu"
          className="space-y-1 border-t border-slate-200 bg-white px-4 py-3 dark:border-slate-700 dark:bg-slate-950 md:hidden"
        >
          <li>
            <a
              href="#about"
              className={linkClass}
              onClick={() => setOpen(false)}
            >
              About
            </a>
          </li>
          <li>
            <a
              href="#skills"
              className={linkClass}
              onClick={() => setOpen(false)}
            >
              Skills
            </a>
          </li>
          <li>
            <a
              href="#projects"
              className={linkClass}
              onClick={() => setOpen(false)}
            >
              Projects
            </a>
          </li>
          <li>
            <a
              href="#contact"
              className={linkClass}
              onClick={() => setOpen(false)}
            >
              Contact
            </a>
          </li>
        </ul>
      )}
    </header>
  );
}

export default Navbar;

Tailwind chunks explained: header bar

Class Meaning
sticky top-0 Stick to the top while scrolling
z-10 Stay above page content
border-b Bottom border only
border-slate-200 Light divider
bg-white/90 White background at 90% opacity
backdrop-blur Blur content behind the translucent bar
dark:border-slate-800 / dark:bg-slate-950/90 Dark-mode pairing

Tailwind chunks explained: flex row

Class Meaning
mx-auto Center the nav container
flex Flexbox row
max-w-5xl Cap content width
items-center Vertically center logo, links, toggle
justify-between Push logo left and controls right
px-6 / py-4 Horizontal and vertical padding
gap-6 Space between desktop links
hiddenmd:flex Hide links on small screens; show as flex from md up
md:hidden Show the Menu button only below md

Tailwind chunks explained: brand Contact button

Class Meaning
bg-brand-600 Brand fill from @theme
text-white Contrast text
hover:bg-brand-700 Darker on hover
dark:bg-brand-500 Slightly brighter brand in dark mode

Tailwind chunks explained: mobile panel

Class Meaning
space-y-1 Vertical gap between stacked links
border-t Top border under the main nav row
block Full-width tap targets for mobile links
hover:bg-slate-100 Soft highlight on press/hover

3. Wire into App.jsx

import Navbar from "./components/Navbar";

function App() {
  return (
    <div className="min-h-screen bg-surface font-sans text-ink antialiased dark:bg-slate-950 dark:text-slate-100">
      <Navbar />

      <main>
        <p className="p-6 text-muted">Navbar and theme toggle are ready.</p>
      </main>
    </div>
  );
}

export default App;

Try it

  • Click Dark / Light: the page and nav should flip colors
  • Refresh: the choice should stick (localStorage)
  • Resize below md: desktop links hide; Menu opens the list

Next: build the Hero section.