Aug 1, 2024

Vercel's Portfolio Template With Next Themes Toggle

Vercel's Portfolio Template With Next Themes Toggle

This template now includes Next-Themes for light and dark modes, along with a convenient toggle button.

  • Jorge Perez Avatar
    Jorge Perez
    7 min read
  • Portfolio Blog Starter With Next Themes Toggle.

    If you're reading this, you’ve probably noticed that I’m using the Vercel template myself. I customized it a bit, adding a light and dark mode toggle for convenience, and now I can share it with you.

    To clarify, I only added a few components and changed the CSS structure; the rest is exactly the same as the original Vercel portfolio template. This template already includes:

    Features:

    • MDX and Markdown support
    • Optimized for SEO (sitemap, robots, JSON-LD schema)
    • RSS Feed
    • Dynamic OG images
    • Syntax highlighting
    • Vercel Speed Insights / Web Analytics
    • Geist font
    • Next-Themes Light & Dark Mode Toggle. (New, added by me.)

    Live Demo:

    https://portfolio-starter-with-toggle.vercel.app/

    Clone, Deploy & GitHub Repository:

    Execute create-next-app with pnpm to bootstrap the example:

    pnpm create next-app --example https://github.com/JPerez00/portfolio-starter-toggle/tree/main your-project-name-here
    

    Then, run Next.js in development mode:

    pnpm dev

    Or just go to the GitHub Repo and star the project 😁

    https://github.com/JPerez00/portfolio-starter-with-toggle
    

    Step-by-step Changes Made:

    Okay so, I'm not an expert, these are just the steps that worked for me, if you want to follow along, this is exactly what I did:

    1. Reinstall Old Dependencies & Install New Packages:

    I was having issues with the alpha version of Tailwind 4, so I reverted back to the latest stable release and I also installed Next-Themes.

    pnpm add tailwindcss@latest postcss@latest autoprefixer@latest next-themes   

    2. Create a 'tailwind.config.js' file at the root of your project and add the following configuration:

    /** @type {import('tailwindcss').Config} */
    module.exports = {
      darkMode: 'class', // Enable class-based dark mode
      content: [
        './app/**/*.{js,ts,jsx,tsx,mdx}',
        './pages/**/*.{js,ts,jsx,tsx,mdx}',
        './components/**/*.{js,ts,jsx,tsx,mdx}',
      ],
      theme: {
        extend: {},
      },
      plugins: [],
    };

    3. Update your 'postcss.config.js' to ensure it includes Tailwind CSS:

    module.exports = {
      plugins: {
        tailwindcss: {},
        autoprefixer: {},
      },
    };

    4. Update your 'global.css' to include the necessary Tailwind directives, this includes imports and the pre & code (bash) colors:

    @tailwind base;
    @tailwind components;
    @tailwind utilities;
    
    ::selection {
      background-color: #47a3f3;
      color: #fefefe;
    }
    
    /* Light mode variables */
    :root {
      --sh-class: #2d5e9d;
      --sh-identifier: #354150;
      --sh-sign: #8996a3;
      --sh-string: #007f7a;
      --sh-keyword: #e02518;
      --sh-comment: #a19595;
      --sh-jsxliterals: #6266d1;
      --sh-property: #e25a1c;
      --sh-entity: #e25a1c;
    }
    
    /* Dark mode variables */
    .dark {
      --sh-class: #4c97f8;
      --sh-identifier: white;
      --sh-keyword: #f47067;
      --sh-string: #0fa295;
    }
    
    html {
      min-width: 360px;
    }
    
    .prose .anchor {
      @apply absolute invisible no-underline;
    
      margin-left: -1em;
      padding-right: 0.5em;
      width: 80%;
      max-width: 700px;
      cursor: pointer;
    }
    
    .anchor:hover {
      @apply visible;
    }
    
    .prose a {
      @apply underline transition-all decoration-neutral-400 dark:decoration-neutral-600 underline-offset-2 decoration-[0.1em];
    }
    
    .prose .anchor:after {
      @apply text-neutral-300 dark:text-neutral-700;
      content: '#';
    }
    
    .prose *:hover > .anchor {
      @apply visible;
    }
    
    .prose pre {
      @apply bg-neutral-50 dark:bg-neutral-900 rounded-lg overflow-x-auto border border-neutral-200 dark:border-neutral-900 py-2 px-3 text-sm;
    }
    
    .prose code {
      @apply px-1 py-0.5 rounded-lg;
    }
    
    .prose pre code {
      @apply p-0;
      border: initial;
      line-height: 1.5;
    }
    
    .prose code span {
      @apply font-medium;
    }
    
    .prose img {
      /* Don't apply styles to next/image */
      @apply m-0;
    }
    
    .prose p {
      @apply my-4 text-neutral-800 dark:text-neutral-200;
    }
    
    .prose h1 {
      @apply text-4xl font-medium tracking-tight mt-6 mb-2;
    }
    
    .prose h2 {
      @apply text-xl font-medium tracking-tight mt-6 mb-2;
    }
    
    .prose h3 {
      @apply text-xl font-medium tracking-tight mt-6 mb-2;
    }
    
    .prose h4 {
      @apply text-lg font-medium tracking-tight mt-6 mb-2;
    }
    
    .prose strong {
      @apply font-medium;
    }
    
    .prose ul {
      @apply list-disc pl-6;
    }
    
    .prose ol {
      @apply list-decimal pl-6;
    }
    
    .prose > :first-child {
      /* Override removing top margin, causing layout shift */
      margin-top: 1.25em !important;
      margin-bottom: 1.25em !important;
    }
    
    pre::-webkit-scrollbar {
      display: none;
    }
    
    pre {
      -ms-overflow-style: none; /* IE and Edge */
      scrollbar-width: none; /* Firefox */
    }
    
    /* Remove Safari input shadow on mobile */
    input[type='text'],
    input[type='email'] {
      -webkit-appearance: none;
      -moz-appearance: none;
      appearance: none;
    }
    
    table {
      display: block;
      max-width: fit-content;
      overflow-x: auto;
      white-space: nowrap;
    }
    
    .title {
      text-wrap: balance;
    }

    5. Create the 'mode-toggle.tsx' component in the "app/components/" directory:

    "use client";
    
    import { useTheme } from "next-themes";
    
    export function ModeToggle() {
      const { setTheme, theme } = useTheme();
    
      const handleToggle = () => {
        console.log('Current theme:', theme);
        setTheme(theme === "light" ? "dark" : "light");
      };
    
      return (
        <button
          onClick={handleToggle}
          className="border rounded-md w-6 h-6 flex items-center justify-center border-current"
        >
          <span className="sr-only">Toggle mode</span>
          {theme !== "dark" ? (
            <svg
              xmlns="http://www.w3.org/2000/svg"
              fill="none"
              viewBox="0 0 24 24"
              strokeWidth={1.5}
              stroke="currentColor"
              className="w-4 h-4"
            >
              <path
                strokeLinecap="round"
                strokeLinejoin="round"
                ="M21.752 15.002A9.718 9.718 0 0118 15.75c-5.385 0-9.75-4.365-9.75-9.75 0-1.33.266-2.597.748-3.752A9.753 9.753 0 003 11.25C3 16.635 7.365 21 12.75 21a9.753 9.753 0 009.002-5.998z"
              />
            </svg>
          ) : (
            <svg
              xmlns="http://www.w3.org/2000/svg"
              fill="none"
              viewBox="0 0 24 24"
              strokeWidth={1.5}
              stroke="currentColor"
              className="w-4 h-4"
            >
              <path
                strokeLinecap="round"
                strokeLinejoin="round"
                ="M12 3v2.25m6.364.386l-1.591 1.591M21 12h-2.25m-.386 6.364l-1.591-1.591M12 18.75V21m-4.773-4.227l-1.591 1.591M5.25 12H3m4.227-4.773L5.636 5.636M15.75 12a3.75 3.75 0 11-7.5 0 3.75 3.75 0 017.5 0z"
              />
            </svg>
          )}
        </button>
      );
    }

    6. Create the 'theme-provider.tsx' component in the "app/components/" directory. This component will wrap your whole application:

    'use client';
    
    import { ThemeProvider as NextThemesProvider } from 'next-themes';
    
    export default function ThemeProvider({ children }: { children: React.ReactNode }) {
      return <NextThemesProvider attribute="class">{children}</NextThemesProvider>;
    }

    7. Update your "app/layout.tsx" file to use the 'ThemeProvider' component and add the 'suppressHydrationWarning':

    import './global.css';
    import type { Metadata } from 'next';
    import { GeistSans } from 'geist/font/sans';
    import { GeistMono } from 'geist/font/mono';
    import { Navbar } from './components/nav';
    import { Analytics } from '@vercel/analytics/react';
    import { SpeedInsights } from '@vercel/speed-insights/next';
    import Footer from './components/footer';
    import { baseUrl } from './sitemap';
    import ThemeProvider from './components/theme-provider';
    
    export const metadata: Metadata = {
      metadataBase: new URL(baseUrl),
      title: {
        default: 'Next.js Portfolio Starter',
        template: '%s | Next.js Portfolio Starter',
      },
      description: 'This is my portfolio.',
      openGraph: {
        title: 'My Portfolio',
        description: 'This is my portfolio.',
        url: baseUrl,
        siteName: 'My Portfolio',
        locale: 'en_US',
        type: 'website',
      },
      robots: {
        index: true,
        follow: true,
        googleBot: {
          index: true,
          follow: true,
          'max-video-preview': -1,
          'max-image-preview': 'large',
          'max-snippet': -1,
        },
      },
    };
    
    const cx = (...classes) => classes.filter(Boolean).join(' ');
    
    export default function RootLayout({
      children,
    }: {
      children: React.ReactNode;
    }) {
      return (
        <html
          lang="en"
          className={cx(GeistSans.variable, GeistMono.variable)}
          suppressHydrationWarning
        >
          <body className="text-black bg-white dark:text-white dark:bg-black antialiased max-w-xl mx-4 mt-8 lg:mx-auto">
            <ThemeProvider>
              <main className="flex-auto min-w-0 mt-6 flex flex-col px-2 md:px-0">
                <Navbar />
                {children}
                <Footer />
              </main>
              <Analytics />
              <SpeedInsights />
            </ThemeProvider>
          </body>
        </html>
      );
    }

    8. Update your 'nav.tsx' file, inside your "app/components/" directory, to include the 'mode-toggle.tsx' component.

    'use client';
    
    import Link from 'next/link';
    import { ModeToggle } from './mode-toggle';
    
    const navItems = {
      '/': {
        name: 'home',
      },
      '/blog': {
        name: 'blog',
      },
      'https://github.com/JPerez00/portfolio-starter-with-toggle': {
        name: 'deploy',
      },
    }
    
    export function Navbar() {
      return (
        <aside className="-ml-[8px] mb-16 tracking-tight">
          <div className="lg:sticky lg:top-20">
            <nav
              className="flex flex-row items-start relative px-0 pb-0 fade md:overflow-auto scroll-pr-6 md:relative"
              id="nav"
            >
              <div className="flex flex-1">
                <div className="transition-all hover:text-neutral-800 dark:hover:text-neutral-200 flex align-middle relative py-1 m-1">
                  <ModeToggle />
                </div>
              </div>
              <div className="flex flex-row space-x-0">
                {Object.entries(navItems).map(([path, { name }]) => {
                  return (
                    <Link
                      key={path}
                      href={path}
                      className="transition-all hover:text-neutral-800 dark:hover:text-neutral-200 flex align-middle relative py-1 pl-2 m-1"
                    >
                      {name}
                    </Link>
                  );
                })}
              </div>
            </nav>
          </div>
        </aside>
      );
    }

    Debugging & Conclusion:

    Basically, what we did here is ensuring the project can manage the theme context and persist the theme in local storage:

    1. We added the Tailwind CSS Dark Mode Configuration.
    2. We created a Theme Context for managing the theme state.
    3. We modified the Nav component to include the toggle button.
    4. Saved the theme state in local storage.
    5. Debugging/Console log added.

    Open the browser console to verify if the console.log('Current theme:', theme) statement prints when you click the button. This confirms the toggle function is being called.

    And that's it! that's all I did to make the Next-themes and the toggle button work. Hope that helps! Please star on GitHub if you found this helpful! cheers!

    https://github.com/JPerez00/portfolio-starter-with-toggle