Skip to content
Go back

Small UX Fixes That Make Your Laravel App Feel More Polished

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

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:

ApproachHow it worksTradeoff
Always show, disable when not relevantUser dropdown visible but greyed out on Team/LeaderboardSlightly more cluttered, but zero layout shift
Invisible placeholderDropdown hidden with invisible but still takes up spaceNo shift, but a visible gap looks odd
Separate rowUser picker on its own row below the other filtersNo shift, but takes more vertical space
Left-align all filtersAnchor everything to the left so the shift is less noticeableStill 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:

BeforeAfter
border-gray-200border-zinc-200
border-gray-300border-zinc-300
divide-gray-200divide-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.

BeforeAfterWhere
bg-blue-600bg-zinc-900Stepper, buttons
border-blue-600border-zinc-900Stepper active step
hover:bg-blue-900hover:bg-zinc-700Stepper hover
text-blue-600text-zinc-800CSS mentions, links
text-blue-500text-zinc-500Movement 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:

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.


Back to top ↑