Skip to content

How to Polish UI Designs Before Shipping

Problem

I shipped a feature last month. All tests passed. Functional requirements met. Code reviewed and approved.

Then a designer sent me a screenshot with three red circles:

  1. Button text wasn’t vertically centered (2px off)
  2. Focus state was missing the outline
  3. The hover transition was jerky

These weren’t bugs. They were quality issues. And they accumulated into a feeling of “this doesn’t feel polished.”

Users notice misalignment, missing states, and inconsistent spacing unconsciously. They can’t articulate what’s wrong, but they feel it. That feeling determines whether they trust the interface.

The Solution

Polish is a systematic pass through 11 dimensions. Not random fixes—a checklist.

I work through each dimension before shipping. No skipping. The goal is catching everything that separates functional from professional.

Dimension 1: Visual Alignment

Pixel-perfect matters. Users notice 1-2px misalignments even if they can’t articulate the problem.

What I Check

Alignment checklist
□ All elements align to grid
□ Consistent spacing scale (4px, 8px, 16px, 24px, 32px)
□ Vertical rhythm in typography
□ Buttons and inputs share height and padding
□ Icons align with text baselines
□ Card edges align across rows

Common Mistakes

I used to center-align everything. Now I realize: consistent left-alignment is more professional than inconsistent centering.

alignment-fix.css
/* WRONG: Inconsistent spacing */
.card {
padding: 15px;
margin: 12px;
gap: 9px;
}
/* CORRECT: Consistent spacing scale */
.card {
padding: 16px; /* 4 × 4 */
margin: 12px; /* 4 × 3 */
gap: 8px; /* 4 × 2 */
}

The difference is subtle but perceptible. I stick to multiples of 4px for everything.

Dimension 2: Typography

Typography is 90% of web design. When it’s wrong, everything feels off.

What I Check

Typography checklist
□ Font hierarchy is consistent (h1 > h2 > h3 > p)
□ Line length doesn't exceed 80 characters
□ No widows (single word on last line)
□ No orphans (single line at top of new page/column)
□ Font loading doesn't cause layout shift
□ Line height is 1.4-1.6 for body text

The Widow Problem

I kept seeing single words dangling at the end of paragraphs. The fix isn’t obvious.

widow-fix.css
/* Prevent widows in headings and short paragraphs */
h1, h2, h3, p {
text-wrap: balance; /* Modern browsers */
}
/* Fallback for older browsers */
p {
/* Add non-breaking space manually between last two words */
}

Wait, text-wrap: balance is new. Let me check browser support. As of 2024, it’s in Chrome 114+, Safari 17.2+, and Firefox preview. I use it, but test for fallbacks.

Font Loading and Layout Shift

I got dinged on Core Web Vitals for CLS (Cumulative Layout Shift). The culprit: font loading.

font-loading.css
/* Preload critical fonts in <head> */
<link rel="preload" href="/fonts/inter-var.woff2" as="font" type="font/woff2" crossorigin>
/* Use font-display: optional to prevent FOIT */
@font-face {
font-family: 'Inter';
src: url('/fonts/inter-var.woff2') format('woff2');
font-display: optional; /* Don't swap if loaded late */
}

font-display: optional means if the font isn’t ready on first paint, use the fallback forever. This prevents layout shift at the cost of font consistency. I make this trade-off for better CLS scores.

Dimension 3: Color and Contrast

Color problems fall into three buckets: accessibility, consistency, and theming.

What I Check

Color checklist
□ All text meets WCAG AA (4.5:1 for normal, 3:1 for large)
□ Interactive elements meet WCAG AA (3:1 against background)
□ Colors use design tokens, not hard-coded values
□ Dark mode colors are tested (not just inverted)
□ Hover/focus states maintain contrast

The Hard-Coded Color Trap

I wrote this during prototyping:

hard-coded-color.css
.button-primary {
background-color: #3b82f6; /* Hard-coded blue */
}
.button-primary:hover {
background-color: #2563eb; /* Hard-coded darker blue */
}

