Skip to main content

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-600 or indigo-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:

pxremTailwind ClassUse
12px0.75remtext-xsCaptions, copyright, timestamps
14px0.875remtext-smSecondary descriptions, table content, labels
16px1remtext-baseBody text — the most important size
18px1.125remtext-lgSubtitles, article leads
20px1.25remtext-xlCard titles, section headings
24px1.5remtext-2xlPage section titles
30px1.875remtext-3xlPage main titles
36px2.25remtext-4xlDisplay text, marketing headlines
48px3remtext-5xlHero 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.6 or Tailwind's leading-relaxed — the most comfortable for CJK text
  • Titles: line-height: 1.3 or leading-tight (larger text can be tighter)
  • Small text: line-height: 1.5 or leading-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-sans already 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, or gray-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":

ColorTailwind ValueBest For
Blueblue-600 #2563EBGeneral SaaS, tools, tech
Indigoindigo-600 #4F46E5Creative, design, slightly more personality
Emeraldemerald-600 #059669Finance, health, sustainability
Violetviolet-600 #7C3AEDSocial, entertainment, AI products
Orangeorange-500 #F97316E-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:

NameTailwindHexUse
gray-50bg-gray-50#F9FAFBPage background
gray-100bg-gray-100#F3F4F6Card background, input background
gray-200border-gray-200#E5E7EBBorders, dividers
gray-300text-gray-300#D1D5DBPlaceholder text, disabled states
gray-400text-gray-400#9CA3AFHelper text
gray-500text-gray-500#6B7280Secondary body text
gray-600text-gray-600#4B5563Body text
gray-700text-gray-700#374151Important text
gray-800text-gray-800#1F2937Title text
gray-900text-gray-900#111827Most important titles

Semantic Colors (Status Colors)

These four colors are globally standardized — don't invent your own:

StatusColorTailwindUse
SuccessGreengreen-600 #16A34ASuccess, online status, positive growth
WarningAmberamber-500 #F59E0BNeeds attention, expiring soon, low balance
ErrorRedred-600 #DC2626Failure, form errors, offline
InfoBlueblue-600 #2563EBTips, 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

ValueTailwindCommon Use
4pxp-1 gap-1Space between icon and text
8pxp-2 gap-2Compact component padding, small button padding
12pxp-3 gap-3Normal component padding
16pxp-4 gap-4Most common spacing — card padding, default element spacing
20pxp-5 gap-5Large component padding
24pxp-6 gap-6Paragraph spacing, section separators
32pxp-8 gap-8Large section spacing
40pxp-10Page area padding
48pxp-12Between sections
64pxp-16Top/bottom page whitespace
80pxp-20Landing page section spacing
96pxp-24Large 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:

NameWidthDevice
sm:≥ 640pxPhone landscape
md:≥ 768pxTablet
lg:≥ 1024pxLaptop
xl:≥ 1280pxDesktop monitor
2xl:≥ 1536pxLarge 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

PatternUse CaseTailwind Implementation
Centered single columnArticles, formsmax-w-2xl mx-auto px-4
Left navigationAdmin dashboardsgrid grid-cols-[240px_1fr]
Card gridProduct listingsgrid grid-cols-1 md:grid-cols-3 gap-6
Holy grail layoutTraditional web pagesgrid 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/uiRadix UIHeadless UI
StylesHas defaults (customizable)UnstyledUnstyled
TailwindNative integrationDIY integrationNative integration
Learning curveLowMediumLow
Component countMostManyFewer
Best forFast shippingDeep customizationTailwind 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:

LucideHeroiconsPhosphor
Count1500+300+7000+
StyleOutlineOutline + Solid6 styles
Bundle sizeSmallSmallestMedium
Best forGeneral useTailwind projectsNeed 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 :hov to 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.