Introduction
Tailwind utilities are powerful, but repeating class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6" across 40 pages is not maintainable. Blade components let you encapsulate responsive layouts behind a single tag so your templates stay readable and the rules stay in one place.
Key Concepts
- Reusable layout component: A Blade component that wraps a common responsive pattern (navigation, card grid, split layout) and exposes a small prop API.
- Slot: The named or default placeholder inside a component where the caller supplies content.
- Container queries: A Tailwind v4 feature (
@container) that lets a component react to the size of its parent rather than the viewport. Perfect for components that live inside sidebars or modals.
Real World Context
Reusing raw utility classes is fine until you need to change them globally. When your designer wants '3 columns on desktop, 2 on tablet, 1 on mobile' to become '4/2/1', you do not want to hand-edit every page. A single <x-card-grid> component means the change happens in one file.
Deep Dive
A Responsive Navigation Component
Put this in resources/views/components/nav-bar.blade.php:
blade@props(['user' => null]) <nav x-data="{ open: false }" class="bg-white border-b dark:bg-gray-900 dark:border-gray-800" > <div class="max-w-7xl mx-auto px-4"> <div class="flex h-16 items-center justify-between"> <a href="/" class="text-xl font-bold">{{ config('app.name') }}</a> {{-- Desktop nav --}} <div class="hidden md:flex items-center gap-6"> {{ $slot }} </div> {{-- Mobile trigger --}} <button class="md:hidden p-2" @click="open = !open" :aria-expanded="open" aria-label="Toggle navigation" > <svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h16" /> </svg> </button> </div> {{-- Mobile menu --}} <div x-show="open" x-transition class="md:hidden py-4 flex flex-col gap-2 border-t dark:border-gray-800" > {{ $slot }} </div> </div> </nav>
Callers supply the links once as the default slot, and the component renders them in both the desktop and mobile menus:
blade<x-nav-bar> <a href="/" class="hover:text-blue-600">Home</a> <a href="/blog" class="hover:text-blue-600">Blog</a> <a href="/contact" class="hover:text-blue-600">Contact</a> </x-nav-bar>
The responsive breakpoint lives inside the component — pages never repeat it.
A Responsive Card Grid
blade{{-- resources/views/components/card-grid.blade.php --}} @props([ 'cols' => [1, 2, 3], // [mobile, tablet, desktop] 'gap' => 6, ]) @php // Use a literal class lookup so Tailwind's content scanner can see every class. // Building class names via string interpolation like "grid-cols-{$n}" would get purged in production. $colsMap = [ 1 => 'grid-cols-1', 2 => 'grid-cols-2', 3 => 'grid-cols-3', 4 => 'grid-cols-4', ]; $tabletMap = [ 1 => 'md:grid-cols-1', 2 => 'md:grid-cols-2', 3 => 'md:grid-cols-3', 4 => 'md:grid-cols-4', ]; $desktopMap = [ 1 => 'lg:grid-cols-1', 2 => 'lg:grid-cols-2', 3 => 'lg:grid-cols-3', 4 => 'lg:grid-cols-4', ]; $gapMap = [2 => 'gap-2', 4 => 'gap-4', 6 => 'gap-6', 8 => 'gap-8']; [$mobile, $tablet, $desktop] = array_pad($cols, 3, 1); $classes = 'grid ' . ($colsMap[$mobile] ?? 'grid-cols-1') . ' ' . ($tabletMap[$tablet] ?? 'md:grid-cols-2') . ' ' . ($desktopMap[$desktop] ?? 'lg:grid-cols-3') . ' ' . ($gapMap[$gap] ?? 'gap-6'); @endphp <div {{ $attributes->merge(['class' => $classes]) }}> {{ $slot }} </div>
Usage:
blade<x-card-grid :cols="[1, 2, 4]" :gap="4"> @foreach ($products as $product) <x-product-card :product="$product" /> @endforeach </x-card-grid>
Note: the literal-class lookup pattern above is important. If you build class names via string interpolation (e.g.
grid-cols-{$mobile}), Tailwind's content scanner never sees the full class and the style gets purged from the production build. Use a map of literal classes, or add every possible variant to a Tailwind safelist.
Container Queries for Truly Reusable Components
A component that lives in both a full-width page and a narrow sidebar should adapt to its own width, not the viewport. Tailwind v4 supports container queries natively:
blade{{-- resources/views/components/product-card.blade.php --}} <article class="@container bg-white dark:bg-gray-800 rounded-lg shadow"> <div class="flex flex-col @md:flex-row gap-4 p-4"> <img src="{{ $product->image }}" class="w-full @md:w-32 h-32 object-cover rounded"> <div class="flex-1"> <h3 class="font-semibold">{{ $product->name }}</h3> <p class="text-sm text-gray-600 dark:text-gray-400">{{ $product->description }}</p> </div> </div> </article>
The @container class enables container queries on the parent. The @md:flex-row utility applies when the container (not the viewport) is at least medium width. Drop the same card into a sidebar and it automatically stacks; drop it into a main column and it lays out horizontally — no viewport breakpoints involved.
Common Pitfalls
- Dynamic utility class names — Strings like
grid-cols-{$mobile}can be purged by Tailwind's content scanner if the raw classes never appear in source. Safelist them or use a lookup map of literal classes. - Overloading one component — Don't build
<x-layout>that handles navigation, hero, grid, footer, and modals. Break it into focused components. - Duplicating slot content for desktop and mobile — The navigation example renders
$slottwice. Make sure your links don't rely on sibling-specific styles that break in one context.
Best Practices
- Encapsulate one layout concept per component — A nav bar, a card grid, a hero section. If it has a name, it's probably a component.
- Prefer container queries over viewport queries for reusable components — They travel better across contexts.
- Lean on slots, not giant prop arrays — Slots compose naturally and keep components shallow. Reach for props when a value is genuinely structural (like a column count).
Summary
- Wrap repeated responsive patterns in Blade components to keep templates maintainable.
- Use slots for content and props for structural options like column counts.
- Container queries (
@container+@md:) make components react to their parent's width, not the viewport's — ideal for reusable layouts. - Watch out for dynamic utility class names; safelist or precompute them so Tailwind ships them.