Then I switched to dark mode. The buttons looked fine, but the hover state was invisible against a dark background.

token-based-color.css
.button-primary {
background-color: var(--color-primary-500);
}
.button-primary:hover {
background-color: var(--color-primary-600);
}
/* Dark mode adjustments happen automatically */
@media (prefers-color-scheme: dark) {
:root {
--color-primary-500: oklch(0.70 0.15 250);
--color-primary-600: oklch(0.60 0.15 250); /* Darker, maintains contrast */
}
}

Using design tokens means I define the relationship once. Dark mode adjusts the palette, not individual components.

Dimension 4: Interaction States

Every interactive element needs 8 states. I used to think 4 was enough (default, hover, focus, active). I was wrong.

The 8 Essential States

Interaction states checklist
□ Default (resting state)
□ Hover (subtle feedback on mouse over)
□ Focus (keyboard navigation indicator - NEVER remove)
□ Active (pressed/clicked feedback)
□ Disabled (clearly non-interactive)
□ Loading (async action in progress)
□ Error (validation failed)
□ Success (action completed successfully)

The Focus State Sin

I used to remove focus outlines because they looked “uggy.”

NEVER DO THIS
/* This is an accessibility crime */
button:focus {
outline: none; /* WRONG: Removes keyboard indicator */
}

Users who navigate by keyboard have no idea where they are. This is a WCAG A violation.

proper-focus.css
/* Custom focus that's visible and attractive */
button:focus-visible {
outline: 2px solid var(--color-focus-ring);
outline-offset: 2px;
border-radius: 4px;
}
/* Only show focus ring on keyboard navigation, not click */
button:focus:not(:focus-visible) {
outline: none; /* OK: Only hiding for mouse users */
}

focus-visible is the modern solution. It shows focus ring for keyboard navigation but not mouse clicks.

The Disabled State Problem

I used to just reduce opacity for disabled states.

naive-disabled.css
.button:disabled {
opacity: 0.5; /* Not enough */
}

But 0.5 opacity on a low-contrast color might not meet WCAG requirements. Disabled buttons should be clearly non-interactive, but the text must still be readable.

proper-disabled.css
.button:disabled {
opacity: 0.5;
cursor: not-allowed;
color: var(--color-text-disabled); /* Ensure contrast */
background-color: var(--color-background-disabled);
}

The Loading State

When a user clicks a button that triggers an async action, they need feedback.

button-loading.css
.button-loading {
position: relative;
color: transparent; /* Hide text */
}
.button-loading::after {
content: '';
position: absolute;
width: 16px;
height: 16px;
border: 2px solid var(--color-loading);
border-top-color: transparent;
border-radius: 50%;
animation: spin 0.6s linear infinite;
}
@keyframes spin {
to { transform: rotate(360deg); }
}

But wait—I need to respect prefers-reduced-motion.

reduced-motion.css
@media (prefers-reduced-motion: reduce) {
.button-loading::after {
animation: none;
border: none;
content: ''; /* Simple fallback */
}
}

Dimension 5: Micro-Interactions

Transitions are where good interfaces feel great. But they need to be purposeful.

What I Check

Micro-interactions checklist
□ All transitions are smooth (60fps)
□ Easing functions are appropriate
□ Reduced motion preference is respected
□ Transitions don't block interaction
□ Durations are 150-300ms for UI elements

The Jerky Transition

I wrote this transition:

jerky-transition.css
.modal {
transform: translateY(-20px);
opacity: 0;
transition: all 0.3s ease;
}
.modal.open {
transform: translateY(0);
opacity: 1;
}

But it felt jerky on my laptop. The issue: I was animating transform and opacity together, and the browser was doing extra work.

smooth-transition.css
.modal {
transform: translateY(-20px);
opacity: 0;
transition:
transform 0.3s cubic-bezier(0.4, 0, 0.2, 1),
opacity 0.15s ease;
will-change: transform;
}
.modal.open {
transform: translateY(0);
opacity: 1;
}

