Skip to content

How to make your web app look more professional without a redesign

Practical CSS and component fixes that make a web app look polished: spacing and type scales, a tighter palette, consistent components, focus styles and real states.

By 7 min read

Most web apps that look "a bit off" are not ugly. They are inconsistent. Three slightly different greys, four button heights, padding that is 14px here and 18px there, a heading that wraps with one lonely word on the second line. None of these is a big problem on its own, but together they read as unfinished. The good news is that you can fix almost all of it without a redesign, a new brand or a new framework. This guide walks through the changes that make the biggest visible difference for the least effort, in the order we would do them.

Start with an audit, not a mood board

Before changing anything, find out what you actually have. Search the codebase for every colour, font size, spacing value, radius and shadow in use. A few shell commands get you most of the way:

Terminal
# Every hex colour, most common first
grep -rhoE '#[0-9a-fA-F]{3,8}\b' src | sort | uniq -c | sort -rn

# Every font-size declaration
grep -rhoE 'font-size:\s*[^;]+' src | sort | uniq -c | sort -rn

# Every padding and margin value
grep -rhoE '(padding|margin)(-[a-z]+)?:\s*[^;]+' src | sort | uniq -c | sort -rn

If you use Tailwind, do the same for arbitrary values such as p-[13px] or text-[#3b82f6]. The output is usually sobering: dozens of near-identical values that nobody chose on purpose. That list is your to-do list. The goal of the rest of this guide is to collapse it into a small set of deliberate choices.

Take full-page screenshots of your five most-used screens at the same time. You will want them later to show the before and after, especially if a client or designer needs to sign off on the changes.

Pick a spacing scale and stick to it

Spacing does more for perceived quality than colour does. When gaps between elements follow a rhythm, the page feels organised even if nobody can say why.

Pick a base unit (4px is common) and a short scale built from it. Then replace every one-off value with the nearest step.

CSS
:root {
  --space-1: 4px;
  --space-2: 8px;
  --space-3: 12px;
  --space-4: 16px;
  --space-6: 24px;
  --space-8: 32px;
  --space-12: 48px;
  --space-16: 64px;
}

Two rules make the scale work in practice:

  • Related things sit closer together than unrelated things. A label and its input might be 4px apart; two form fields 16px; two form sections 32px. If everything is 16px apart, nothing is grouped.
  • Inside a component, padding should be consistent across siblings. Every card has the same inner padding. Every table cell uses the same horizontal padding. Every modal has the same gutter.

Do not agonise over the exact numbers. A consistent scale that is slightly too tight looks better than a perfect value used in one place and ignored everywhere else.

Tighten the typography

Typography is the second big lever. Most apps need far fewer text styles than they have.

Use a small type scale

Five or six sizes covers most product UI: a small size for captions and metadata, a body size, a slightly larger size for emphasis, and two or three heading sizes. Pair each size with a line height and weight so they travel together.

CSS
:root {
  --text-xs: 12px;  --leading-xs: 16px;
  --text-sm: 14px;  --leading-sm: 20px;
  --text-md: 16px;  --leading-md: 24px;
  --text-lg: 20px;  --leading-lg: 28px;
  --text-xl: 24px;  --leading-xl: 32px;
  --text-2xl: 32px; --leading-2xl: 40px;
}

Use weight sparingly. Regular for body, medium or semibold for labels and headings. If you find yourself reaching for bold, a larger size or more space is often the better fix.

Small details that read as polish

A few CSS properties do a lot of quiet work:

CSS
h1, h2, h3 {
  text-wrap: balance;
}

p {
  text-wrap: pretty;
  max-width: 70ch;
}

.table td,
.stat-value {
  font-variant-numeric: tabular-nums;
}

According to MDN, text-wrap: balance balances the number of characters on each line and is intended for short blocks such as headings and captions, while pretty uses a slower algorithm that favours better layout for body copy. font-variant-numeric: tabular-nums makes all digits the same width, so numbers line up in tables and do not jitter when they update. Capping paragraph width keeps long text readable on wide screens.

Reduce the palette

Colour is where inconsistency is most visible. Your audit probably found many greys and several blues. Collapse them.

A workable product palette has:

  • One neutral ramp of roughly eight to ten steps, from page background to strongest text.
  • One accent colour with a couple of shades for hover and pressed states.
  • Status colours for success, warning, danger and information.

Then name colours by what they do, not what they look like. This is the step that keeps the palette from drifting again:

CSS
:root {
  --color-bg: #ffffff;
  --color-surface: #f7f7f8;
  --color-border: #e4e4e7;
  --color-text: #18181b;
  --color-text-muted: #52525b;
  --color-accent: #4f46e5;
  --color-accent-hover: #4338ca;
  --color-danger: #dc2626;
}

Components reference --color-text-muted, never #52525b. When someone wants to tweak the muted grey, they change one line.

Check contrast while you are there

Muted grey text on a light grey surface is one of the most common reasons an app looks washed out, and it is also an accessibility failure. WCAG 2.2 asks for a contrast ratio of at least 4.5:1 for normal text and 3:1 for large text (success criterion 1.4.3), and at least 3:1 for the visual information needed to identify user interface components and their states, such as input borders and focus indicators (1.4.11). Check your muted text and border colours against the surfaces they actually sit on, not just against white.

Make components agree with each other

Once the scales exist, apply them to the components people see on every screen: buttons, inputs, selects, cards and badges.

The most visible fix is height alignment. A button next to an input should be the same height, with the same radius and the same horizontal padding logic. Pick two or three sizes and make every control use them.

CSS
:root {
  --radius-sm: 4px;
  --radius-md: 8px;
  --control-height-sm: 32px;
  --control-height-md: 40px;
}

.button,
.input,
.select {
  height: var(--control-height-md);
  border-radius: var(--radius-md);
  padding-inline: var(--space-4);
  font-size: var(--text-sm);
}

Use one radius for controls and at most one larger radius for containers. Mixed radii (a 4px button inside a 12px card next to a 6px input) are a quiet but strong signal of a codebase built in pieces.

Do the same for shadows. Two or three elevation levels are enough: one for cards, one for dropdowns and popovers, one for modals. If a shadow is not doing a job, remove it and use a border instead.

If you work in React, the next step is to make sure there is exactly one Button and everyone uses it. We cover that in detail in fixing inconsistent buttons and colours across a React codebase.

Do not forget focus and hover

Interaction states are often where polish breaks down. Hover is usually there; focus is often missing or removed with outline: none.

CSS
.button:focus-visible,
.input:focus-visible {
  outline: 2px solid var(--color-accent);
  outline-offset: 2px;
}

MDN explains that :focus-visible applies when the browser decides focus should be made evident, so keyboard users get a clear ring while a mouse click on a button generally does not draw one. WCAG 2.2 also requires that keyboard focus is visible (success criterion 2.4.7). A consistent focus ring in your accent colour looks deliberate, which is exactly the impression you are after.

Design the states, not just the happy path

A screen that looks great with perfect data and falls apart when the list is empty, the request fails or the user lacks permission does not feel professional. Go through your main screens and make sure each one has:

  • Loading: a skeleton or spinner that matches the layout it is replacing, so the page does not jump.
  • Empty: a short explanation of what will appear here and one clear action.
  • Error: what went wrong in plain language, and a way to retry.
  • Denied: why the person cannot see this and who can give them access.

These states are where users spend more time than you think, especially in the first session. We go deeper in designing every state: loading, empty and error.

Make the changes reviewable

If you work with a designer, a client or a product owner, the hard part is often agreement rather than CSS. A few habits help:

  • Show before and after side by side. The screenshots from your audit make the improvement obvious and give people something concrete to comment on.
  • Change the scales first, then the screens. Agreeing on "these are our eight spacing steps and these are our colours" is a short conversation. Arguing about individual paddings on every page is a long one.
  • Write the decisions down. A one-page list of tokens, sizes and component rules stops the same debate happening again in three months.

This is the workflow M1rror is built around: it statically scans a React and TypeScript repository to find your components and design issues, turns your references, code and feedback into a versioned design system that designers, clients and developers review and approve together, and turns the approved design into a scoped pull request for your team to merge.

Wrap-up

Looking professional is mostly about removing accidental variety. Audit what you have, choose a spacing scale, a short type scale and a small, purpose-named palette, then make your components agree on heights, radii and shadows. Add visible focus styles and handle loading, empty, error and denied states. None of this needs a redesign, and each step is visible on its own. If you want to formalise the result, the next step is to turn your existing CSS into a design system.