Skip to content

Core Concepts: Polish & UX

Understanding user experience, feedback, and accessibility

Before we add polish to your app, let's understand what makes software feel professional and user-friendly. Polish isn't just "making things pretty" — it's about guiding users, preventing confusion, and handling errors gracefully.

What is User Experience (UX)?

User Experience (UX) is how a person feels when using your app. Good UX means the app is easy to use, provides clear feedback, and handles errors gracefully.

Think of UX like a conversation:

  • Good conversation: Clear questions, attentive listening, helpful responses
  • Bad conversation: Confusing questions, no acknowledgment, abrupt endings

Your app should be a good conversation.

Elements of Good UX

Clarity:

  • Users understand what to do next
  • Buttons have clear labels
  • Forms explain what's expected

Feedback:

  • App confirms actions ("Todo created!")
  • Shows progress during long operations
  • Explains errors in plain English

Forgiveness:

  • Asks before destructive actions ("Delete this todo?")
  • Prevents accidental mistakes
  • Allows undo when possible

Accessibility:

  • Keyboard navigation works
  • Screen readers can understand the content
  • Focus states are visible

What Are Loading States?

Loading states tell users the app is working on their request. Without them, users don't know if:

  • The app is processing
  • They should wait
  • Something went wrong
  • They need to click again

The Problem: No Loading State

User clicks "Create Todo"

[Nothing visible happens]

User waits... confused

User clicks again (creating duplicate!)

Finally: Todo appears

User thinks: "Is this app broken? Should I click again?"

The Solution: Loading State

User clicks "Create Todo"

Button shows spinner, becomes disabled

User sees: "Creating..."

Todo created successfully

Success message: "Todo created!"

User thinks: "Got it! The app is working on it."

Visual Diagram: Loading States Flow

┌─────────────────────────────────────────────────────┐
│ User Action: Click "Save"                           │
└──────────────────┬──────────────────────────────────┘


        ┌──────────────────────┐
        │ IDLE                 │
        │ Button: "Save"       │
        │ Enabled: ✅          │
        └──────────┬───────────┘
                   │ User clicks

        ┌──────────────────────┐
        │ LOADING              │
        │ Button: "Saving..."  │
        │ Spinner: 🔄          │
        │ Enabled: ❌          │
        └──────────┬───────────┘

         ┌─────────┴─────────┐
         │                   │
         ▼                   ▼
    ✅ SUCCESS          ❌ ERROR
         │                   │
         ▼                   ▼
  ┌────────────┐      ┌────────────┐
  │ Show toast │      │ Show error │
  │ "Saved!"   │      │ message    │
  └────────────┘      └────────────┘
         │                   │
         ▼                   ▼
  ┌────────────────────────────┐
  │ Back to IDLE               │
  │ Button: "Save"             │
  │ Enabled: ✅                │
  └────────────────────────────┘

Why Loading States Matter

Prevent duplicate submissions:

  • Disabled buttons can't be clicked twice
  • User knows the first click is being processed

Set expectations:

  • User knows to wait
  • Reduces anxiety about whether app is working

Professional feel:

  • Apps that respond immediately feel polished
  • Users trust apps that provide feedback

What is User Feedback?

User feedback is how your app communicates results to the user. Every action should have a visible result.

Types of Feedback

Success feedback:

  • "Todo created!"
  • "Changes saved"
  • "Item deleted"

Error feedback:

  • "Title is required"
  • "Network error. Please try again."
  • "You don't have permission to do that"

Progress feedback:

  • Spinner while loading
  • Progress bars for uploads
  • "Processing..." messages

The Feedback Loop

┌──────────────────────────────────────────────────────┐
│                    USER                              │
└──────────────────┬───────────────────────────────────┘

                   ▼ (1) Takes action
        ┌──────────────────────┐
        │ APP                  │
        │ Processes action     │
        └──────────┬───────────┘

                   ▼ (2) Shows feedback
        ┌──────────────────────┐
        │ VISUAL FEEDBACK      │
        │ ✅ Success toast     │
        │ ❌ Error message     │
        │ 🔄 Loading spinner   │
        └──────────┬───────────┘

                   ▼ (3) User understands
        ┌──────────────────────┐
        │ USER KNOWS:          │
        │ - What happened      │
        │ - If successful      │
        │ - What to do next    │
        └──────────────────────┘