Separating the transitions lets the browser optimize. will-change hints that transform will animate.

Reduced Motion

Some users experience motion sickness from animations. I must respect their preference.

respect-reduced-motion.css
@media (prefers-reduced-motion: reduce) {
*,
*::before,
*::after {
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.01ms !important;
}
}

This effectively disables all animations for users who prefer reduced motion.

Dimension 6: Content Consistency

Copy is UI. Inconsistent terminology creates confusion.

What I Check

Content checklist
□ Terminology is consistent (not "Delete" and "Remove" for the same action)
□ Capitalization follows a style (Title Case vs sentence case)
□ Grammar is consistent (Oxford comma, serial comma usage)
□ Tone is appropriate for the audience
□ No lorem ipsum placeholder text

The Capitalization Inconsistency

I used to mix Title Case and sentence case randomly:

inconsistent-caps.text
- Submit Form
- Cancel form
- Delete Account
- Remove user

Now I follow a rule: Title Case for headings and buttons, sentence case for everything else.

consistent-caps.text
- Submit Form
- Cancel Form
- Delete Account
- Remove User

Dimension 7: Icons and Images

Icons and images need consistency in style, sizing, and accessibility.

What I Check

Icons and images checklist
□ Icon style is consistent (outline vs filled)
□ Icon sizes follow a scale (16px, 20px, 24px, 32px)
□ All images have alt text (or empty alt for decorative)
□ SVG icons have proper viewBox and preserveAspectRatio
□ Images are optimized (WebP/AVIF, compressed)

The Alt Text Problem

I used to skip alt text because I was lazy.

missing-alt.html
<img src="/images/avatar.jpg" />

This fails WCAG A. Every image needs alt text.

proper-alt.html
<!-- Informative image -->
<img src="/images/avatar.jpg" alt="User profile photo" />
<!-- Decorative image (empty alt is OK) -->
<img src="/images/decoration.svg" alt="" role="presentation" />
<!-- Complex image -->
<img src="/images/chart.png" alt="Sales increased 40% in Q4" />
<figcaption>Detailed description of chart...</figcaption>

Dimension 8: Forms

Forms are where users interact most. Every form needs labels, validation, and logical tab order.

What I Check

Forms checklist
□ All inputs have visible labels (not just placeholders)
□ Validation messages are clear and actionable
□ Tab order follows visual order
□ Required fields are marked
□ Error messages appear inline, not in alerts
□ Success feedback is provided after submission

The Placeholder Trap

I used placeholders as labels.

placeholder-label.html
<!-- WRONG: Placeholder disappears on focus -->
<input type="email" placeholder="Email address" />

When the user focuses the input, the placeholder disappears. If they forget what the field is for, they have to blur to see it again.

proper-label.html
<!-- CORRECT: Visible label that persists -->
<label for="email">Email address</label>
<input type="email" id="email" placeholder="[email protected]" />

Now the label is always visible.

Dimension 9: Edge Cases

What happens when things go wrong? Every interface needs states for loading, empty, error, and success.

What I Check

Edge cases checklist
□ Loading state for async data
□ Empty state for no data (with helpful action)
□ Error state with recovery options
□ Success state for completed actions
□ Offline/poor connectivity handling

The Empty State Problem

I used to show nothing when a list was empty. Just blank space.

empty-state.tsx
function TodoList({ todos }) {
if (todos.length === 0) {
return null; // WRONG: User sees nothing
}
return (
<ul>
{todos.map(todo => <li key={todo.id}>{todo.text}</li>)}
</ul>
);
}

Now I provide context and action:

helpful-empty-state.tsx
function TodoList({ todos }) {
if (todos.length === 0) {
return (
<div className="empty-state">
<h3>No todos yet</h3>
<p>Create your first todo to get started</p>
<button onClick={onCreateTodo}>Add Todo</button>
</div>
);
}
return (
<ul>
{todos.map(todo => <li key={todo.id}>{todo.text}</li>)}
</ul>
);
}

