The portfolio uses a clean layout system with a root Layout component and custom navigation, providing consistent structure across all pages.
Layout Component
The Layout component is the root wrapper for all pages, providing the base structure, navigation, and footer.
Location
Structure
Visual Layout
Implementation
┌─────────────────────────────────────┐
│ Centered Column │
│ (max-w-2xl) │
│ │
│ ┌───────────────────────────────┐ │
│ │ Navbar │ │
│ └───────────────────────────────┘ │
│ │
│ ┌───────────────────────────────┐ │
│ │ │ │
│ │ Main Content │ │
│ │ (Outlet) │ │
│ │ │ │
│ └───────────────────────────────┘ │
│ │
│ ┌───────────────────────────────┐ │
│ │ Footer │ │
│ └───────────────────────────────┘ │
└─────────────────────────────────────┘
import { Outlet } from "react-router-dom" ;
import { Navbar } from "@/custom/nav" ;
export function Layout () {
return (
< div className = "min-h-screen flex flex-col items-center px-6 w-full" >
{ /* Narrow centered column - content flows top to bottom */ }
< div className = "w-full max-w-2xl flex flex-col pt-10 pb-16 min-h-screen" >
< Navbar />
< main className = "w-full mt-2 flex-1" >
< Outlet />
</ main >
< footer className = "mt-auto pt-8 text-left text-[12px] text-[#0B0F1F]/80" >
© {new Date (). getFullYear () } | Phillipa Bennett-Eghan
</ footer >
</ div >
</ div >
);
}
Design Decisions
Centered Layout Max width of 672px (max-w-2xl) for optimal reading
Flexible Height Uses flex-1 on main and mt-auto on footer for sticky footer
Responsive Padding px-6 (24px) horizontal padding for mobile-friendly spacing
Consistent Spacing pt-10 and pb-16 for top/bottom padding throughout
Key Features
Uses <Outlet /> from React Router to render child routes: import { Outlet } from "react-router-dom" ;
< main className = "w-full mt-2 flex-1" >
< Outlet /> { /* Child routes render here */ }
</ main >
This allows the layout to wrap all pages while each page controls its own content.
Footer displays current year automatically: © { new Date (). getFullYear ()} | Phillipa Bennett - Eghan
Navigation Component
The Navbar component provides consistent navigation across all pages with active state highlighting.
Location
Implementation
import { NavLink } from "react-router-dom" ;
const navItems = {
"/" : { name: "home" },
"/projects" : { name: "projects" },
"/blog" : { name: "blog" },
};
export function Navbar () {
return (
< aside className = "-ml-[8px] mb-12 mt-10" >
< div className = "lg:sticky lg:top-20" >
< nav className = "flex flex-row items-center gap-1 px-0" id = "nav" >
{ Object . entries ( navItems ). map (([ path , { name }]) => (
< NavLink
key = { path }
to = { path }
end // important for exact match on "/"
className = { ({ isActive }) =>
`relative px-2 py-1 text-[12px] font-medium transition-all duration-200
${
isActive
? "bg-[#E1E4EA] text-[#0B0F1F]"
: "text-[#0B0F1F] hover:bg-[#E1E4EA]/60"
} `
}
>
{ name }
</ NavLink >
)) }
</ nav >
</ div >
</ aside >
);
}
Features
Active State
Sticky Navigation
Hover Effects
NavLink automatically applies active styles based on current route: className = {({ isActive }) =>
isActive
? "bg-[#E1E4EA] text-[#0B0F1F]" // Active: solid background
: "text-[#0B0F1F] hover:bg-[#E1E4EA]/60" // Inactive: hover effect
}
The end prop ensures exact matching for the home route (”/”), preventing it from being active on all routes.
On large screens, navigation becomes sticky: < div className = "lg:sticky lg:top-20" >
< nav > ... </ nav >
</ div >
Only active on lg breakpoint (1024px+)
Sticks 80px (top-20) from viewport top
Mobile: normal flow navigation
Smooth hover transitions on inactive links: "transition-all duration-200"
"hover:bg-[#E1E4EA]/60" // 60% opacity background on hover
Creates subtle feedback before navigation.
Design Details
Navigation uses the portfolio’s custom color palette:
Text : #0B0F1F (dark navy)
Active Background : #E1E4EA (light gray)
Hover Background : #E1E4EA/60 (60% opacity)
These colors are consistent with the overall design system.
Negative Margin Technique
The negative left margin optically aligns the navigation with page content by compensating for the internal padding of nav items.
Font size: text-[12px] (12px)
Weight: font-medium (500)
Lowercase styling for casual, friendly appearance
Page Wrapper Patterns
Motion Wrapper
All pages use a consistent motion wrapper for page transitions:
import { motion } from "framer-motion" ;
function HomePage () {
return (
< motion.div
className = "w-full"
initial = { { opacity: 0 , y: 50 } }
animate = { { opacity: 1 , y: 0 } }
transition = { { duration: 0.8 , ease: [ 0.25 , 0.1 , 0.25 , 1 ] } }
>
{ /* Page content */ }
</ motion.div >
);
}
Animation Properties :
Initial : Invisible (opacity: 0) and 50px down (y: 50)
Animate : Fully visible (opacity: 1) at normal position (y: 0)
Duration : 800ms with custom cubic-bezier easing
Easing : [0.25, 0.1, 0.25, 1] for smooth, professional feel
SEO Wrapper
Pages use React Helmet for dynamic meta tags:
import { Helmet } from "react-helmet-async" ;
function HomePage () {
return (
<>
< Helmet >
< title > abena | swe </ title >
< meta name = "description" content = "..." />
< meta property = "og:title" content = "..." />
< meta property = "og:description" content = "..." />
< meta property = "og:image" content = "..." />
< meta property = "og:url" content = "..." />
< meta property = "og:type" content = "website" />
</ Helmet >
{ /* Page content */ }
</>
);
}
Each page defines its own meta tags for proper SEO and social media sharing. The react-helmet-async library ensures server-side rendering compatibility.
Real-World Examples
Home Page Structure
// src/pages/home.tsx
import { Helmet } from "react-helmet-async" ;
import { motion } from "framer-motion" ;
function Home () {
return (
<>
< Helmet >
< title > abena | swe </ title >
< meta name = "description" content = "..." />
</ Helmet >
< motion.div
className = "w-full"
initial = { { opacity: 0 , y: 50 } }
animate = { { opacity: 1 , y: 0 } }
transition = { { duration: 0.8 , ease: [ 0.25 , 0.1 , 0.25 , 1 ] } }
>
{ /* Hero banner */ }
< div className = "flex justify-start mb-5 hidden md:flex" >
< div className = "inline-flex items-center gap-3 px-1.5 py-0.5 rounded-[2px]" >
{ /* Status indicator */ }
</ div >
</ div >
{ /* Introduction */ }
< p className = "text-[16px] font-medium text-[#0B0F1F]" >
hi, i'm Abena 👋🏿
</ p >
{ /* Content sections */ }
</ motion.div >
</>
);
}
Projects Page Structure
// src/pages/projects.tsx
import { motion } from "framer-motion" ;
import { Github , LinkSlant } from "pikaicons" ;
import { Helmet } from "react-helmet-async" ;
function Projects () {
const projects = [
// Project data array
];
return (
<>
< Helmet >
< title > abena | projects </ title >
< meta name = "description" content = "things i have built" />
</ Helmet >
< motion.div
className = "w-full"
initial = { { opacity: 0 , y: 50 } }
animate = { { opacity: 1 , y: 0 } }
transition = { { duration: 0.8 , ease: [ 0.25 , 0.1 , 0.25 , 1 ] } }
>
< p className = "text-[16px] font-medium text-[#0B0F1F]" >
featured projects
</ p >
{ /* Projects grid with staggered animation */ }
< div className = "mt-6 space-y-6" >
{ projects . map (( project , index ) => (
< motion.article
key = { project . title }
initial = { { opacity: 0 , y: 20 } }
animate = { { opacity: 1 , y: 0 } }
transition = { {
duration: 0.5 ,
delay: index * 0.1 , // Stagger effect
ease: "easeOut" ,
} }
>
{ /* Project card content */ }
</ motion.article >
)) }
</ div >
</ motion.div >
</>
);
}
Staggered Animation Pattern
The projects page uses a stagger effect where each project card animates in sequence: delay : index * 0.1 // Each card delayed by 100ms
This creates a cascading effect:
Card 0: 0ms delay
Card 1: 100ms delay
Card 2: 200ms delay
etc.
Responsive Design
Breakpoints
The layout uses Tailwind’s default breakpoints:
Default (< 768px)
Full-width content with padding
Normal flow navigation
Simplified layouts
< div className = "px-6" > { /* 24px padding */ }
< div className = "max-w-2xl" > { /* Constrains on larger screens */ }
md: 768px+
Status banner appears: hidden md:flex
More horizontal spacing
Enhanced layouts
< div className = "hidden md:flex" >
{ /* Only visible on md+ screens */ }
</ div >
lg: 1024px+
Sticky navigation: lg:sticky lg:top-20
Full feature set
Optimized spacing
< div className = "lg:sticky lg:top-20" >
< nav > ... </ nav >
</ div >
Mobile Optimization
Touch Targets Navigation items have adequate touch target size (px-2 py-1 + text)
Responsive Text Text sizes use exact pixel values for consistent rendering
Flexible Layout Flexbox ensures content adapts to all screen sizes
Performance Minimal layout shifts thanks to defined spacing
Best Practices
Use this template when creating new pages: import { Helmet } from "react-helmet-async" ;
import { motion } from "framer-motion" ;
function NewPage () {
return (
<>
< Helmet >
< title > Page Title | abena </ title >
< meta name = "description" content = "..." />
{ /* OpenGraph tags */ }
</ Helmet >
< motion.div
className = "w-full"
initial = { { opacity: 0 , y: 50 } }
animate = { { opacity: 1 , y: 0 } }
transition = { { duration: 0.8 , ease: [ 0.25 , 0.1 , 0.25 , 1 ] } }
>
{ /* Your page content */ }
</ motion.div >
</>
);
}
export default NewPage ;
Follow the established spacing scale:
Section gaps: mt-6 or space-y-6
Paragraph spacing: mt-4
Large section breaks: mt-12 or mt-16
Header to content: mt-2
Stick to the defined color palette:
Primary text : text-[#0B0F1F]
Muted text : text-[#0B0F1F]/80
Background accents : bg-[#E1E4EA]
Borders : border-[#0B0F1F]/20
Component Overview Learn about the component architecture
UI Components Explore reusable UI components
Getting Started Set up your development environment