Without feedback: User is confused, uncertain With feedback: User is informed, confident

What Are Error Messages?

Error messages explain what went wrong and (ideally) how to fix it.

Bad vs Good Error Messages

❌ Bad error messages:

"Error: PERMISSION_DENIED"

Problems:

  • Technical jargon ("PERMISSION_DENIED")
  • No context (what permission?)
  • No guidance (what should I do?)
  • Scary (users panic)

✅ Good error messages:

"You don't have permission to edit this item.
Only the owner can make changes."

Why it's better:

  • Plain English
  • Explains the problem (not the owner)
  • Provides context (only owner can edit)
  • Calm tone

More Examples

BadGood
"Error: 400""Something's wrong with your input. Please check and try again."
"null reference exception""We couldn't load that item. It may have been deleted."
"NETWORK_FAILED""Can't connect to the internet. Please check your connection."
"Invalid input""Title must be between 1 and 200 characters."

Error Message Guidelines

Be specific:

  • Not: "An error occurred"
  • Better: "Title is required"

Be helpful:

  • Not: "Invalid data"
  • Better: "Title must be at least 1 character"

Be human:

  • Not: "OPERATION_FAILED_CODE_500"
  • Better: "Something went wrong. Please try again."

Suggest solutions:

  • Not: "Network error"
  • Better: "Can't connect. Please check your internet connection."

What is Graceful Degradation?

Graceful degradation means your app handles errors without breaking or confusing users.

Example: Deleting a Todo

Ungraceful (breaks):

User clicks "Delete"

Network fails

Error thrown, app crashes

White screen of death

Graceful (handles it):

User clicks "Delete"

Network fails

Error caught

Show friendly message: "Can't delete right now. Check your connection."

Todo stays in list, user can try again

Key principles:

  • Always catch errors
  • Show user-friendly messages
  • Keep app functional
  • Allow retry

What is Defensive Programming?

Defensive programming means writing code that expects things to go wrong and handles them gracefully.

Examples

Non-defensive (fragile):

typescript
function deleteTodo(id: string) {
  await deleteDoc(doc(db, 'todos', id));  // What if this fails?
  router.push('/todos');
}

Defensive (robust):

typescript
function deleteTodo(id: string) {
  try {
    await deleteDoc(doc(db, 'todos', id));
    showToast('Todo deleted!', 'success');
    router.push('/todos');
  } catch (error) {
    console.error('Delete failed:', error);
    showToast('Could not delete todo. Please try again.', 'error');
    // User stays on page, can retry
  }
}

Defensive techniques:

  • Always use try/catch for async operations
  • Validate inputs before processing
  • Provide fallbacks for missing data
  • Show user-friendly errors
  • Log technical details for debugging

What Are Toast Notifications?

Toast notifications are small, temporary messages that appear (usually at the top or bottom of the screen) to provide feedback.

Think of them like toast popping out of a toaster:

  • Appears quickly
  • Shows briefly
  • Disappears automatically
  • Doesn't block the UI

When to Use Toasts

✅ Good uses:

  • Success confirmations ("Todo created!")
  • Quick errors ("Title is required")
  • Info messages ("Changes saved")
  • Brief notifications

❌ Bad uses:

  • Long error messages (use dialog instead)
  • Critical warnings (use modal instead)
  • Permanent info (use static text instead)
  • Important actions requiring confirmation (use dialog)

Toast Example

┌─────────────────────────────────────┐
│ App Content                         │
│                                     │
│ ┌─────────────────────────────────┐ │
│ │ ✅ Todo created successfully!   │ │ ← Toast
│ └─────────────────────────────────┘ │
│                                     │
│ [List of todos...]                  │
│                                     │
└─────────────────────────────────────┘