Dimension 10: Responsiveness

Responsive design isn’t just about fitting content. It’s about usability at every size.

What I Check

Responsiveness checklist
□ All breakpoints are tested (mobile, tablet, desktop)
□ Touch targets are 44x44px minimum
□ No horizontal scroll on any viewport
□ Font sizes are readable on mobile
□ Images don't exceed viewport width
□ Interactive elements aren't too close together

The Touch Target Problem

I had small buttons on mobile:

small-touch-target.css
.icon-button {
width: 32px;
height: 32px;
}

Users with motor impairments couldn’t tap them accurately. WCAG requires 44x44px minimum.

proper-touch-target.css
.icon-button {
width: 32px;
height: 32px;
padding: 6px; /* Total touch target: 44px */
}

Or:

touch-target-expansion.css
.icon-button {
width: 44px;
height: 44px;
display: flex;
align-items: center;
justify-content: center;
}
.icon-button svg {
width: 20px;
height: 20px;
}

Dimension 11: Performance

Performance is a feature. Slow interfaces feel broken.

What I Check

Performance checklist
□ No layout shift on load (CLS < 0.1)
□ Interactions are responsive (FID < 100ms)
□ Images are lazy-loaded below fold
□ Critical CSS is inlined
□ No console errors or warnings
□ Animations run at 60fps

The Layout Shift Problem

I had an image causing CLS:

cls-image.html
<img src="/images/hero.jpg" alt="Hero image" />

When the image loaded, it pushed content down. The fix: reserve space.

reserved-space.html
<div style="aspect-ratio: 16/9;">
<img
src="/images/hero.jpg"
alt="Hero image"
width="1600"
height="900"
loading="lazy"
/>
</div>

aspect-ratio reserves the space. The image fills it when loaded. No shift.

The Complete Checklist

I work through this systematically before every ship:

Polish checklist
□ Visual alignment perfect at all breakpoints
□ Typography hierarchy consistent
□ All text meets contrast requirements (WCAG AA)
□ All 8 interaction states implemented
□ Transitions are smooth (60fps)
□ Reduced motion preference respected
□ Content terminology consistent
□ Icons/images have alt text
□ Forms have labels and validation
□ Edge cases handled (loading, empty, error, success)
□ Touch targets meet 44x44px minimum
□ No horizontal scroll on any viewport
□ CLS < 0.1
□ No console errors or warnings

Common Polish Anti-Patterns

I’ve learned to avoid these:

  1. Polishing before functional completion: Polish is the final pass. Not the middle pass. Get it working, then make it beautiful.

  2. Missing interaction states: Default and hover aren’t enough. I need all 8 states.

  3. Inconsistent spacing scale: Mixing 8px, 10px, 12px, 14px padding looks chaotic. I stick to multiples of 4px.

  4. Ignoring reduced motion: Animations that can’t be disabled are an accessibility issue.

  5. Hard-coded colors: They break theming and make maintenance harder.

  6. Placeholder as label: Users need persistent labels to know what they’re filling out.

  7. No empty states: Blank screens confuse users. Provide context and action.

  8. Tiny touch targets: Mobile users need 44x44px minimum tap areas.

The Polish Mindset

Polish isn’t about perfection. It’s about trust.

When users see misalignment, missing states, or inconsistent spacing, they unconsciously question the quality of everything else. If the UI is sloppy, maybe the code is too. Maybe the security is. Maybe their data isn’t safe.

Polish signals care. It signals professionalism. It signals trust.

I don’t polish everything perfectly. But I systematically work through the dimensions and fix the issues that matter. The result: interfaces that feel solid, reliable, and trustworthy.

Final Words + More Resources

My intention with this article was to help others share my knowledge and experience. If you want to contact me, you can contact by email: Email me

Here are also the most important links from this article along with some further resources that will help you in this scope:

Oh, and if you found these resources useful, don’t forget to support me by starring the repo on GitHub!

Comments