Product Design and Debugging
You Don't Need to Be a Designer to Build Great-Looking Products
Let's be honest — when most people start building products, the most painful thing isn't the code. It's "this looks absolutely terrible."
Don't panic. You don't need to become a designer. You need two skills: learn from the best and keep it simple.
"Learn from the best" doesn't mean copy-paste. It means finding well-designed products, studying their layouts, color schemes, and spacing, then using AI to generate similar styles.
"Keep it simple" is even more important. Use at most one or two primary colors per page, one body font, and one consistent button style. Less is more.
Design Principles: Four Is Enough
1. Whitespace
Whitespace isn't wasted space — it lets the page breathe. The most common beginner mistake is cramming everything together.
Practical tips: At least 16px between cards, at least 24px between paragraphs, and generous left/right margins for the main content. Not sure how much? Try doubling it — it usually looks better.
<!-- ❌ Too cramped -->
<div class="p-2">
<h2 class="text-sm mb-1">Title</h2>
<p class="text-xs">Content...</p>
<button class="mt-1 text-xs">Button</button>
</div>
<!-- ✅ Comfortable spacing -->
<div class="p-6">
<h2 class="text-xl mb-4">Title</h2>
<p class="text-base text-gray-600 mb-6">Content...</p>
<button class="px-4 py-2">Button</button>
</div>
2. Alignment
Every element on the page should align along some invisible line. Left-align text, left-align buttons, left-align cards — alignment makes everything clean. Use Tailwind's flex, grid, items-start, and gap-4 to solve alignment issues.
3. Consistency
The same elements should look the same everywhere. Use one primary color site-wide, one consistent button style, and only 3-4 font sizes (title 24px, subtitle 18px, body 16px, small 14px), with consistent border radius like rounded-lg or rounded-md.
4. Color
Color is the easiest thing to mess up. Remember this formula:
- One primary color: For buttons, links, and emphasis — try
blue-600orindigo-600 - Gray palette:
gray-900(titles),gray-600(body text),gray-300(borders) - Status colors: Green (success), yellow (warning), red (error)
- Background: Pure white or
gray-50
Can't pick colors at all? Go to coolors.co and hit the spacebar to randomly generate palettes — pick one that looks good.
Design Standards Quick Reference
This section is a "digital cheat sheet" — open this page and all common design values are at your fingertips. No need to memorize, just look them up when needed.
Typography
Font Size Scale
Use only 4-5 font sizes across your entire product, chosen from this scale:
| px | rem | Tailwind Class | Use |
|---|---|---|---|
| 12px | 0.75rem | text-xs | Captions, copyright, timestamps |
| 14px | 0.875rem | text-sm | Secondary descriptions, table content, labels |
| 16px | 1rem | text-base | Body text — the most important size |
| 18px | 1.125rem | text-lg | Subtitles, article leads |
| 20px | 1.25rem | text-xl | Card titles, section headings |
| 24px | 1.5rem | text-2xl | Page section titles |
| 30px | 1.875rem | text-3xl | Page main titles |
| 36px | 2.25rem | text-4xl | Display text, marketing headlines |
| 48px | 3rem | text-5xl | Hero text on landing pages |
Tip: A page should use at most 4 font sizes. A typical SaaS product uses: body 16px + small 14px + heading 20px + title 24px.
Line Height
Line height determines how "airy" text feels. Three rules:
- Body text:
line-height: 1.6or Tailwind'sleading-relaxed— the most comfortable for CJK text - Titles:
line-height: 1.3orleading-tight(larger text can be tighter) - Small text:
line-height: 1.5orleading-normal— small text shouldn't be too tight either
/* Define it like this in real projects */
body { line-height: 1.6; }
h1, h2, h3 { line-height: 1.3; }
small, .text-sm { line-height: 1.5; }
Font Weight
Three weights are enough for daily development:
- 400 (normal): Body text, descriptions
- 500 (medium): Button text, nav items, emphasis
- 600-700 (semibold/bold): Titles, important numbers
/* ❌ Using font-bold everywhere looks aggressive */
/* ✅ Most text uses font-normal, key elements use font-semibold */
h1 { font-weight: 700; }
h2, h3 { font-weight: 600; }
button { font-weight: 500; }
p { font-weight: 400; }
Chinese Font Stack
No need to download font files — use system defaults:
/* Recommended cross-platform Chinese font stack */
font-family:
-apple-system, /* macOS/iOS PingFang */
BlinkMacSystemFont, /* macOS Chrome */
"Segoe UI", /* Windows */
"PingFang SC", /* macOS Chinese */
"Microsoft YaHei", /* Windows Chinese */
"Hiragino Sans GB", /* macOS legacy Chinese */
"Noto Sans SC", /* Linux/Android Noto Sans */
sans-serif;
/* English-first projects, pair with Inter */
font-family: "Inter", -apple-system, "PingFang SC", "Microsoft YaHei", sans-serif;
Tip: If you use Tailwind CSS, its default
font-sansalready includes a solid font stack. Just use it — no need to tinker.
Color System
60-30-10 Color Rule
A classic principle from interior design that works great for UI:
- 60% — Background: White,
gray-50, orgray-100— covers large areas - 30% — Text/secondary:
gray-900(titles),gray-600(body),gray-300(borders) - 10% — Accent (primary color): Buttons, links, highlighted icons — all interactive elements
Example: A typical SaaS dashboard
├── 60% Background: white / gray-50
├── 30% Text hierarchy: gray-900 titles / gray-600 body / gray-400 secondary
└── 10% Primary: blue-600 for all buttons and links
Choosing a Primary Color
Not sure which color to pick? Choose from these "safe colors":
| Color | Tailwind Value | Best For |
|---|---|---|
| Blue | blue-600 #2563EB | General SaaS, tools, tech |
| Indigo | indigo-600 #4F46E5 | Creative, design, slightly more personality |
| Emerald | emerald-600 #059669 | Finance, health, sustainability |
| Violet | violet-600 #7C3AED | Social, entertainment, AI products |
| Orange | orange-500 #F97316 | E-commerce, food, youthful brands |
Once chosen: Use *-600 for button backgrounds, *-50 for light backgrounds (alert bars), *-700 for hover states.
Gray Palette
Use grays for all text and borders. Avoid pure black (#000) — it's too harsh:
| Name | Tailwind | Hex | Use |
|---|---|---|---|
| gray-50 | bg-gray-50 | #F9FAFB | Page background |
| gray-100 | bg-gray-100 | #F3F4F6 | Card background, input background |
| gray-200 | border-gray-200 | #E5E7EB | Borders, dividers |
| gray-300 | text-gray-300 | #D1D5DB | Placeholder text, disabled states |
| gray-400 | text-gray-400 | #9CA3AF | Helper text |
| gray-500 | text-gray-500 | #6B7280 | Secondary body text |
| gray-600 | text-gray-600 | #4B5563 | Body text |
| gray-700 | text-gray-700 | #374151 | Important text |
| gray-800 | text-gray-800 | #1F2937 | Title text |
| gray-900 | text-gray-900 | #111827 | Most important titles |
Semantic Colors (Status Colors)
These four colors are globally standardized — don't invent your own:
| Status | Color | Tailwind | Use |
|---|---|---|---|
| Success | Green | green-600 #16A34A | Success, online status, positive growth |
| Warning | Amber | amber-500 #F59E0B | Needs attention, expiring soon, low balance |
| Error | Red | red-600 #DC2626 | Failure, form errors, offline |
| Info | Blue | blue-600 #2563EB | Tips, new features, help |
// Form error message standard usage
<p className="text-red-600 text-sm mt-1">Invalid email format</p>
// Success message
<div className="bg-green-50 border border-green-200 text-green-700 rounded-lg p-4">
Saved successfully!
</div>
Dark Mode
To support dark mode, remember these mappings:
Light mode → Dark mode correspondence:
├── bg-white → bg-gray-900 (background)
├── bg-gray-50 → bg-gray-800 (card background)
├── text-gray-900 → text-gray-100 (titles)
├── text-gray-600 → text-gray-400 (body text)
├── border-gray-200→ border-gray-700(borders)
└── Primary color stays the same, but hover uses one shade lighter
Tailwind's dark: prefix works out of the box:
<div class="bg-white dark:bg-gray-900 text-gray-900 dark:text-gray-100">
Auto-adapts to light/dark mode
</div>
Spacing System
4px Grid
All spacing should be a multiple of 4. This is the industry standard, and Tailwind defaults are based on 4px.
Spacing Quick Reference
| Value | Tailwind | Common Use |
|---|---|---|
| 4px | p-1 gap-1 | Space between icon and text |
| 8px | p-2 gap-2 | Compact component padding, small button padding |
| 12px | p-3 gap-3 | Normal component padding |
| 16px | p-4 gap-4 | Most common spacing — card padding, default element spacing |
| 20px | p-5 gap-5 | Large component padding |
| 24px | p-6 gap-6 | Paragraph spacing, section separators |
| 32px | p-8 gap-8 | Large section spacing |
| 40px | p-10 | Page area padding |
| 48px | p-12 | Between sections |
| 64px | p-16 | Top/bottom page whitespace |
| 80px | p-20 | Landing page section spacing |
| 96px | p-24 | Large whitespace, hero areas |
Memory aid: 16px is default, 8px is compact, 24px is relaxed. When in doubt, use 16px.
Layout System
12-Column Grid
The web layout standard is 12 columns because 12 is divisible by 2, 3, 4, and 6 — super flexible for layouts:
12-column grid:
|1|2|3|4|5|6|7|8|9|10|11|12|
Common layouts:
├── 1 column: 12 cols (full-width content)
├── 2 columns: 6+6 (left-right split)
├── 3 columns: 4+4+4 (three cards)
├── 4 columns: 3+3+3+3 (four-column icon grid)
├── Left sidebar: 3+9 (sidebar + main content)
└── Right sidebar: 8+4 (main content + right panel)
Container Widths
/* Max content width for different screens */
sm: 640px /* Phone landscape */
md: 768px /* Tablet */
lg: 1024px /* Small desktop */
xl: 1280px /* Desktop */
2xl: 1536px /* Large screen */
/* Usually this is enough */
.container { max-width: 1200px; margin: 0 auto; padding: 0 16px; }
Breakpoint System
Tailwind default breakpoints — look them up here:
| Name | Width | Device |
|---|---|---|
sm: | ≥ 640px | Phone landscape |
md: | ≥ 768px | Tablet |
lg: | ≥ 1024px | Laptop |
xl: | ≥ 1280px | Desktop monitor |
2xl: | ≥ 1536px | Large monitor |
Mobile-first approach: Write phone styles first, then add desktop styles with md: and lg::
<!-- Single column on phone, double on tablet+ -->
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
<!-- Cards go here -->
</div>
Common Layout Patterns
| Pattern | Use Case | Tailwind Implementation |
|---|---|---|
| Centered single column | Articles, forms | max-w-2xl mx-auto px-4 |
| Left navigation | Admin dashboards | grid grid-cols-[240px_1fr] |
| Card grid | Product listings | grid grid-cols-1 md:grid-cols-3 gap-6 |
| Holy grail layout | Traditional web pages | grid grid-rows-[auto_1fr_auto] min-h-screen |
Component Library Recommendations
Don't build UI components from scratch — using a component library saves 60% of styling time.
shadcn/ui ⭐ Top Pick
What it is: Not a traditional npm package, but a set of component code you copy directly into your project. Built on Radix UI + Tailwind CSS.
Why it's great:
- Code lives in your project — modify anything, full control
- Clean, modern design that looks great out of the box
- Perfect integration with Tailwind CSS
- Active community, rich components (buttons, forms, modals, tables, charts...)
npx shadcn@latest init # Initialize
npx shadcn@latest add button # Add button component
Radix UI
What it is: Unstyled, interactive component primitives that handle all the accessibility heavy lifting.
For whom: People who want fully custom styles but don't want to handle keyboard navigation, focus management, and screen reader compatibility themselves. shadcn/ui uses it under the hood.
npm install @radix-ui/react-dialog @radix-ui/react-dropdown-menu
Headless UI
What it is: Unstyled component library from the Tailwind CSS team — the smoothest integration with Tailwind.
For whom: Heavy Tailwind users who want something more lightweight.
npm install @headlessui/react
Comparison:
| shadcn/ui | Radix UI | Headless UI | |
|---|---|---|---|
| Styles | Has defaults (customizable) | Unstyled | Unstyled |
| Tailwind | Native integration | DIY integration | Native integration |
| Learning curve | Low | Medium | Low |
| Component count | Most | Many | Fewer |
| Best for | Fast shipping | Deep customization | Tailwind projects |
My advice: For new projects, use shadcn/ui — it's hassle-free and the design quality is guaranteed.
Icon System
Don't hunt for random SVGs everywhere. Pick one icon library and use it consistently.
Lucide Icons ⭐ Top Pick
- Icon count: 1500+
- Style: Outline, clean and consistent
- Highlights: Community continuation of Feather Icons, actively maintained
- Install:
npm install lucide-react
import { Home, Settings, User, Search } from 'lucide-react'
<Home size={20} strokeWidth={1.5} />
<Settings className="text-gray-500" />
Heroicons
- Icon count: 300+
- Style: Both outline and solid variants
- Highlights: From the Tailwind team, natural fit for Tailwind projects
- Install:
npm install @heroicons/react
import { HomeIcon } from '@heroicons/react/24/outline'
import { HomeIcon as HomeSolid } from '@heroicons/react/24/solid'
Phosphor Icons
- Icon count: 7000+
- Style: Six weights (thin/light/regular/bold/fill/duotone)
- Highlights: Largest collection, varied styles
- Install:
npm install phosphor-react
import { House, Gear, User } from 'phosphor-react'
<House weight="bold" size={24} />
Comparison:
| Lucide | Heroicons | Phosphor | |
|---|---|---|---|
| Count | 1500+ | 300+ | 7000+ |
| Style | Outline | Outline + Solid | 6 styles |
| Bundle size | Small | Smallest | Medium |
| Best for | General use | Tailwind projects | Need lots of icons |
Practical tip: Choose one at the start and use it everywhere. Mixing icon libraries is one of the most common design mistakes. I recommend Lucide — enough icons, consistent style, small bundle.
Learning from Great Designs
Where to find inspiration:
- Mobbin: Real app screenshots, categorized by type — best for direct reference
- Dribbble: Designer portfolio showcase
- shadcn/ui: Free component library with clean, modern design
How to "borrow": Find a nice screenshot → have AI analyze the design → ask AI to generate a similar page → tweak the details. You can also screenshot your own page and have AI do a design review — it'll point out inconsistent spacing, alignment issues, lack of hierarchy, and more.
Bug Debugging: Four Steps
Step 1: Reproduce. Make sure you can reliably trigger the bug. Document the conditions: what action, what data, what browser.
Step 2: Isolate. Narrow down the problem. Frontend or backend? Use curl to test the API directly and rule out frontend issues.
Step 3: Identify. Read error messages, add console.log, find the exact location of the problem.
Step 4: Fix and Verify. After fixing, go back to the reproduction steps to confirm the bug is gone, then check that you haven't introduced new ones.
console.log: The Most Practical Debugging Tool
80% of bugs can be found with console.log. The key is knowing how to use it.
Basics and Advanced
// Add labels to find what you care about in a sea of output
console.log('[DEBUG] API response:', response.data)
console.log('[DEBUG] user ID:', userId)
// Format objects with JSON.stringify
console.log('Full data:', JSON.stringify(data, null, 2))
// console.table displays arrays beautifully
console.table(users)
// console.time measures duration
console.time('query time')
const data = await fetch('/api/data')
console.timeEnd('query time') // query time: 123.45ms
Debugging Techniques
// Log before and after data changes to find the tipping point
console.log('[BEFORE] state:', state)
setState(newState)
console.log('[AFTER] state:', state)
// Conditional output: only print in specific cases
if (userId === 'problem-user-123') {
console.log('Problem user data:', userData)
}
// Debug React render count
useEffect(() => {
console.log('[RENDER] MyComponent rendered, deps:', dep1, dep2)
})
Reminder: Clean up console.log before going live. Production console.log hurts performance and may leak sensitive info.
Browser DevTools: Your Swiss Army Knife
Chrome DevTools (press F12) — three most useful tabs:
Elements Tab
- Inspect elements: Right-click an element → "Inspect" to jump to its HTML
- Live-edit styles: Change CSS in the right panel — changes apply instantly
- View box model: Selected element shows margin/border/padding/content — spacing issues become obvious
- Force states: Click
:hovto force hover/focus states
Console Tab
// Test APIs directly
fetch('/api/posts').then(r => r.json()).then(console.log)
// Count elements
document.querySelectorAll('button').length
Network Tab
- Click "Fetch/XHR" to see only API requests
- Click a request to view Headers, Payload, Response
- Time bar on the right shows load duration
- "Network throttling" simulates slow networks for testing
Common Next.js Errors and Solutions
Error 1: Hydration Error
Error: Text content does not match server-rendered HTML.
Server and client rendering results don't match. Common with Date.now() or typeof window usage.
// ✅ Render only on client with useEffect
function MyComponent() {
const [time, setTime] = useState('')
useEffect(() => { setTime(new Date().toLocaleString()) }, [])
return <div>{time}</div>
}
Error 2: Missing "use client"
Error: useState only works in Client Components.
Next.js 13+ App Router defaults to Server Components — hooks require "use client".
"use client"
import { useState } from 'react'
export default function Counter() {
const [count, setCount] = useState(0)
return <button onClick={() => setCount(c => c + 1)}>Count: {count}</button>
}
Error 3: Unconfigured Image Domains
Add allowed domains in next.config.js:
const nextConfig = {
images: {
remotePatterns: [
{ protocol: 'https', hostname: '*.supabase.co' },
],
},
}
module.exports = nextConfig
Error 4: useEffect Infinite Loop
useEffect modifies state that's also a dependency, causing infinite triggers.
// ❌ Infinite loop
useEffect(() => {
setItems(data.filter(item => item.active))
}, [items]) // items change → trigger → changes again → trigger again...
// ✅ Depend on original data
useEffect(() => {
setItems(data.filter(item => item.active))
}, [data])
React Error Boundary: Handle Errors Gracefully
By default, a JS error in a React component crashes the entire page. Error Boundaries catch errors and show friendly fallback UI.
"use client"
import React from 'react'
export class ErrorBoundary extends React.Component<
{ children: React.ReactNode; fallback?: React.ReactNode },
{ hasError: boolean }
> {
state = { hasError: false }
static getDerivedStateFromError() {
return { hasError: true }
}
componentDidCatch(error: Error, info: React.ErrorInfo) {
console.error('[ErrorBoundary] Caught error:', error, info)
}
render() {
if (this.state.hasError) {
return this.props.fallback || (
<div className="p-6 text-center">
<h2 className="text-xl font-semibold mb-2">Something went wrong</h2>
<p className="text-gray-600 mb-4">This section failed to load. Try refreshing the page.</p>
<button onClick={() => this.setState({ hasError: false })}
className="px-4 py-2 bg-blue-600 text-white rounded-lg">Retry</button>
</div>
)
}
return this.props.children
}
}
Usage:
<ErrorBoundary>
<ChartComponent /> {/* Errors here don't affect other parts */}
</ErrorBoundary>
Error Boundaries only catch errors during rendering. Errors in event handlers need try-catch.
Writing Good Error Messages
Good error messages tell users three things: what happened, why, and what to do next.
// ❌ "Error 500", "Something went wrong", "Operation failed"
// ✅ "Network connection lost. Check your internet and try again."
// ✅ "This email is already registered. Log in or use a different email."
// ✅ "File exceeds size limit (max 5MB). Compress and try again."
Writing rules: No technical jargon (don't say "null reference," say "data loading failed"), tell them the next step, use a friendly tone without blaming the user.
{error && (
<div className="bg-red-50 border border-red-200 rounded-lg p-4 text-red-700">
<p className="font-medium">Operation failed</p>
<p className="text-sm mt-1">{getErrorMessage(error)}</p>
<button onClick={retry} className="text-sm mt-2 underline">Click to retry</button>
</div>
)}
Logging: Leave Clues for Future You
Logging Best Practices
// ❌ console.log(data) / console.log('here') / console.log('test')
// ✅ With timestamp, label, and context
console.log(`[${new Date().toISOString()}] [API] User ${userId} requested /api/posts`)
console.error(`[${new Date().toISOString()}] [ERROR] Database query failed:`, error.message)
Log Levels
- DEBUG: Development debugging, turn off in production
- INFO: Normal business flow records, like successful login
- WARN: Non-critical but noteworthy, like cache expiration
- ERROR: Something broke, needs investigation
Production Logs
- Vercel: Functions → Logs
- Sentry: Frontend error tracking (
npm install @sentry/nextjs), free tier enough for personal projects
Common Debugging Patterns
"It just worked": Use git diff to see recent changes, or git stash to roll back and compare.
"Works locally but not in production": Check environment variables. Look at Vercel's Environment Variables panel for anything missing.
"Only some users report errors": Edge cases. A user's data is empty, a field wasn't filled, a browser doesn't support an API.
"Fix one, break three": Code is too tightly coupled. Step back and rethink the architecture.
Final Words
Debugging is a skill — the more you practice, the better you get. After each bug fix, take a minute to reflect: What type was it? How did you find it? How could you locate it faster next time?
Design is the same — it doesn't require talent. It requires looking at good designs, remembering four principles, and using AI for reviews. A product built with care will never look ugly.