schaefersoft/laravel-headless-ui
Installation:
composer require schaefersoft/laravel-headless-ui
No manual service provider registration needed (auto-discovery).
Import Assets:
@import '../../vendor/schaefersoft/laravel-headless-ui/resources/css/hui.css';
import '../../vendor/schaefersoft/laravel-headless-ui/dist/js/hui.js';
First Component:
Use the x-hui:: prefix in Blade:
<x-hui::tabs>
<x-hui::tabs.tablist>
<x-hui::tabs.tab>Tab 1</x-hui::tabs.tab>
<x-hui::tabs.tab>Tab 2</x-hui::tabs.tab>
</x-hui::tabs.tablist>
<x-hui::tabs.panel>Content 1</x-hui::tabs.panel>
<x-hui::tabs.panel>Content 2</x-hui::tabs.panel>
</x-hui::tabs>
docs/tabs.md).Replace a basic tab system: Replace a jQuery-based or Bootstrap tab implementation with this headless version for better accessibility and no external JS dependencies.
Component Composition:
x-hui::tabs + x-hui::tabs.tab).<x-hui::dropdown>
<x-hui::dropdown.trigger>Menu</x-hui::dropdown.trigger>
<x-hui::dropdown.content>
<x-hui::dropdown.item>Item 1</x-hui::dropdown.item>
<x-hui::dropdown.item>Item 2</x-hui::dropdown.item>
</x-hui::dropdown.content>
</x-hui::dropdown>
State Management:
:initial-index for Tabs or active for Disclosure:
<x-hui::disclosure :open="true">
<x-hui::disclosure.button>Click me</x-hui::disclosure.button>
<x-hui::disclosure.panel>Content</x-hui::disclosure.panel>
</x-hui::disclosure>
Dynamic Content:
<x-hui::tabs>
<x-hui::tabs.tablist>
@foreach ($items as $item)
<x-hui::tabs.tab>{{ $item->name }}</x-hui::tabs.tab>
@endforeach
</x-hui::tabs.tablist>
<!-- Panels -->
</x-hui::tabs>
Form Integration:
<x-hui::range-slider>
<x-hui::range-slider.track>
<x-hui::range-slider.thumb role="min" name="price_min" value="{{ old('price_min', 20) }}"/>
<x-hui::range-slider.thumb role="max" name="price_max" value="{{ old('price_max', 80) }}"/>
</x-hui::range-slider.track>
</x-hui::range-slider>
Accessibility-First Development:
aria-disabled, aria-expanded).ArrowLeft/Right for Tabs, Space/Enter for Dropdowns).Tailwind CSS Integration:
@import '../../vendor/schaefersoft/laravel-headless-ui/resources/css/hui.css' layer(base);
data-[active]:bg-blue-500).Conditional Rendering:
@if to toggle components dynamically:
@if (auth()->user()->can('edit'))
<x-hui::dropdown>
<x-hui::dropdown.trigger>Actions</x-hui::dropdown.trigger>
<x-hui::dropdown.content>
<x-hui::dropdown.item>Edit</x-hui::dropdown.item>
<x-hui::dropdown.item>Delete</x-hui::dropdown.item>
</x-hui::dropdown.content>
</x-hui::dropdown>
@endif
Reusable Components:
x-hui::* components:
<!-- resources/views/components/admin-tabs.blade.php -->
<x-hui::tabs :vertical="true">
<x-hui::tabs.tablist>
<x-hui::tabs.tab active>Overview</x-hui::tabs.tab>
<x-hui::tabs.tab>Settings</x-hui::tabs.tab>
</x-hui::tabs.tablist>
{{ $slot }}
</x-hui::tabs>
<x-admin-tabs>
<x-hui::tabs.panel>Overview content</x-hui::tabs.panel>
<x-hui::tabs.panel>Settings content</x-hui::tabs.panel>
</x-admin-tabs>
Laravel Mix/Vite:
// vite.config.js
export default defineConfig({
resolve: {
alias: {
'laravel-headless-ui': path.resolve(__dirname, 'vendor/schaefersoft/laravel-headless-ui'),
},
},
});
Form Requests:
public function rules()
{
return [
'price_min' => 'sometimes|integer|min:0|max:100',
'price_max' => 'sometimes|integer|min:0|max:100',
];
}
Livewire/Alpine.js:
<div x-data="{ open: false }">
<x-hui::disclosure @open.window="open = true">
<x-hui::disclosure.button>Toggle</x-hui::disclosure.button>
<x-hui::disclosure.panel x-show="open">Content</x-hui::disclosure.panel>
</x-hui::disclosure>
</div>
Testing:
$this->get('/dashboard')
->assertSeeInOrder([
'Tab 1',
'Tab 2',
])
->press('Tab 2')
->assertSee('Content 2');
Missing role Attribute:
role="min" or role="max".<x-hui::range-slider.thumb role="min" ... />
<x-hui::range-slider.thumb role="max" ... />
CSS Specificity:
!important sparingly or increase specificity:
.custom-slider .hui-range-slider-thumb::-webkit-slider-thumb {
background: red !important;
}
JavaScript Conflicts:
hui.ts), ensure your bundler supports ES modules.hui.js for simplicity.Disabled State:
data-[disabled] attributes in CSS:
[data-disabled] {
opacity: 0.5;
pointer-events: none;
}
Initial State:
:initial-index may not work as expected if tabs are dynamically loaded.document.querySelector('[data-active]').click();
How can I help you explore Laravel packages today?