Back to Blog
4 min read

Building a Reusable, Responsive Data Table in a Legacy PHP App

The Problem: Tables That Broke More Than Data

Fifteen years ago, AustinsElite was built on a custom PHP framework with early Laravel components sprinkled in. Fast forward to 2025, and while the business logic still holds, the frontend was creaking—especially my data tables. They were copy-pasted across views, inconsistently styled, and completely unresponsive. On mobile? Forget it. Horizontal scroll traps, clipped action buttons, and unreadable column widths made admin tasks painful.

I didn’t have the bandwidth for a full rewrite. But with a major refactor wave kicking off in December 2025, I saw an opportunity: build one component that could modernize the experience across dozens of pages—without touching the backend.

My goal was clear: create a reusable, responsive data table using Blade and Tailwind that worked across existing views, supported mobile touch patterns, and preserved backward compatibility. And I needed it yesterday.

Designing a Blade Component That Fights for You

I started by isolating the table structure into a Blade component: responsive-table.blade.php. The key was balancing flexibility with consistency. I needed dynamic columns, optional actions, and variable data sources—but also predictable behavior.

I embraced mobile-first breakpoints in Tailwind, but went further. Instead of just hiding columns on small screens (a common cop-out), I implemented a priority-based visibility system using data-priority attributes and corresponding classes like min-[640px]:table-cell or min-[768px]:table-cell. This let me define which columns to show at each breakpoint directly in the data layer.

@props(['columns', 'rows', 'actions'])*

<table class="w-full border-collapse">
  <thead>
    <tr class="bg-gray-50 dark:bg-gray-800">
      @foreach ($columns as $key => $label)
        <th class="px-4 py-2 text-left text-sm font-medium text-gray-500 {{ $columnPriorities[$key] }}">
          {{ $label }}
        </th>
      @endforeach
      @if ($actions)
        <th class="px-4 py-2 text-right text-sm font-medium text-gray-500 sticky right-0 bg-white dark:bg-gray-900">
          Actions
        </th>
      @endif
    </tr>
  </thead>
  <tbody class="divide-y divide-gray-200 dark:divide-gray-700">
    <!-- rows -->
  </tbody>
</table>

The sticky action column was non-negotiable. Admins were constantly losing access to edit/delete buttons after horizontal scrolling. I used sticky right-0 combined with a fixed background to keep actions visible, even on wide tables. It worked—but only if I also set table-layout: fixed and controlled column widths.

Smarter Widths and Swipe Fallbacks

Tailwind’s utility classes are great, but applying them consistently across dynamic tables is tricky. I needed a way to assign proportional widths without hardcoding classes.

My solution? A simple PHP helper that calculated relative column weights based on priority and content type (e.g., ID columns = narrow, description = wide). This returned Tailwind w-1/6, w-1/4, etc., classes dynamically.

But mobile wasn’t just about shrinking. I added a swipe hint overlay for touch devices—subtle, non-intrusive—using a pointer-events-none pseudo-element with a "swipe →" animation. It disappeared after first scroll, thanks to a tiny inline script that set a session flag.

For narrow screens, I also introduced a card-like fallback: when viewport width dropped below 480px, the table switched to a stacked layout using a mobile-only Blade include. Each row became a card with key-value pairs, preserving all data in a touch-friendly format. This wasn’t graceful degradation—it was progressive enhancement within server-rendered limits.

Lessons from the Trenches

This wasn’t just about tables. It was about proving that modern UX is possible in legacy systems without greenfield rewrites. Here’s what stuck:

  • Blade can do more than I thought. With scoped props and dynamic rendering, I built something that felt like a React component—but without the JS overhead.
  • Tailwind’s utility-first approach shines in legacy contexts. I could incrementally modernize styles without disrupting existing CSS.
  • Mobile-first doesn’t mean mobile-only. I designed for the worst-case viewport, then enhanced upward—ensuring usability everywhere.
  • Backward compatibility is a feature. By keeping the same data contracts and view integrations, I rolled this out across 12+ pages in under a week.

The result? A single component now powers reports, user lists, and audit logs. I even refactored the filter forms to use a shared filter-form component, reducing duplication and improving consistency.

This table won’t win design awards. But it works. It scales. And it proves that with the right abstractions, even a 15-year-old Laravel-adjacent app can feel modern—without starting over.

Newer post

From Bloat to Blazing: How I Slashed CSS Bundle Size by Removing Tailwind Preflight in a Legacy PHP App

Older post

From Spaghetti to Structure: Refactoring Legacy Blade & CSS for Mobile-First Responsiveness