From Clutter to Clarity: How We Built a Reusable Dropdown System in a Legacy PHP App
The Problem: Action Buttons Everywhere
A few months ago, if you looked at any staff management page in AustinsElite (our legacy PHP app built on a custom framework with Laravel components), you’d see the same pattern: a growing cluster of action buttons—"Edit," "Suspend," "Reset Password," "View Logs"—sprawled across the UI like weeds. Each was hardcoded directly into Blade templates, styled inconsistently, and duplicated across dozens of pages.
This wasn’t just ugly—it was dangerous. Adding a new action meant touching multiple files, increasing the risk of bugs. On mobile? Forget it. The buttons either wrapped awkwardly or triggered horizontal scrolling. And with no shared logic, behaviors like confirmation modals or permission checks were reimplemented (and often forgotten) every time.
We needed a way to consolidate these actions without rewriting the entire frontend.
The Solution: A Reusable dropdown-menu with Alpine.js
Instead of a full rewrite, we opted for surgical extraction. The goal: create a single, reusable dropdown-menu component that could be dropped into any page, accept a list of actions, and handle dynamic interactions gracefully.
We built it as a Blade component—<x-dropdown-menu>—that accepted two props: a trigger label (like "Actions") and an array of action items. Each action could define its label, URL, permission requirement, and confirmation text. Under the hood, we used Alpine.js to manage the open/closed state, leveraging its lightweight reactivity without introducing a full frontend framework.
Here’s a simplified version of how it looked in practice:
<x-dropdown-menu :actions="[
['label' => 'Edit Profile', 'url' => route('staff.edit', $user), 'permission' => 'edit_staff'],
['label' => 'Reset Password', 'url' => '#', 'confirm' => 'Are you sure?', 'permission' => 'reset_password'],
['label' => 'View Audit Log', 'url' => route('logs.staff', $user)]
]" />
The component handled everything: rendering the trigger button, conditionally showing items based on permissions, attaching Alpine-powered dropdown toggles, and wiring up confirmation dialogs using a shared confirmAction() helper—already proven in our recent payment modal refactor.
Crucially, we didn’t try to make it do everything. No icons, no submenus, no async loading—just a focused, predictable pattern that solved the immediate problem.
Impact: Cleaner Code, Clearer UX, Faster Iteration
After rolling out the dropdown-menu component and refactoring the staff-actions block across the app, the benefits were immediate:
- Code duplication dropped by ~70%: We replaced 21 unique action blocks with a single component.
- Mobile layout improved dramatically: Actions were now tucked into a compact dropdown, eliminating horizontal overflow.
- Future extensions became trivial: Adding a new action? Just push to the array. Need to hide it behind a permission? Already built in.
But beyond metrics, the real win was psychological. Developers stopped dreading the "action button shuffle" when adding features. Designers got consistent spacing and behavior. QA reported fewer UI-related edge cases.
This refactor also set a precedent. The success of dropdown-menu—paired with Alpine.js for interactivity—validated our strategy of incrementally modernizing the frontend. It showed we could introduce component thinking without abandoning the legacy stack.
Now, when someone asks, "How do we add a new action?" the answer isn’t "Copy-paste and pray." It’s "Pass it to the dropdown." And that’s progress.