This document outlines the actual design system, components, and styling patterns implemented in this blog. It serves as both documentation and a reference for maintaining consistency across the site.
Blog Post Features
Banner Images
Blog posts support banner images that appear both in the post header and as OpenGraph meta tags for social media sharing.
TypeScriptindex.tsDIFF// Frontmatter example --- title: "My Blog Post" date: "2025-01-15" description: "A description of the post" banner: "/images/my-banner.jpg" // Optional banner image ---
Features:
- Social Media Ready - Automatically generates OpenGraph and Twitter Card meta tags
- Responsive Design - Banner scales appropriately on different screen sizes
- SEO Optimized - Includes proper alt text and dimensions for search engines
- Optional - Posts work perfectly without banners
Design Philosophy
The design system is built on the principles of minimalism, accessibility, and functionality. Every element serves a purpose, and unnecessary decoration is eliminated in favor of clean, readable content.
Core Principles
→ Simplicity over complexity
Clean, uncluttered interfaces
→ Content first
Design supports content, not the other way around
→ Accessibility
Usable by everyone, regardless of ability
→ Performance
Fast loading and smooth interactions
→ Consistency
Predictable patterns across all pages
Color Palette
The color system uses a carefully curated palette of grays with custom accent colors defined in Tailwind config.
Custom Colors (Tailwind Config)
TypeScriptindex.ts// tailwind.config.ts colors: { dark: "#1E1E24", // Custom dark background light: "#F2F2F2", // Custom light text accent: "#FFBD07", // Custom accent color (yellow) }
Semantic Design Tokens
TypeScriptindex.ts// Tailwind config semantic tokens colors: { dark: "#1E1E24", light: "#F2F2F2", accent: "#FFBD07", surface: { light: "#ffffff", dark: "rgba(0, 0, 0, 0.1)", }, surfaceSecondary: { light: "#f3f4f6", dark: "rgba(0, 0, 0, 0.1)", }, border: { light: "#e5e7eb", dark: "rgba(55, 65, 81, 0.3)", }, }
Usage Patterns
- Primary text:
text-dark dark:text-light - Secondary text:
text-dark/80 dark:text-light/80 - Muted text:
text-dark/60 dark:text-light/60 - Disabled text:
text-dark/40 dark:text-light/40 - Borders:
border-border-light dark:border-border-dark - Surface backgrounds:
bg-surface-light dark:bg-surface-dark - Secondary backgrounds:
bg-surfaceSecondary-light dark:bg-surfaceSecondary-dark - Accent color:
bg-accentortext-accent
Typography
Typography uses Geist fonts with a clear hierarchy and consistent spacing.
Font Stack
TypeScriptindex.ts// layout.tsx import { GeistSans } from "geist/font/sans"; import { GeistMono } from "geist/font/mono"; // Applied to html element className={`${GeistSans.variable} ${GeistMono.variable} scroll-smooth`}
Actual Heading Styles
TypeScriptindex.ts// From MDX components using semantic tokens h1: "mt-8 mb-4 text-xl font-medium text-dark dark:text-light"; h2: "mt-6 mb-3 text-lg font-medium text-dark dark:text-light"; h3: "mt-5 mb-2 text-base font-medium text-dark dark:text-light"; h4: "mt-4 mb-2 text-sm font-medium text-dark dark:text-light"; h5: "mt-4 mb-2 text-sm font-medium text-dark/80 dark:text-light/80"; h6: "mt-4 mb-2 text-xs font-medium text-dark/60 dark:text-light/60";
Body Text Patterns
- Font size:
text-md(16px) for intro,text-basefor body - Line height:
leading-relaxed(1.625) - Color:
text-dark/80 dark:text-light/80(intro),text-dark/60 dark:text-light/60(body) - Spacing:
mb-4between paragraphs
Component Library
Header Component
TypeScriptindex.ts// Actual header implementation <header className="w-[95%] md:w-full max-w-xl z-50 left-0 right-0 mx-auto top-4 md:top-8 p-3 pr-6 flex flex-col gap-y-8 rounded-full border fixed backdrop-blur-md justify-between items-center theme-loaded:transition-colors theme-loaded:duration-300" style={{ backgroundColor: "var(--header-bg)", borderColor: "var(--header-border)", }} > <div className="flex justify-between items-center w-full gap-x-3"> <Link href="/" className="flex items-center gap-x-3"> <Image src="/images/me.jpg" alt="Joe Richardson" width={35} height={35} className="rounded-full hover:scale-105 transition-transform duration-300 ease-in-out" /> <div className="flex flex-col"> <p className="text-dark dark:text-light">{SITE_TITLE}</p> <p className="text-xs opacity-60 text-dark/60 dark:text-light/60"> {SITE_DESCRIPTION} </p> </div> </Link> {/* Navigation */} <nav ref={navRef} className="text-xs font-medium flex items-center gap-x-6 transform relative"> {/* Active indicator */} <span className="h-[1px] absolute bottom-[-16px] theme-loaded:transition-all theme-loaded:duration-300 bg-gradient-to-l from-transparent via-accent/60 dark:via-accent/60 to-transparent" style={{ left: `${spanStyle.left}px`, width: `${spanStyle.width}px`, }} /> {/* Nav links */} <AudioLink className="hover:bg-surfaceSecondary-light dark:hover:bg-surfaceSecondary-dark border border-transparent hover:border-border-light dark:hover:border-border-dark p-2 rounded-lg theme-loaded:transition-all theme-loaded:duration-300 text-dark/80 dark:text-light"> {link.label} </AudioLink> </nav> </div> </header>
Tooltip System
TypeScriptindex.ts// Portal-based tooltip context export const TooltipProvider = ({ children, }: { children: React.ReactNode; }) => { const [tooltip, setTooltip] = useState<{ content: string; x: number; y: number; visible: boolean; }>({ content: "", x: 0, y: 0, visible: false, }); const showTooltip = (content: string, rect: DOMRect) => { const viewportWidth = window.innerWidth; const padding = 8; let x = rect.left + rect.width / 2; const tooltipWidth = 200; if (x - tooltipWidth / 2 < padding) { x = rect.left + tooltipWidth / 2; } else if (x + tooltipWidth / 2 > viewportWidth - padding) { x = rect.right - tooltipWidth / 2; } setTooltip({ content, x, y: rect.top - 30, // Position above the element visible: true, }); }; return ( <TooltipContext.Provider value={{ showTooltip, hideTooltip }}> {children} {tooltip.visible && createPortal( <AnimatePresence> <motion.div key="tooltip" initial={{ opacity: 0, scale: 0.8, y: 10, x: "-50%" }} exit={{ opacity: 0, scale: 0.8, y: 10, x: "-50%" }} transition={{ duration: 0.15, ease: [0.16, 1, 0.3, 1], // Custom easing for a nice pop }} className="fixed z-50 px-3 py-1.5 bg-dark dark:bg-light text-light dark:text-dark text-xs rounded-lg pointer-events-none" style={{ left: tooltip.x, top: tooltip.y, }} animate={{ opacity: 1, scale: 1, y: 0, x: "-50%", }} > {tooltip.content} </motion.div> </AnimatePresence>, document.body )} </TooltipContext.Provider> ); }; // Usage <Tooltip text="Switch to dark mode"> <button>Theme Toggle</button> </Tooltip>;
TypeScriptindex.ts// Custom link component with audio feedback export const AudioLink = ({ children, className, href, onClick }) => { const audioRef = useRef<HTMLAudioElement | null>(null); useEffect(() => { const audio = new Audio("/audio/click.mp3"); audio.preload = "auto"; audioRef.current = audio; }, []); const handleClick = () => { audioRef.current?.play(); onClick?.(); }; return ( <Link href={href} className={className} onClick={handleClick}> {children} </Link> ); };
Theme Toggle Component
TypeScriptindex.ts// Actual theme toggle implementation with tooltip const toggleTheme = () => { const newTheme: Theme = theme === "light" ? "dark" : theme === "dark" ? "system" : "light"; setTheme(newTheme); localStorage.setItem("theme", newTheme); applyTheme(newTheme); }; // Theme toggle with tooltip (uses lucide-react icons) <Tooltip text={ theme === "system" ? "System mode (default)" : theme === "light" ? "Light mode (click for dark)" : "Dark mode (click for auto)" } > <button onClick={toggleTheme} className="p-1.5 rounded-lg hover:bg-surfaceSecondary-light dark:hover:bg-surfaceSecondary-dark border border-transparent hover:border-border-light dark:hover:border-border-dark theme-loaded:transition-all theme-loaded:duration-300" aria-label={`Toggle theme (current: ${theme})`} > {theme === "system" && ( <Monitor className="w-4 h-4 text-dark/80 dark:text-light/80" /> )} {theme === "light" && ( <Sun className="w-4 h-4 text-dark/80" /> )} {theme === "dark" && ( <Moon className="w-4 h-4 text-light/80" /> )} <p className="sr-only">Toggle Theme</p> </button> </Tooltip>;
Post List Component
TypeScriptindex.ts// Actual post list implementation <ul className="flex flex-col relative"> {sortedPosts.map((post, index) => ( <ScrollAnimation key={post.slug} animationType="fade-in" delay={index * 50} > <li className="relative group hover:bg-surfaceSecondary-light dark:hover:bg-surfaceSecondary-dark border border-transparent hover:border-border-light dark:hover:border-border-dark overflow-hidden rounded-full theme-loaded:transition-colors theme-loaded:duration-300"> <Link href={`/posts/${post.slug}`} className="flex items-center justify-between py-3 px-4 gap-x-4" > <h3 className="text-md font-light whitespace-nowrap text-dark dark:text-light"> {truncate(post.meta.title, 40)} </h3> <span className="block w-full h-[1px] bg-border-light dark:bg-border-dark" /> <div className="flex flex-col items-end gap-y-0.5"> <time className="block text-xs font-mono whitespace-nowrap opacity-60 text-dark/60 dark:text-light/60"> {post.formattedDate} </time> </div> </Link> </li> </ScrollAnimation> ))} </ul>
Footer Component
TypeScriptindex.ts// Actual footer implementation (uses CSS variables for theming) <footer className="pb-8"> <div className="flex gap-x-4 justify-between items-center py-4 text-xs text-dark/60 dark:text-light/60 px-6"> <p>London, UK</p> <p>{currentTime}</p> <p className="flex items-center gap-x-2"> {buildWeatherIcon(currentWeather)} {currentWeather} </p> </div> <div className="flex justify-between items-center py-4 text-xs text-dark/60 dark:text-light/60 px-6 rounded-3xl border theme-loaded:transition-colors theme-loaded:duration-300" style={{ backgroundColor: "var(--footer-bg)", borderColor: "var(--footer-border)", }} > <p>© {new Date().getFullYear()}</p> <nav className="flex gap-x-4"> <Link className="hover:text-dark dark:hover:text-light theme-loaded:transition-colors pb-[2px]" target="_blank" href="//bsky.app/profile/joe.city" > Bluesky </Link> {/* More links */} </nav> </div> </footer>
Layout Patterns
Container Widths
TypeScriptindex.ts// Actual container patterns max-w-xl // Header, main content (36rem) max-w-3xl // Blog posts (48rem) max-w-4xl // Wide content (56rem)
Main Layout Structure
TypeScriptindex.ts// Root layout implementation <body className="bg-surface-light dark:bg-dark text-dark dark:text-light theme-loaded:transition-colors theme-loaded:duration-300"> <main className="max-w-xl mx-auto px-6 md:px-0 flex flex-col min-h-screen pt-28 md:pt-32"> <Header /> <PageWrapper>{children}</PageWrapper> <Footer /> </main> </body>
Page Wrapper
TypeScriptindex.ts// Simplified page wrapper (no complex animations) export const PageWrapper = ({ children }: { children: React.ReactNode }) => { return <div className="w-full flex-1">{children}</div>; };
Dark Mode Implementation
Theme System
TypeScriptindex.tstype Theme = "light" | "dark" | "system"; // Theme application const applyTheme = (newTheme: Theme) => { if (newTheme === "system") { const isDarkMode = window.matchMedia( "(prefers-color-scheme: dark)" ).matches; document.documentElement.classList.toggle("dark", isDarkMode); } else { document.documentElement.classList.toggle("dark", newTheme === "dark"); } }; // System preference listener useEffect(() => { if (theme !== "system") return; const mediaQuery = window.matchMedia("(prefers-color-scheme: dark)"); const handleChange = (e: MediaQueryListEvent) => { document.documentElement.classList.toggle("dark", e.matches); }; mediaQuery.addEventListener("change", handleChange); return () => mediaQuery.removeEventListener("change", handleChange); }, [theme]);
Flash Prevention Script
HTMLindex.html<!-- In layout.tsx head - Sets theme immediately and CSS custom properties --> <script dangerouslySetInnerHTML={{ __html: ` (function() { // Prevent flash by setting theme immediately function getTheme() { try { const storedTheme = localStorage.getItem('theme'); if (storedTheme === 'light' || storedTheme === 'dark') { return storedTheme; } } catch (e) { // localStorage might not be available } return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'; } const theme = getTheme(); const isDark = theme === 'dark'; // Set CSS custom properties immediately document.documentElement.style.setProperty('--header-bg', isDark ? 'rgba(0, 0, 0, 0.1)' : '#ffffff'); document.documentElement.style.setProperty('--footer-bg', isDark ? 'rgba(0, 0, 0, 0.1)' : '#f3f4f6'); document.documentElement.style.setProperty('--header-border', isDark ? 'rgba(55, 65, 81, 0.3)' : '#e5e7eb'); document.documentElement.style.setProperty('--footer-border', isDark ? 'rgba(31, 41, 55, 0.3)' : 'transparent'); document.documentElement.classList.toggle('dark', isDark); document.documentElement.classList.add('theme-loaded'); })(); `, }} />
Animation & Transitions
Framer Motion Integration
TypeScriptindex.ts// Page transitions import { AnimatePresence, motion } from "framer-motion"; // Standard transitions transition - colors; // Color transitions transition - all; // All property transitions transition - transform; // Transform transitions duration - 300; // 300ms duration
Hover Effects
TypeScriptindex.ts// Common hover patterns hover:scale-105 // Image scaling hover:bg-surfaceSecondary-light dark:hover:bg-surfaceSecondary-dark // Background hover hover:border-border-light dark:hover:border-border-dark // Border hover hover:text-dark/60 dark:hover:text-light/60 // Text color hover
Code Block System
Enhanced Code Blocks with Search
TypeScriptindex.ts// Advanced code block component with search functionality export const CodeBlock = ({ children, className, highlight, diff, showTerminal, title, files, showMetrics, }) => { const [searchTerm, setSearchTerm] = useState(""); const [showSearch, setShowSearch] = useState(false); const [isCollapsed, setIsCollapsed] = useState(false); const [activeFileIndex, setActiveFileIndex] = useState(0); const [showMetricsPanel, setShowMetricsPanel] = useState(showMetrics); const [isFullscreen, setIsFullscreen] = useState(false); const [showMobileMenu, setShowMobileMenu] = useState(false); const codeRef = useRef<HTMLDivElement>(null); const searchInputRef = useRef<HTMLInputElement>(null); // Keyboard shortcuts useEffect(() => { const handleKeyDown = (e: KeyboardEvent) => { if (e.metaKey || e.ctrlKey) { switch (e.key) { case "f": e.preventDefault(); setShowSearch(true); setTimeout(() => searchInputRef.current?.focus(), 0); break; case "d": e.preventDefault(); handleDownload(); break; } } if (e.key === "Escape") { setShowSearch(false); setSearchTerm(""); } }; document.addEventListener("keydown", handleKeyDown); return () => document.removeEventListener("keydown", handleKeyDown); }, []); // Search highlighting useEffect(() => { if (!searchTerm) return; const codeElement = codeRef.current?.querySelector("pre code"); if (!codeElement) return; const text = codeElement.textContent || ""; const regex = new RegExp(`(${searchTerm})`, "gi"); const highlightedText = text.replace(regex, "<mark>$1</mark>"); // Apply highlighting const walker = document.createTreeWalker( codeElement, NodeFilter.SHOW_TEXT, null ); const textNodes: Text[] = []; let node; while ((node = walker.nextNode())) { textNodes.push(node as Text); } textNodes.forEach((textNode) => { const text = textNode.textContent || ""; if (regex.test(text)) { const highlightedHTML = text.replace(regex, "<mark>$1</mark>"); const wrapper = document.createElement("span"); wrapper.innerHTML = highlightedHTML; textNode.parentNode?.replaceChild(wrapper, textNode); } }); }, [searchTerm]); return ( <div className="group mb-4 overflow-hidden rounded-lg border border-border-light dark:border-border-dark bg-surface-light dark:bg-surface-dark"> {/* Header with controls */} <div className="flex items-center justify-between bg-surfaceSecondary-light dark:bg-surfaceSecondary-dark px-4 py-2 border-b border-border-light dark:border-border-dark"> <div className="flex items-center space-x-2"> {/* Traffic light buttons */} <div className="flex space-x-1"> <div className="w-3 h-3 rounded-full bg-red-500"></div> <div className="w-3 h-3 rounded-full bg-yellow-500"></div> <div className="w-3 h-3 rounded-full bg-green-500"></div> </div> {/* File tabs for multi-file support */} {files && files.length > 1 && ( <div className="flex space-x-1"> {files.map((file, index) => ( <button key={index} onClick={() => setActiveFileIndex(index)} className={`px-2 py-1 text-xs rounded ${ activeFileIndex === index ? "bg-accent/20 text-dark dark:text-light" : "text-dark/60 dark:text-light/60 hover:bg-surfaceSecondary-light dark:hover:bg-surfaceSecondary-dark" }`} > {file.name} </button> ))} </div> )} </div> {/* Control buttons */} <div className="flex items-center space-x-1"> {/* Search - using lucide-react icons */} <button onClick={() => setShowSearch(!showSearch)} className="p-1.5 text-dark/60 dark:text-light/60 hover:text-dark dark:hover:text-light hover:bg-surfaceSecondary-light dark:hover:bg-surfaceSecondary-dark rounded transition-colors" title="Search code (⌘+F)" > <Search className="w-3.5 h-3.5" /> </button> {/* Download */} <button onClick={handleDownload} className="p-1.5 text-dark/60 dark:text-light/60 hover:text-dark dark:hover:text-light hover:bg-surfaceSecondary-light dark:hover:bg-surfaceSecondary-dark rounded transition-colors" title="Download code (⌘+D)" > <Download className="w-3.5 h-3.5" /> </button> {/* Fullscreen */} <button onClick={() => setIsFullscreen(!isFullscreen)} className="p-1.5 text-dark/60 dark:text-light/60 hover:text-dark dark:hover:text-light hover:bg-surfaceSecondary-light dark:hover:bg-surfaceSecondary-dark rounded transition-colors" title="Toggle fullscreen" > <Maximize className="w-3.5 h-3.5" /> </button> {/* Copy */} <button onClick={handleCopy} className="p-1.5 text-dark/60 dark:text-light/60 hover:text-dark dark:hover:text-light hover:bg-surfaceSecondary-light dark:hover:bg-surfaceSecondary-dark rounded transition-colors" title="Copy code" > {copied ? ( <Check className="w-3.5 h-3.5" /> ) : ( <Copy className="w-3.5 h-3.5" /> )} </button> </div> </div> {/* Search bar */} {showSearch && ( <div className="px-4 py-2 bg-surfaceSecondary-light dark:bg-surfaceSecondary-dark border-b border-border-light dark:border-border-dark"> <input ref={searchInputRef} type="text" placeholder="Search code..." value={searchTerm} onChange={(e) => setSearchTerm(e.target.value)} className="w-full px-3 py-1 text-sm bg-surface-light dark:bg-surface-dark border border-border-light dark:border-border-dark rounded focus:outline-none focus:ring-2 focus:ring-accent" /> </div> )} {/* Code content */} <div ref={codeRef} className="relative"> <SyntaxHighlighter language={currentLanguage} style={customTheme} customStyle={{ margin: 0, padding: "0.75rem", fontSize: "14px", lineHeight: "1.5", background: "transparent", overflow: "visible", }} showLineNumbers={showLineNumbers} wrapLines={true} wrapLongLines={wordWrap} > {currentContent} </SyntaxHighlighter> </div> </div> ); };
Code Block Usage Examples
Single File Code Block
TypeScriptindex.tsexport const MyComponent = () => { return <div>Hello World</div>; };
Multi-File Code Block
import React from 'react';
export const Header = () => {
return (
<header className="bg-surface-light dark:bg-surface-dark">
<h1>My App</h1>
</header>
);
};Accessibility Features
Screen Reader Support
TypeScriptindex.ts// Theme toggle accessibility <button onClick={toggleTheme} aria-label={`Toggle theme (current: ${theme})`}> {/* Theme indicator */} <p className="sr-only">Toggle Theme</p> </button>
Focus Management
CSSstyles.css/* Focus states */ focus:ring-2:focus { outline: 2px solid transparent; outline-offset: 2px; box-shadow: 0 0 0 2px #3b82f6; }
Performance Optimizations
Image Optimization
TypeScriptindex.ts// Next.js Image component usage <Image src="/images/me.jpg" alt="Joe Richardson" width={35} height={35} className="rounded-full hover:scale-105 transition-transform duration-300 ease-in-out" />
Audio Preloading
TypeScriptindex.ts// Audio feedback optimization useEffect(() => { const audio = new Audio("/audio/click.mp3"); audio.preload = "auto"; audioRef.current = audio; }, []);
Special Features
Live Data Integration
// Live weather display
const getWeather = async () => {
const response = await fetch(
`https://api.openweathermap.org/data/2.5/weather?q=London&appid=${process.env.NEXT_PUBLIC_OPEN_WEATHER_API_KEY}`
);
const data = await response.json();
return data.weather[0].main;
};Implementation Patterns
Class Variance Authority
TypeScriptindex.ts// Used for conditional styling import { cva, cx } from "class-variance-authority"; // Conditional classes className={cx( "hover:bg-surfaceSecondary-light dark:hover:bg-surfaceSecondary-dark border border-transparent hover:border-border-light dark:hover:border-border-dark p-2 rounded-lg transition-all duration-300 text-dark/80 dark:text-light", activeIndex === index && "border-border-light dark:border-border-dark bg-surfaceSecondary-light dark:bg-surfaceSecondary-dark" )}
TypeScript Patterns
TypeScriptindex.ts// Component prop types type Theme = "light" | "dark" | "system"; interface AudioLinkProps { children: React.ReactNode; className?: string; href: string; onClick?: () => void; } export type Post = { slug: string; formattedDate: string; meta: { [key: string]: any }; };
This style guide documents the actual implementation patterns used in this blog. All code examples are taken directly from the codebase and represent real, working components.
