There’s a gap between an app that works and an app that feels good to use. It’s usually not one big thing — it’s a dozen small things. A dropdown that doesn’t jump around. A link that tells you it opens somewhere else. Consistent spacing.
None of these take long to fix, but together they’re the difference between “this feels like a side project” and “this feels like a real product.” Here’s a collection of small UX improvements I’ve made to my Laravel app, with the exact code changes.
Table of contents
Open Table of contents
- Indicate external links in your navigation
- Prevent layout shift with conditional UI elements
- Use the same border colour scale as your UI framework
- Standardise page layout and heading alignment
- Lighten secondary chart elements
- Check your font imports match your font weights
- Use meatball menus instead of kebab menus
- Audit and remove old brand colours
- Standardise your tooltips
- The general principle: show and disable, don’t hide and show
- More UX fixes coming
- Key takeaway
Indicate external links in your navigation
My app has a Help link in the sidebar that opens the documentation site (docs.growthmethod.com) in a new tab. The problem was there was no visual indication that clicking it would take you away from the app.
I asked:
“it’s not clear when clicking ‘Help’ in the left-hand side bar that this takes you to an external site and opens a new tab, what are some ways / best practises to indicate that this navigates to an external site?”
The options included adding an icon, a tooltip, subtle text like “(external)”, or different styling. The recommendation was clear — an external link icon is the most universally understood pattern. Apps like GitHub, Notion, and Stripe all use it.
The fix
Since my app uses Flux UI with Heroicons, the change was a single attribute:
<!-- Before: no visual indicator -->
<flux:sidebar.item icon="question-mark-circle"
href="https://docs.growthmethod.com/" target="_blank">
Help
</flux:sidebar.item>
<!-- After: external link icon on the right -->
<flux:sidebar.item icon="question-mark-circle"
icon:trailing="arrow-top-right-on-square"
href="https://docs.growthmethod.com/" target="_blank">
Help
</flux:sidebar.item>
The icon:trailing prop adds a small icon on the right side of the menu item. The arrow-top-right-on-square Heroicon is the standard external link indicator — a small arrow pointing out of a box.
The takeaway: If any navigation item opens a new tab or leaves your app, add a visual indicator. Users shouldn’t have to click to find out they’re leaving.
Prevent layout shift with conditional UI elements
My app has three report types — Individual, Team, and Leaderboard — each with dropdown filters at the top. The Individual report has an extra dropdown for selecting a team member. The problem: that dropdown only appeared when Individual was selected, causing all the other filters to jump sideways when switching between report types.
“the app has 3 reports but they currently have inconsistent dropdown filters… as a result of the additional user/name dropdown on the individual report, the selector fields jump around when navigating between different report types. what are some options/best practises for fixing this?”
The response outlined four approaches:
| Approach | How it works | Tradeoff |
|---|---|---|
| Always show, disable when not relevant | User dropdown visible but greyed out on Team/Leaderboard | Slightly more cluttered, but zero layout shift |
| Invisible placeholder | Dropdown hidden with invisible but still takes up space | No shift, but a visible gap looks odd |
| Separate row | User picker on its own row below the other filters | No shift, but takes more vertical space |
| Left-align all filters | Anchor everything to the left so the shift is less noticeable | Still shifts slightly |
I went with option 1 — always show the dropdown, but disable it when it’s not relevant.
The fix
The change was removing the @if conditional and adding a :disabled attribute instead:
<!-- Before: dropdown only renders for Individual -->
@if ($selectedReport === 'Individual Report')
<flux:select variant="listbox" size="sm"
wire:model.live="selectedTeamMemberId" class="w-48">
@foreach ($teamMembers as $user)
<flux:select.option value="{{ $user['id'] }}">
{{ $user['name'] }}
</flux:select.option>
@endforeach
</flux:select>
@endif
<!-- After: always renders, disabled when not Individual -->
<flux:select variant="listbox" size="sm"
wire:model.live="selectedTeamMemberId" class="w-48"
:disabled="$selectedReport !== 'Individual Report'">
@foreach ($teamMembers as $user)
<flux:select.option value="{{ $user['id'] }}">
{{ $user['name'] }}
</flux:select.option>
@endforeach
</flux:select>
The takeaway: When you have UI elements that appear conditionally, consider whether hiding them causes layout shift. Showing them in a disabled state is often better than hiding them entirely. The user still sees a consistent layout, and the disabled state communicates “this filter exists but doesn’t apply here.”
Use the same border colour scale as your UI framework
If you’re using Flux UI, your components use Tailwind’s zinc colour scale for borders. But if you’ve written custom components or layouts, there’s a good chance you used gray — which is a different, warmer tone.
I asked:
“I’d like to just tidy up a few consistency niggles, starting with borders. I’d like every single user facing screen/view to have the same border styling, please can you investigate”
The audit found border-gray-200 and border-gray-300 scattered across 38+ files, while Flux components used border-zinc-200. Side by side, they’re noticeably different — gray is warmer, zinc is cooler.
The fix
A straightforward find-and-replace across the codebase:
| Before | After |
|---|---|
border-gray-200 | border-zinc-200 |
border-gray-300 | border-zinc-300 |
divide-gray-200 | divide-zinc-200 |
One gotcha: bare border classes with no colour specified default to gray in Tailwind, not zinc. If you’re using Flux UI, always specify the colour explicitly — border border-zinc-200 instead of just border.
The takeaway: Check which colour scale your UI framework uses, and make sure your custom code matches. A subtle colour mismatch across dozens of borders adds up to an app that feels slightly “off” without anyone being able to say why.
Standardise page layout and heading alignment
After fixing borders, I noticed inconsistent padding between pages. Some pages had extra padding overrides, others used the standard layout pattern.
“can you check page margins/paddings as all of these seem consistent… but this page isn’t… what’s the difference here, and which do you think I should go with as the standard?”
The reports page had custom padding overrides (px-0 sm:px-4) and an extra nested container that pushed the content into a different position than every other page.
The fix
Stripped the overrides and used the standard layout pattern:
<!-- Before: custom overrides -->
<x-page-container class="px-0 sm:px-4">
<x-container class="relative">
<div class="p-6 bg-white border-b border-gray-100">
<h3 class="text-xl font-semibold">Team Report</h3>
<!-- After: standard pattern -->
<x-page-container>
<x-container>
<x-heading>
<flux:heading size="xl" level="1">Team Report</flux:heading>
I also found that page headings weren’t vertically aligned with the sidebar logo — they sat slightly too low. The heading component had my-6 (margin top and bottom), which combined with the container’s padding pushed headings down. Changing to mb-6 mt-2 brought them into line.
The takeaway: Pick one standard layout pattern and use it everywhere. If you find yourself adding padding overrides to individual pages, that’s a sign your base pattern needs adjusting — not that specific pages need exceptions.
Lighten secondary chart elements
The forecast dashed line on my dashboard charts was visually competing with the actual data line. Both were similar weights and colours.
The fix
<!-- Before: heavy dash, darker colour -->
stroke-dasharray="6 4" class="text-zinc-400"
<!-- After: lighter dash, softer colour -->
stroke-dasharray="4 4" class="text-zinc-300"
Shorter dashes (4 4 instead of 6 4) and a lighter colour (zinc-300 instead of zinc-400) made the forecast line clearly secondary to the actual data.
The takeaway: Secondary elements like forecasts, targets, and guidelines should be visually lighter than primary data. If everything is the same weight, nothing stands out.
Check your font imports match your font weights
I tried to make a heading bolder with Tailwind’s font-extrabold class. Nothing happened. I tried again. Still nothing. After four frustrated attempts:
“still no change!!! what is happening here”
The root cause: my Google Fonts import only loaded weights 400, 500, and 600. font-extrabold requires weight 800, which wasn’t being loaded. The browser was silently falling back to the closest available weight (600).
<!-- Before: missing weight 800 -->
<link href="...inter:400,500,600" />
<!-- After: includes extrabold -->
<link href="...inter:400,500,600,800" />
The takeaway: If a Tailwind font weight class does nothing, check your font import first. The CSS is correct — the font just isn’t loaded.
Use meatball menus instead of kebab menus
The three-dot icon that opens an action dropdown — on ideas, campaigns, and table rows — was using vertical dots (a “kebab menu”). I switched to horizontal dots (a “meatball menu”).
“what do you call the 3 little dots on this page that triggers the dropdown”
The terminology: vertical dots = “kebab menu”, horizontal dots = “meatball menu”. Meatball is increasingly the standard for action menus in table rows (used by Google, Notion, and most modern SaaS apps).
The fix
<!-- Before: vertical dots (kebab) -->
<flux:button icon="ellipsis-vertical" variant="ghost" />
<!-- After: horizontal dots (meatball) -->
<flux:button icon="ellipsis-horizontal" variant="ghost" />
The takeaway: Small icon choices signal whether your app feels current. Meatball menus (horizontal dots) are the modern default for action dropdowns in tables and cards.
Audit and remove old brand colours
After rebranding, I suspected old colours were still hiding in the codebase. I asked:
“do these colours exist anywhere else in the codebase? look for anything with ‘blue’ in it please as this is an old brand colour”
The audit found bg-blue-600, text-blue-600, border-blue-600, and hover:bg-blue-900 scattered across CSS files, Blade views, stepper components, and even AI chat styling. All were replaced with zinc/black equivalents.
| Before | After | Where |
|---|---|---|
bg-blue-600 | bg-zinc-900 | Stepper, buttons |
border-blue-600 | border-zinc-900 | Stepper active step |
hover:bg-blue-900 | hover:bg-zinc-700 | Stepper hover |
text-blue-600 | text-zinc-800 | CSS mentions, links |
text-blue-500 | text-zinc-500 | Movement indicators |
The takeaway: When you rebrand, do a codebase-wide search for the old colour name. Old brand colours hide in CSS files, component files, hover states, and places you’d never think to check. A single blue in a sea of zinc is immediately noticeable to users.
Standardise your tooltips
When your app has multiple tooltips — score, date, version, avatar — they can easily end up with different styling. Different text colours, padding, separator lines in some but not others, and inconsistent positioning.
After adding date/stage tooltips to my campaign view, I had four different tooltip styles across the same page. The fix was simple but tedious: pick one style (white text on dark background, consistent padding, no separator lines) and apply it everywhere.
The takeaway: When you add a new tooltip or popover, check the existing ones on the same page. Inconsistencies between tooltips are surprisingly visible — users notice when two similar elements look different.
The general principle: show and disable, don’t hide and show
Many of these fixes follow the same pattern. Instead of hiding things and surprising the user, you show everything up front and use visual cues to communicate state:
- An external link icon says “this goes somewhere else” — before you click
- A disabled dropdown says “this filter exists but doesn’t apply right now” — without shifting the layout
- Consistent borders, spacing, and font weights say “someone cared about this” — even if no one notices consciously
These aren’t features. They’re the absence of confusion. And they’re the kind of details that make users trust your app without being able to articulate why.
More UX fixes coming
This is a living article. As I continue building, I’ll add more small UX improvements here. The individual fixes are often too small for their own article, but together they form a useful reference for anyone polishing a Laravel app.
Key takeaway
Polishing your app doesn’t require a redesign. Small fixes — a border colour here, a heading alignment there — add up fast. The pattern is almost always the same: pick a standard, apply it everywhere, and make secondary elements visually lighter than primary ones. Your app will feel more predictable, more professional, and more trustworthy.