dxgx/blade-tailwind-extract

Extract Tailwind CSS classes to reduce Livewire wire transfer size

Maintainers

Package info

github.com/dxgx/blade-tailwind-extract

pkg:composer/dxgx/blade-tailwind-extract

Statistics

Installs: 65

Dependents: 0

Suggesters: 0

Stars: 0

Open Issues: 0

v2.5.0 2026-06-04 22:00 UTC

This package is auto-updated.

Last update: 2026-06-04 22:00:46 UTC


README

Latest Version on Packagist Total Downloads License

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.

The Problem

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.

The Solution

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.

Features

  • Reduces Livewire wire payload - Short class names instead of verbose Tailwind strings
  • Automated class identification - Wrap command finds and marks repeated class lists
  • Intelligent deduplication - Identical class lists share the same wrapper name
  • Comprehensive pattern support - Works with class="", wire:class="", :class (static & ternary), x-bind:class, @class([]) (simple & conditional)
  • Bidirectional - Extract for production, restore for development
  • Safe - Reserved classes like group, group/, and peer are automatically skipped with informative warnings
  • Collision-free - File-based hashing prevents class name conflicts
  • Pattern matching - Process specific files or entire directories
  • Configurable - Customize prefix, hash length, output path, and more

Installation

Install 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

Usage

Quick Start: 3-Step Workflow

  1. Wrap - Automatically identify and mark class lists with semantic names
  2. Extract - Convert marked classes into compact CSS
  3. Restore - Bring back inline classes for editing

Wrap Classes (Preparation Step)

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:

  • Scans for class lists with 3+ classes (configurable via --min)
  • Automatically deduplicates: identical class lists get the same wrapper name
  • Generates semantic names: __adjective-noun-number__ (e.g., __happy-cat-1__)
  • Skips patterns that should never be extracted: __, material-symbols-outlined, TW-
  • Shows summary of changes with occurrence counts

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.

Supported Class Attribute Patterns

The wrap command supports all major Blade/Livewire/Alpine.js class attribute patterns:

1. Static class Attribute

Standard 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>

2. Livewire wire:class Attribute

Livewire'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>

3. Alpine.js :class with Static Classes

Dynamic 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>

4. Alpine.js :class with Ternary Expression

Conditional 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>

5. Alpine.js x-bind:class Attribute

Explicit 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>

6. Blade @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>

7. Blade @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>

8. Combined Static and Dynamic Classes

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.

Pattern Behavior Summary

  • All patterns respect the minimum class count (default: 3, configurable via --min)
  • Identical class lists across any pattern type share the same wrapper name
  • Already wrapped class lists are skipped (won't double-wrap)
  • Protected patterns (containing __, material-symbols-outlined, or starting with TW-) are never wrapped
  • Ternary expressions have both branches examined and wrapped independently if they meet the threshold
  • Conditional arrays in @class have all string values examined and wrapped independently
  • Static and dynamic attributes on the same element are processed independently

Special Cases Handled

The 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):

  • Classes containing group, group/, or peer can be wrapped but won't be extracted
  • The extract command will skip these patterns with informative warnings
  • This prevents breaking Tailwind's parent-child and sibling selectors

Extract Classes (Development → Production)

The 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 Classes (Production → Development)

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.

Workflow

Option A: Automated (New Files)

  1. Wrap your Blade file to identify and mark repeated class lists
  2. Extract marked classes to generate compact CSS
  3. Commit both Blade files and CSS file

Option B: Manual (Existing Workflow)

  1. Manually add __name__ classes __ markers around classes you want to extract
  2. Extract to generate compact CSS
  3. Commit both Blade files and CSS file

Option C: Editing Existing Extracted Files

  1. Restore before editing (restore Tailwind classes to inline format)
  2. Edit your Blade files normally
  3. Extract after editing (compact back to short class names)
  4. Commit both Blade files and CSS file

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

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:

  • Skip extraction for that pattern
  • Display an informative warning showing which patterns were skipped and why
  • Continue processing other patterns in the same file

Advanced Usage

Custom CSS Output Path

php artisan dg:blade-tailwind:extract ./resources/views --css-file=resources/css/my-custom.css

Working with Livewire Components

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):**

  • Scans for class lists with minimum number of classes (default: 3)
  • Automatically deduplicates identical class lists
  • Generates semantic wrapper names: __adjective-noun-number__
  • Marks class lists: __name__ classes __
  • Skips protected patterns (material-symbols-outlined, TW-, etc.)
  • Shows summary with occurrence counts
  1. Extract Mode:

    • Scans for __name__ tailwind classes __ patterns
    • Generates short class names: {PREFIX}-{HASH}-{NAME}
    • Writes @apply rules to the CSS file
    • Replaces inline classes with short names
  2. Restore Mode:

    • Reads existing @apply rules from CSS file
    • Finds short class names in Blade files
    • Restores original Tailwind class strings
  3. Extract Mode:

    • Scans for __name__ tailwind classes __ patterns
    • Generates short class names: {PREFIX}-{HASH}-{NAME}
    • Writes @apply rules to the CSS file
    • Replaces inline classes with short names
  4. Restore Mode:

    • Reads existing @apply rules from CSS file
    • Finds short class names in Blade files
    • Restores original Tailwind class strings
  5. File Hash:

    • Each file gets a unique 4-character hash (configurable)
    • Prevents class name collisions between files
    • Allows same semantic name (card-wrapper) across different files

Gotchas & Best Practices

  • ⚠️ Never manually edit TW-* class names in extracted files
  • ⚠️ Always restore → edit → extract workflow
  • ⚠️ Don't extract group, peer, or other parent-modifier classes
  • ✅ Commit both Blade and CSS files together
  • ✅ Run extract before deploying to production
  • ✅ Run restore before starting development

Testing

composer test

Security

If you discover any security-related issues, please email packages@dxgx.dev instead of using the issue tracker.

Credits

License

The MIT License (MIT). Please see License File for more information.

Changelog

Please see CHANGELOG.md for recent changes.

Contributing

Please see CONTRIBUTING.md for details.