Building a Custom Hook for Theme-Aware Logo Management in React
Recently, I've been recreating the premium portfolio template from Aceternity UI's Minimal Portfolio Template.
The template is modern, simple, and professional. I've recreated the entire website with some additional features like syntax highlighting in blogs and dedicated single pages for projects. And yes, I haven't bought the original template - this is my own implementation inspired by the design.
The Challenge: Professional UI/UX with Theme Support
While building the website, I wanted to ensure a professional UI/UX experience that looks great in both light and dark modes, especially when displaying technology logos.
Initially, I hardcoded the tech stack like this:
export const skills: Skill[] = [
{ name: "JavaScript", logo: "/lang-icons/javascript.svg" },
{ name: "TypeScript", logo: "/lang-icons/typescript.svg" },
{ name: "Node", logo: "/lang-icons/node-logo.svg" },
{ name: "React", logo: "/lang-icons/react.svg" },
{ name: "Next.js", logo: "/lang-icons/nextjs-logo.svg" },
// etc...
];
This approach works fine for a single theme, but problems arise when you need to support both light and dark modes. Some logos that look great on light backgrounds become invisible on dark ones, and vice versa.
The Solution: Logo Mapping Object
To solve this, I created an object that maps each technology name to both light and dark logo variants:
type LogoVariant = {
light: string;
dark: string;
};
export const logoMapper: Record<string, LogoVariant> = {
"Next.js": {
light: "/lang-icons/nextjs-black.svg",
dark: "/lang-icons/next-js-logo-white.webp",
},
React: {
light: "/lang-icons/react.svg",
dark: "/lang-icons/react.svg",
},
MongoDB: {
light: "/lang-icons/mongodb.svg",
dark: "/lang-icons/mongodb.svg",
},
Node: {
light: "/lang-icons/node-logo.svg",
dark: "/lang-icons/node-logo.svg",
},
"Tailwind CSS": {
light: "/lang-icons/tailwind.svg",
dark: "/lang-icons/tailwind.svg",
},
// More technologies...
};
Now I have both variants of each logo, but I still need to handle the theme switching logic.
The Problem: Repetitive Theme Logic
With this setup, I had to use the useTheme
hook and write conditional logic everywhere I needed to display a logo:
"use client";
import React from "react";
import { useTheme } from "next-themes";
import { logoMapper } from "@/lib/logoMapper";
import Image from "next/image";
const SingleProjectPage = ({ params }: { params: Promise<{ slug: string }> }) => {
const { theme } = useTheme();
const resolvedParams = React.use(params);
const project = singleProjects.find((p) => p.slug === resolvedParams.slug);
if (!project) return <div>Project not found</div>;
return (
<div className="px-4 md:px-8">
{/* Project header */}
<div className="flex flex-col items-start gap-3 px-6">
<h1>{project.title}</h1>
<p>{project.shortDescription}</p>
</div>
{/* Technologies section */}
<div className="shadow-section-inset p-4 md:p-6 md:pb-8">
<h2>Technologies Used</h2>
<div className="grid w-full grid-cols-2 gap-6 pt-0 md:grid-cols-4">
{project.technologies.map((tech, idx) => (
<div key={idx} className="group cursor-default">
<div className="h-full p-2 transition-all duration-300">
<div className="flex items-start justify-between">
<div className="flex items-center gap-x-4">
{logoMapper[tech.name] && (
<div className="relative size-5 md:size-7">
<Image
src={
theme === "dark"
? logoMapper[tech.name].dark
: logoMapper[tech.name].light
}
alt={`${tech.name} logo`}
fill
className="object-contain"
/>
</div>
)}
<h3>{tech.name}</h3>
</div>
</div>
</div>
</div>
))}
</div>
</div>
</div>
);
};
export default SingleProjectPage;
The repetitive part was this conditional logic:
<Image
src={
theme === "dark"
? logoMapper[tech.name].dark
: logoMapper[tech.name].light
}
alt={`${tech.name} logo`}
fill
className="object-contain"
/>
This became tedious and error-prone when used across multiple components.
The Solution: Custom useLogo Hook
To eliminate this repetition, I created a custom hook called useLogo
that handles all the theme logic:
"use client";
import { useTheme } from "next-themes";
type LogoVariant = {
light: string;
dark: string;
};
export const logoMapper: Record<string, LogoVariant> = {
"Next.js": {
light: "/lang-icons/nextjs-black.svg",
dark: "/lang-icons/next-js-logo-white.webp",
},
React: {
light: "/lang-icons/react.svg",
dark: "/lang-icons/react.svg",
},
MongoDB: {
light: "/lang-icons/mongodb.svg",
dark: "/lang-icons/mongodb.svg",
},
Node: {
light: "/lang-icons/node-logo.svg",
dark: "/lang-icons/node-logo.svg",
},
"Tailwind CSS": {
light: "/lang-icons/tailwind.svg",
dark: "/lang-icons/tailwind.svg",
},
Neon: {
light: "/lang-icons/neon-dark.svg",
dark: "/lang-icons/neon-light.svg",
},
Cloudinary: {
light: "/lang-icons/cloudinary.png",
dark: "/lang-icons/cloudinary.png",
},
// Add more technologies as needed
};
export const useLogo = () => {
const { theme } = useTheme();
const getLogo = (techName: string): string => {
const logoVariant = logoMapper[techName];
if (!logoVariant) {
console.warn(`Logo not found for technology: ${techName}`);
return "";
}
return theme === "dark" ? logoVariant.dark : logoVariant.light;
};
return { getLogo };
};
Using the Custom Hook
Now, using the hook is much cleaner and more maintainable:
"use client";
import { useLogo } from "@/lib/logoMapper";
import Image from "next/image";
const SingleProjectPage = ({ params }: { params: Promise<{ slug: string }> }) => {
const { getLogo } = useLogo();
return (
<div>
{/* Your component JSX */}
<Image
src={getLogo(tech.name)}
alt={`${tech.name} logo`}
fill
className="object-contain"
/>
</div>
);
};
Benefits of This Approach
1. Clean Code: No more repetitive conditional logic scattered throughout components.
2. Centralized Management: All logo mapping logic lives in one place, making it easy to maintain and update.
3. Error Handling: The hook includes built-in warning messages for missing logos, helping with debugging.
4. Type Safety: Full TypeScript support ensures you catch errors at compile time.
5. Reusability: Use the hook in any component that needs theme-aware logos.
6. Easy Extension: Adding new technologies is as simple as updating the logoMapper
object.
Key Features
- Automatic Theme Detection: The hook automatically detects the current theme and returns the appropriate logo
- Fallback Handling: Returns an empty string and logs a warning for missing logos
- Performance: Minimal overhead with efficient theme detection
- Developer Experience: Simple API with just one function to remember
Conclusion
This custom hook solution transformed my codebase from repetitive, hard-to-maintain conditional logic to a clean, reusable system. It's a perfect example of how custom hooks can solve common React patterns and improve code quality.
The useLogo
hook not only makes the code more readable but also ensures consistency across the entire application. When I need to add a new technology or update logo paths, I only need to modify the logoMapper
object in one place.
Top comments (0)