After 3 seconds: Toast fades out

Properties:

  • Auto-dismiss (3-5 seconds)
  • Non-blocking (app remains usable)
  • Visible but not intrusive
  • Different colors for success/error/info

What is Accessibility (a11y)?

Accessibility (a11y) means making your app usable by everyone, including people with disabilities.

Why Accessibility Matters

Legal: Many countries require accessible websites Ethical: Everyone deserves to use your app Practical: Accessible apps are better for everyone

Basic Accessibility Principles

Keyboard navigation:

  • All interactive elements reachable via Tab key
  • Enter/Space activates buttons
  • Escape closes modals
  • Users can navigate without a mouse

Screen readers:

  • All content has text alternatives
  • Images have alt text
  • Buttons have descriptive labels
  • Form inputs have labels

Visual clarity:

  • High contrast text (dark on light, light on dark)
  • Focus indicators (visible outline on focused element)
  • Large enough text (at least 16px)
  • Don't rely on color alone to convey meaning

Example: Button Accessibility

❌ Inaccessible:

tsx
<div onClick={handleClick}>
  X
</div>

Problems:

  • Not keyboard accessible (div isn't focusable)
  • Screen reader doesn't know it's a button
  • No label (screen reader says "X")
  • No focus indicator

✅ Accessible:

tsx
<button
  onClick={handleClick}
  aria-label="Delete todo"
  className="delete-button"
>
  X
</button>

Why it's better:

  • <button> is keyboard accessible (Tab, Enter)
  • Screen reader announces "Delete todo button"
  • Browser shows focus outline automatically
  • Semantic HTML

What is Progressive Enhancement?

Progressive enhancement means building a solid foundation, then adding improvements on top.

Approach:

  1. Core functionality works
  2. Add nice-to-have features
  3. Add polish and animations

Example: Form submission

Level 1 (basic):

  • Form submits
  • Redirects to list

Level 2 (better):

  • Shows loading spinner
  • Disables button during submit

Level 3 (polished):

  • Success toast notification
  • Smooth transition
  • Error handling with friendly messages

Start simple, improve gradually.

How It All Fits Together

Let's see a complete example of a polished user experience.

Example: Creating a Todo (Polished)

┌─────────────────────────────────────────────────────┐
│ Step 1: Initial State                               │
│ ─────────────────────────────────────────────────   │
│ Form: [Title input], [Description input]            │
│ Button: "Create Todo" (blue, enabled)               │
│ Focus: Clear blue outline on active input           │
└─────────────────────────────────────────────────────┘
                   │ User fills form, clicks submit

┌─────────────────────────────────────────────────────┐
│ Step 2: Client-Side Validation                      │
│ ─────────────────────────────────────────────────   │
│ Check: Is title empty?                              │
│   ✅ No → Continue                                  │
│   ❌ Yes → Show error: "Title is required"         │
│            Stay on form, focus title input          │
└─────────────────────────────────────────────────────┘
                   │ Validation passed

┌─────────────────────────────────────────────────────┐
│ Step 3: Loading State                               │
│ ─────────────────────────────────────────────────   │
│ Button: "Creating..." with spinner 🔄              │
│ Button: Disabled (can't double-click)               │
│ Cursor: Wait cursor                                 │
│ Form inputs: Disabled                               │
└─────────────────────────────────────────────────────┘
                   │ Firestore request

         ┌─────────┴─────────┐
         │                   │
         ▼                   ▼
    ✅ Success          ❌ Error
         │                   │
         ▼                   ▼
┌──────────────────┐  ┌──────────────────┐
│ Step 4a: Success │  │ Step 4b: Error   │
│ ──────────────── │  │ ──────────────── │
│ Toast:           │  │ Toast:           │
│ "✅ Todo         │  │ "❌ Could not    │
│  created!"       │  │  create todo.    │
│ (green, 3s)      │  │  Try again."     │
│                  │  │ (red, 5s)        │
│ Redirect to list │  │                  │
│                  │  │ Stay on form     │
│ Form clears      │  │ Keep input data  │
│                  │  │ Re-enable button │
└──────────────────┘  └──────────────────┘
         │                   │
         ▼                   ▼
┌─────────────────────────────────────────────────────┐
│ Step 5: Next Action                                 │
│ ─────────────────────────────────────────────────   │
│ Success: User sees new todo in list                 │
│ Error: User can fix and retry                       │
│ Either way: User knows what happened                │
└─────────────────────────────────────────────────────┘

What Makes This Polished?

Clear feedback at every step:

  • User knows form is processing (spinner)
  • User knows if it succeeded (toast)
  • User knows what to do next (redirect or stay)

Prevents errors:

  • Client-side validation (instant feedback)
  • Disabled button during submit (no double-click)
  • Server-side validation via security rules

Handles failures gracefully:

  • Error caught (app doesn't crash)
  • Friendly message (not technical jargon)
  • User can retry (stays on form, data preserved)

Accessible:

  • Focus indicators visible
  • Keyboard navigation works
  • Screen reader friendly

Professional:

  • Smooth transitions
  • Consistent styling
  • Attention to detail

Visual Diagram: Complete UX Flow

                   USER ACTION


              ┌─────────────────┐
              │ Form Validation │
              │ (Client-side)   │
              └────────┬─────────┘

            ┌──────────┴──────────┐
            ▼                     ▼
      ✅ Valid               ❌ Invalid
            │                     │
            │                     ▼
            │           ┌──────────────────┐
            │           │ Show error       │
            │           │ Stay on form     │
            │           │ Focus input      │
            │           └──────────────────┘


   ┌─────────────────┐
   │ LOADING STATE   │
   │ • Spinner       │
   │ • Disable UI    │
   │ • Show progress │
   └────────┬─────────┘


   ┌─────────────────┐
   │ API REQUEST     │
   │ (Firestore)     │
   └────────┬─────────┘

     ┌──────┴──────┐
     ▼             ▼
✅ SUCCESS    ❌ ERROR
     │             │
     ▼             ▼
┌─────────┐   ┌─────────┐
│ Success │   │ Error   │
│ Toast   │   │ Toast   │
│ (Green) │   │ (Red)   │
└────┬────┘   └────┬────┘
     │             │
     ▼             ▼
  Redirect      Stay on
  to list       form
     │             │
     └──────┬──────┘

   ┌─────────────────┐
   │ USER INFORMED   │
   │ Knows result    │
   │ Knows next step │
   └─────────────────┘

Why Polish Matters

Polish is the difference between:

Unprofessional:

  • Clicking buttons does nothing (no feedback)
  • Errors show technical messages ("ERR_500")
  • Can double-submit forms
  • No loading indicators
  • App breaks on errors

Professional:

  • Every action has clear feedback
  • Errors are user-friendly
  • Buttons disable during processing
  • Loading states everywhere
  • Errors handled gracefully

Impact:

Without polish:

  • Users confused and frustrated
  • Looks like a student project
  • Users don't trust it
  • Won't share with others

With polish:

  • Users confident and happy
  • Looks professional
  • Users trust it
  • Will recommend to others

First impressions matter. Polish is what users notice first.

What You'll Build

In this slice, you'll add:

  • ✅ Spinner component (CSS-only loading indicator)
  • ✅ Loading states on all async operations
  • ✅ Toast notification system
  • ✅ Success feedback for all actions
  • ✅ User-friendly error messages
  • ✅ Confirmation dialogs before destructive actions
  • ✅ Improved visual design (spacing, hover states, shadows)
  • ✅ Mobile responsiveness
  • ✅ Basic accessibility (keyboard nav, ARIA labels, focus states)

By the end, your app will:

  • Feel professional and polished
  • Provide clear feedback for all actions
  • Handle errors gracefully
  • Work on mobile
  • Be accessible

This is the final slice. After this, your app is ready to deploy!


Next Steps

Now that you understand the concepts, let's start building:

Step 1: Understanding UX →

Built for learning | Open source on GitHub