You've seen this app. The layout is clean, the spacing is even, the code behind it is probably fine. And it still looks a little cheap, a little off, in a way that's hard to point at directly. Check the colors before you check anything else. Color is the fastest way to make a well-built interface look unfinished, and it usually comes down to the same handful of reasons.
1. One flat color doing every job
The button's blue. Great. Now what's the hover state? What's it look like pressed? Disabled? If the honest answer is "the same blue, maybe a little darker, I'll figure it out later," that's the tell. A single hex value can't cover resting, hover, active, and disabled at once, so those states either get skipped (a button that gives zero feedback when you click it) or get invented on the spot in devtools, one slightly-off shade at a time. Neither looks intentional, because neither is.
The fix: stop thinking in single colors and start thinking in a small ramp. For every interactive color, decide up front on a resting shade, a hover shade (usually one step darker, or ~8% toward black), an active shade (one step past that), and a disabled treatment (drop the opacity or desaturate, don't just lighten). Store those as named tokens, --btn-bg, --btn-bg-hover, and so on, so every button in the app pulls the same four values instead of each developer eyeballing a new one.
2. Grey text nobody can actually read
Light grey text on a white card is the single most common accessibility failure in the wild, and it's almost never on purpose. It usually starts as a reasonable idea, "make the secondary text less loud than the headline," and ends with body copy at a contrast ratio that fails WCAG AA and is genuinely hard to read in direct sunlight or on a cheap monitor. If you have to squint at your own placeholder text, so does everyone else.
The fix: hold body and secondary text to at least a 4.5:1 contrast ratio against whatever it sits on (3:1 is the floor for large headings, 18px bold or 24px regular and up). Check the actual ratio with any contrast tool rather than trusting your eye. When you want secondary text to feel quieter than the headline, get there by dropping the weight or the size, not by fading the color past the readable line, dark grey at a smaller size reads as "secondary" without becoming unreadable.
3. Grabbing a palette off Dribbble and hoping it fits
A five-color palette that looks great as a static image on a mood board rarely survives contact with a real product. It was chosen for how those five swatches sit next to each other, not for how a button reads against a card, or a card reads against a page background, or an error state reads against a form. A palette like that isn't wrong. It just hasn't been tested outside the one layout it was designed for.
The fix: before you commit, drop the palette into the contexts it'll actually live in. Put the accent on a real button, over a real card, on top of the real page background, and check each pairing for contrast. Make sure you've got the surfaces the swatches never include: a page background, a card background one step off it, a border color, and text colors that pass on both. A five-swatch image is a starting point for those decisions, not the finished system.
4. Using the brand color for everything, including errors
If the brand accent is red and the "something went wrong" banner is also red, every promotional email starts looking like an outage notice. This one's sneaky because each individual use of the color is defensible on its own. Only once you look at the whole product do the accent color and the error color turn out to be indistinguishable, and a genuine warning gets read as just more brand chrome.
The fix: keep brand and status colors in separate lanes. Reserve a dedicated set of semantic colors, success, warning, error, info, that exist only to signal state, and keep them visibly distinct from the brand accent. If your brand color happens to be red or green, that's exactly when you need the status colors to differ in more than hue: shift their saturation and lightness too, so an error still reads as an error next to the accent, not as a louder version of it.
5. Too many "just one more" accent colors
Every accent color starts out as a good idea for one specific spot: a badge here, a highlight there, a chart that needed to stand out. Add enough of them one feature at a time and the interface stops having a hierarchy at all, because everything is competing for the same attention. If you can't say, off the top of your head, what each accent color in your app currently means, that's usually a sign there are too many of them.
The fix: settle on one primary accent for the main action, and at most one secondary accent for genuinely different situations. Everything else, the badges, highlights, and chart series, should pull from that small set plus your neutrals and status colors, not from a new hue each time. When something truly needs to stand apart, try reaching for a different shade of a color you already use before you introduce a brand-new one. A useful gut check: if you removed an accent color entirely, would anyone be able to tell what changed? If not, it wasn't carrying meaning.
6. Never actually testing dark mode
Colors picked and contrast-checked for a light background do not just carry over when the background flips to black. A shade that reads as a subtle border on white can disappear entirely on a dark surface, and text that was comfortably readable on a light card can turn muddy against a dark one. If dark mode exists in your app and was never actually eyeballed by a human, it's very likely quietly broken somewhere.
The fix: treat dark mode as its own set of colors, not an inversion of the light ones. Give each token a separate dark value and re-check contrast against the dark surfaces, the same 4.5:1 line still applies. Two things break most often: pure black backgrounds (use a very dark grey instead, it's easier on the eyes and lets shadows read), and fully saturated accents (they tend to glare on dark, so ease off the saturation a touch). Then actually look at every screen with the theme flipped before you ship it.
A quick color checklist
The same six fixes, boiled down to what to actually do:
- Build color ramps, not single values. Every interactive color needs resting, hover, active, and disabled shades, stored as reusable tokens.
- Hold body text to 4.5:1 contrast. To make secondary text quieter, drop its weight or size rather than fading the color past readable.
- Test a palette in context. Check the accent on a real button, a real card, and the real page background before you commit to it.
- Keep brand and status colors separate. Reserve distinct semantic success, warning, and error colors so a real warning never reads as brand chrome.
- Limit your accents. One primary, at most one secondary; everything else pulls from your neutrals and status colors.
- Give dark mode its own colors. Use separate dark values, re-checked for contrast, not a straight inversion of the light theme.
Fixing this without redoing everything
None of these are big redesigns. They're all versions of the same missing piece: colors that were picked once, as flat individual values, instead of as a small connected system with shades, states, and contrast built in from the start. The Palette Builder generates that system in one pass, states, shades, and accessibility checks included, and exports it straight into CSS, Tailwind, or JSON, or as AI-ready instructions and agent rules you can hand to a tool like Claude or Cursor so it builds with your colors instead of inventing its own. If type, spacing, radius, and shadow need the same treatment, the Design Language Studio handles all of it together.