dxgx/blade-tailwind-extract
Laravel package that shrinks Livewire payloads by extracting long Tailwind class strings from Blade/Livewire into short hashed class names and a generated @apply CSS file. Supports class="", @class/->class arrays, dev inject vs prod extract, and collision-safe hashing.
A Laravel package that dramatically reduces Livewire component wire transfer sizes by extracting long Tailwind CSS class strings into short, reusable CSS class names. Perfect for applications with large lists of Livewire components.
This package powers the following projects:
When using Livewire with Tailwind CSS, long class strings in Blade templates get transferred over the wire on every component update:
<div class="flex flex-col gap-2 p-4 bg-white rounded-lg shadow-md hover:shadow-lg transition-shadow">
<!-- Large payload repeated for every list item -->
</div>
With 100 items in a list, this verbose markup significantly impacts performance.
Blade Tailwind Extract extracts your Tailwind classes into a CSS file using @apply, replacing them with short class names:
Development Mode (Injected):
<div class="__card-wrapper__ flex flex-col gap-2 p-4 bg-white rounded-lg shadow-md __">
<!-- Easy to edit inline -->
</div>
Production Mode (Extracted):
<div class="TW-a40f-card-wrapper">
<!-- Minimal wire payload -->
</div>
Generated CSS:
.TW-a40f-card-wrapper {
@apply flex flex-col gap-2 p-4 bg-white rounded-lg shadow-md;
}
The a40f hash is derived from the file path, ensuring no conflicts between files.
class="", wire:class="", :class (static & ternary), x-bind:class, @class([]) (simple & conditional)group, group/, and peer are automatically skipped with informative warningsInstall via Composer as a dev dependency (this is a development tool, not needed in production):
composer require dxgx/blade-tailwind-extract --dev
Publish the configuration file (optional):
php artisan vendor:publish --tag=blade-tailwind-extract-config
Before extraction, use the wrap command to automatically identify and mark repeated Tailwind class lists with semantic wrapper names. This helps you see which classes will be extracted and ensures consistent naming for identical class lists.
# Wrap classes in a specific file
php artisan dg:blade-tailwind:wrap resources/views/components/card.blade.php
# Wrap classes in an entire directory (recursive)
php artisan dg:blade-tailwind:wrap ./resources/views/livewire
# Wrap classes using a pattern
php artisan dg:blade-tailwind:wrap "*preview*"
php artisan dg:blade-tailwind:wrap "*card*.blade.php"
# Wrap multiple files or patterns (comma-separated)
php artisan dg:blade-tailwind:wrap "card.blade.php,list.blade.php"
php artisan dg:blade-tailwind:wrap "*preview*,*card*"
# Wrap all files in search_path (with confirmation prompts)
php artisan dg:blade-tailwind:wrap
# Skip confirmations for automated workflows
php artisan dg:blade-tailwind:wrap --yy
# Preview changes without modifying files
php artisan dg:blade-tailwind:wrap "*preview*" --dry-run
# Customize minimum class count (default: 3)
php artisan dg:blade-tailwind:wrap components/card.blade.php --min=4
# Skip class lists containing specific prefix (default: TW)
php artisan dg:blade-tailwind:wrap components/card.blade.php --skip-prefix=CUSTOM
What it does:
--min)__adjective-noun-number__ (e.g., __happy-cat-1__)__, material-symbols-outlined, TW-Before wrapping:
<div class="flex items-center justify-between gap-4 p-4">Item 1</div>
<div class="flex items-center justify-between gap-4 p-4">Item 2</div>
<div class="bg-blue-500 text-white rounded p-2">Button</div>
After wrapping:
<div class="__happy-cat-1__ flex items-center justify-between gap-4 p-4 __">Item 1</div>
<div class="__happy-cat-1__ flex items-center justify-between gap-4 p-4 __">Item 2</div>
<div class="__quick-fox-2__ bg-blue-500 text-white rounded p-2 __">Button</div>
Notice how identical class lists share the same wrapper name (happy-cat-1), while different lists get unique names.
The wrap command supports all major Blade/Livewire/Alpine.js class attribute patterns:
class AttributeStandard HTML class attribute:
<!-- Before -->
<div class="flex items-center justify-between gap-4 p-4">Content</div>
<!-- After wrapping -->
<div class="__happy-cat-1__ flex items-center justify-between gap-4 p-4 __">Content</div>
wire:class AttributeLivewire's reactive class binding:
<!-- Before -->
<div wire:class="flex items-center space-x-2 p-4">Content</div>
<!-- After wrapping -->
<div wire:class="__sunny-owl-2__ flex items-center space-x-2 p-4 __">Content</div>
:class with Static ClassesDynamic class binding with static string:
<!-- Before -->
<button :class="flex items-center justify-center rounded-md p-2">Click</button>
<!-- After wrapping -->
<button :class="__quick-fox-3__ flex items-center justify-center rounded-md p-2 __">Click</button>
:class with Ternary ExpressionConditional class switching with ternary operator:
<!-- Before -->
<button :class="isActive
? 'bg-blue-500 text-white border-blue-500 hover:bg-blue-700'
: 'bg-white text-gray-700 border-gray-300 hover:bg-gray-100'">
Toggle
</button>
<!-- After wrapping (both branches wrapped independently) -->
<button :class="isActive
? '__brave-ant-4__ bg-blue-500 text-white border-blue-500 hover:bg-blue-700 __'
: '__calm-bee-5__ bg-white text-gray-700 border-gray-300 hover:bg-gray-100 __'">
Toggle
</button>
x-bind:class AttributeExplicit Alpine binding syntax:
<!-- Before -->
<div x-bind:class="flex flex-col gap-4 p-6">Content</div>
<!-- After wrapping -->
<div x-bind:class="__lazy-ram-6__ flex flex-col gap-4 p-6 __">Content</div>
@class Directive (Simple Array)Blade's class directive with simple string:
<!-- Before -->
<div @class(['flex items-center justify-between p-4 border rounded'])>Item</div>
<!-- After wrapping -->
<div @class(['__eager-jay-7__ flex items-center justify-between p-4 border rounded __'])>Item</div>
@class Directive (Conditional Array)Blade's class directive with conditional classes:
<!-- Before -->
<button @class([
'flex items-center justify-center size-9',
'text-gray-200 cursor-not-allowed opacity-50' => !$isEnabled,
'text-blue-500 hover:text-blue-600 hover:bg-blue-50' => $isEnabled
])>Action</button>
<!-- After wrapping (all qualifying strings wrapped) -->
<button @class([
'__gentle-fox-8__ flex items-center justify-center size-9 __',
'__jolly-cat-9__ text-gray-200 cursor-not-allowed opacity-50 __' => !$isEnabled,
'__kind-dog-10__ text-blue-500 hover:text-blue-600 hover:bg-blue-50 __' => $isEnabled
])>Action</button>
Elements with both static class and dynamic :class attributes (wrapped independently):
<!-- Before -->
<button
:class="active ? 'bg-blue-500 text-white' : 'bg-gray-200 text-gray-700'"
class="flex items-center justify-center rounded-md">
Toggle
</button>
<!-- After wrapping -->
<button
:class="active ? 'bg-blue-500 text-white' : 'bg-gray-200 text-gray-700'"
class="__lively-hen-11__ flex items-center justify-center rounded-md __">
Toggle
</button>
Note: In this example, the ternary branches have only 2 classes each (below the default minimum of 3), so they aren't wrapped. The static class attribute with 4 classes is wrapped.
--min)__, material-symbols-outlined, or starting with TW-) are never wrapped@class have all string values examined and wrapped independentlyThe wrap command correctly handles complex Tailwind patterns:
Arbitrary values with quotes:
<!-- Classes with arbitrary values containing quotes are fully supported -->
<div class="after:content-[''] before:content-['>'] w-full">Content</div>
<!-- After wrapping (closing __ correctly placed at end) -->
<div class="__happy-cat-1__ after:content-[''] before:content-['>'] w-full __">Content</div>
Reserved classes (automatically skipped during extraction):
group, group/, or peer can be wrapped but won't be extractedThe target parameter accepts multiple formats:
0. No target (processes all files in search_path with confirmation):
# Prompts for confirmation before processing all .blade.php files
php artisan dg:blade-tailwind:extract
# Shows:
# 1. First confirmation: "Are you sure you want to process X file(s)?"
# 2. List of files to be processed (max 50 shown)
# 3. Second confirmation: "Proceed with extract operation?"
# Cancel at any prompt to abort
1. Directory (processes all .blade.php files recursively):
php artisan dg:blade-tailwind:extract ./resources/views
php artisan dg:blade-tailwind:extract ./resources/views/components
2. Single file path:
php artisan dg:blade-tailwind:extract resources/views/livewire/garage/list/item.blade.php
php artisan dg:blade-tailwind:extract components/card.blade.php
3. Pattern matching (searches in configured search_path):
php artisan dg:blade-tailwind:extract *preview*
php artisan dg:blade-tailwind:extract *card*.blade.php
php artisan dg:blade-tailwind:extract list-item
4. Multiple targets (comma-separated, can mix patterns and files):
php artisan dg:blade-tailwind:extract image-preview.blade.php,card-list.blade.php
php artisan dg:blade-tailwind:extract *preview*,*card*,header.blade.php
Restore Tailwind classes for editing (accepts same target formats as extract):
# No target (with confirmation prompts)
php artisan dg:blade-tailwind:restore
# Directory
php artisan dg:blade-tailwind:restore ./resources/views
# Pattern
php artisan dg:blade-tailwind:restore *preview*
# Specific file
php artisan dg:blade-tailwind:restore components/card.blade.php
# Multiple
php artisan dg:blade-tailwind:restore *card*,*list*
Automatic CSS Cleanup: When you restore classes, the command automatically removes the restored CSS rules from the output file, keeping your CSS clean and preventing stale rules from accumulating.
Option A: Automated (New Files)
Option B: Manual (Existing Workflow)
__name__ classes __ markers around classes you want to extractOption C: Editing Existing Extracted Files
You can add markers manually or use the wrap command to add them automatically.
Manual approach: Wrap classes you want to extract with double underscores:
<div class="__card-wrapper__ flex flex-col gap-2 p-4 __">
<!-- Content -->
</div>
Automated approach: Use the wrap command to automatically identify and mark class lists:
php artisan dg:blade-tailwind:wrap your.view.namelass="__card-wrapper__ flex flex-col gap-2 p-4 __">
<!-- Content -->
</div>
After extraction:
<div class="TW-a40f-card-wrapper">
<!-- Content -->
</div>
Important: The group and peer Tailwind classes cannot be extracted (they break parent-child selectors). The tool will warn and skip them automatically.
Configuration file: config/dg-blade-tailwind-extract.php
return [
// CSS output file path
'css_output_path' => resource_path('css/tw-extracted.css'),
// Class prefix (default: TW)
'class_prefix' => 'TW',
// Hash length for file-based collision avoidance (default: 4)
'hash_length' => 4,
// Default search path when using pattern matching
'search_path' => resource_path('views'),
// Directories to ignore during file scanning
'ignored_directories' => [
'./vendor/',
'vendor/',
'./node_modules/',
'node_modules/',
],
// Reserved Tailwind classes that cannot be extracted
// These use parent-child selectors that break when extracted to @apply
// Also checks for variants like 'group/' and 'peer/'
'reserved_classes' => [
'group',
'peer',
],
// Safety limit for extraction loops
'max_iterations' => 10,
];
Note: When the extract command encounters patterns containing reserved classes (like group, group/item, or peer), it will:
php artisan dg:blade-tailwind:extract ./resources/views --css-file=resources/css/my-custom.css
The tool works seamlessly with Livewire's ->class([...]) method and Blade's @class([...]) directive:
Before Extraction:
<div @class([
'__item-wrapper__ flex gap-2 p-4 __',
'bg-green-50' => $selected,
])>
After Extraction:
<div @class([
'TW-a40f-item-wrapper',
'bg-green-50' => $selected,
])>
Wrap Mode (Optional Preparation):**
__adjective-noun-number____name__ classes __Extract Mode:
__name__ tailwind classes __ patterns{PREFIX}-{HASH}-{NAME}@apply rules to the CSS fileRestore Mode:
@apply rules from CSS fileExtract Mode:
__name__ tailwind classes __ patterns{PREFIX}-{HASH}-{NAME}@apply rules to the CSS fileRestore Mode:
@apply rules from CSS fileFile Hash:
card-wrapper) across different filesTW-* class names in extracted filesgroup, peer, or other parent-modifier classescomposer test
If you discover any security-related issues, please email [email protected] instead of using the issue tracker.
The MIT License (MIT). Please see License File for more information.
Please see CHANGELOG.md for recent changes.
Please see CONTRIBUTING.md for details.
How can I help you explore Laravel packages today?