Chapter 7: Polish — States & Onboarding
Your app works, but it doesn't feel finished yet. This chapter makes it feel real.
The difference between a project and a product is polish. Right now, loading feels abrupt, empty screens are blank, errors happen silently, and first-time users have no idea what the app does. We are going to fix all of that -- skeleton loading screens, rich empty states, toast notifications, an onboarding flow, and friendlier timestamps.
What You'll Build
- Skeleton loading cards that mimic entry shapes while data loads
- A rich empty state with a call-to-action when the jar has no entries
- A toast notification system for success and error feedback
- A three-slide onboarding screen for first-time users
- Relative timestamps on entry cards
What You'll Learn
- The three states every screen must handle: loading, empty, and content
- Why skeleton screens feel faster than spinners
- How to build a lightweight toast system without any external library
- Using
localStorageto track whether a user has seen onboarding - CSS animations with Tailwind utility classes
The Three States
Every screen in your app can be in one of three states:
┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│ Loading │ │ Empty │ │ Content │
│ │ │ │ │ │
│ ░░░░░░░░ │ │ │ │ ┌───────┐ │
│ ░░░░░ │ │ Your jar │ │ │ Entry │ │
│ ░░░░░░░░ │ │ is empty │ │ └───────┘ │
│ ░░░░░ │ │ [Create] │ │ ┌───────┐ │
│ │ │ │ │ │ Entry │ │
└─────────────┘ └─────────────┘ └─────────────┘
↓ ↓ ↓
Data loading No data found Data available
Most beginners only build the Content state. A polished app handles all three gracefully. We will tackle them one at a time.
Step 1: Build Skeleton Loading Cards
Skeleton screens show the shape of content before it loads. Research shows users perceive them as faster than spinners because they hint at what is coming rather than just saying "please wait."
$ claude "Create a skeleton loading card component at src/components/EntrySkeleton.tsx tha..."The key idea is that the skeleton card should mirror the exact shape of a real EntryCard -- same border radius, same padding, same section layout -- but filled with pulsing gray bars instead of text. Tailwind's animate-pulse class handles the animation automatically. Three skeleton cards stacked together give the impression of a populated feed that is about to appear.
After Claude Code finishes, check that your Feed component now shows three gently pulsing placeholder cards instead of a loading spinner when data is being fetched.
Step 2: Build a Rich Empty State
When there are no entries, we should do more than show a blank screen. A good empty state explains what the app does and nudges the user toward creating their first entry.
$ claude "Create a rich empty state component at src/components/EmptyState.tsx. Add some c..."The empty state should feel welcoming, not empty. A circular icon area, a friendly heading, a short description encouraging the user to start journaling, and a prominent button that takes them straight to the Create tab. Make sure the button actually navigates -- Claude Code should wire it up so tapping "Create Your First Entry" switches to the create screen.
Step 3: Build a Toast Notification System
Toasts are non-blocking messages that slide in, confirm an action, and auto-dismiss. They are perfect for feedback like "Entry saved!" after creating an entry or "Failed to delete" when something goes wrong. Right now your app does these things silently -- the user has no idea if their action succeeded or failed.
$ claude "Build a toast notification system that shows brief messages like 'Entry saved!' ..."The toast system has three parts. First, a Toast component that renders the visual toast messages with appropriate colors -- green for success, red for errors. Second, a useToast hook that manages the list of active toasts and exposes showToast and dismissToast functions. Third, a CSS animation that makes toasts slide down smoothly from the top of the screen.
Once wired in, wrap your entry creation and deletion handlers with showToast calls so users get immediate visual feedback. A green "Entry saved!" toast sliding down after creating an entry is one of those small touches that makes an app feel professional.
Step 4: Build the Onboarding Screen
First-time users should see a welcome screen that explains what the app does before they are dropped into a login form. This is especially important for an app like a gratitude journal where the concept might not be immediately obvious from the UI alone.
$ claude "Create an onboarding screen at src/pages/Onboarding.tsx with three slides: Welco..."The onboarding has three slides. The first welcomes the user and describes the app. The second explains writing entries and snapping photos. The third introduces the shake-to-discover feature. Each slide has an icon, a title, and a short description.
Navigation works through dot indicators showing which slide you are on, a "Next" button that advances through slides (which becomes "Get Started" on the last slide), and a "Skip" option for impatient users who want to jump straight in.
The critical piece is persistence. When the user taps "Get Started" or "Skip," the onComplete callback fires, which sets localStorage.setItem('gratitude-tree-onboarded', 'true'). On future visits, App.tsx checks this key and skips straight to the auth flow. The onboarding never appears again unless the user clears their browser storage.
Step 5: Add Relative Timestamps
Full dates like "March 15, 2026" feel formal and detached. For recent entries, relative timestamps like "Just now," "5m ago," or "Yesterday" feel much more natural and human.
$ claude "Add a utility function that converts dates to relative timestamps like 'Just now..."The function works by calculating the difference between now and the entry's creation date, then choosing the right label based on thresholds: under a minute is "Just now," under an hour shows minutes, under a day shows hours, one day is "Yesterday," under a week shows days, and anything older falls back to the full formatted date. It is a small change that makes the entire feed feel more alive.
Step 6: Test the Polish
Start the dev server if it is not already running:
npm run dev
Walk through every state you just built:
- Onboarding -- Clear your localStorage (DevTools, then Application, then Local Storage, then Clear All) and refresh the page. The three-slide onboarding should appear. Tap through the slides or use Skip.
- Loading state -- After signing in, you should briefly see three skeleton cards pulse while entries load.
- Empty state -- If you have no entries, you should see the "Your jar is empty" screen with the create button.
- Toast notifications -- Create a new entry. A green "Entry saved!" toast should slide down from the top and disappear after 3 seconds.
- Relative timestamps -- Your new entry should show "Just now" instead of a full date.
- Persistence -- Refresh the page. Onboarding should not appear again.
If the onboarding keeps appearing on every refresh, open DevTools and check Application then Local Storage. The key gratitude-tree-onboarded should be set to true. If it is missing, the onComplete callback is not firing correctly.
Step 7: Commit Your Progress
git add -A
git commit -m "Add polish: skeleton loading, empty states, toasts, onboarding, relative timestamps"
git push
Checkpoint
Your app should now:
- Skeleton loading cards appear while entries load, replacing the old spinner
- Empty state shows "Your jar is empty" with a "Create Your First Entry" button when the feed has no entries
- Toast notifications slide down from the top for success and error feedback, auto-dismissing after 3 seconds
- The onboarding flow shows three slides for first-time users and never appears again after completion
- Relative timestamps show "Just now," "5m ago," "Yesterday" on recent entry cards
- All three states -- loading, empty, and content -- are handled gracefully in the Feed
What's Next
Your app now feels like a real product. Loading is smooth, empty screens are helpful, actions give feedback, and new users get a proper welcome. In the next chapter, we add the final web feature -- a streak counter and calendar heatmap on the Profile page to help users visualize their journaling habit.
Follow @parvsondhi for build threads, tips, and updates on this tutorial.