Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Laravel Headless Ui Laravel Package

schaefersoft/laravel-headless-ui

Unstyled, accessible Laravel Blade UI components for building data-driven interfaces with traditional Blade. No extra JavaScript dependencies, performance-focused and easy to customize with your own CSS or utility classes. Requires PHP 8.2+ and Laravel 10–13.

View on GitHub
Deep Wiki
Context7

Getting Started

Requirements: PHP 8.2+, Laravel 10–13. The service provider auto-discovers, so there is no config/app.php step and no publish step.

composer require schaefersoft/laravel-headless-ui

Then wire up both assets — this package is "no JS dependencies", not "no JS". The components need hui.js for the interaction/ARIA behaviour, and hui.css for the state-driven show/hide rules.

/* resources/css/app.css */
@import '../../vendor/schaefersoft/laravel-headless-ui/resources/css/hui.css' layer(base);
/* drop layer(base) if you are not on Tailwind v4 */
// resources/js/app.js
import '../../vendor/schaefersoft/laravel-headless-ui/dist/js/hui.js'

Use resources/js/hui.ts only if you already have a TypeScript pipeline; the prebuilt dist/js/hui.js is the recommended path and avoids adding vite TS config just for one dependency.

Where to look first

  1. docs/<component>.md in the vendor directory — one file per component (avatar, dropdown, dialog, disclosure, flyout, range-slider, tabs, toggle, tooltip), each with the markup contract. Read the dropdown and dialog docs before anything else; they establish the trigger/panel composition pattern the rest follow.
  2. resources/views/components/ — the actual Blade class list and props for each subcomponent (maxWidth, position, open/opened, disabled, closeOnClick, closeOnBgClick, overFlow all come from documented props).
  3. resources/js/hui.ts — the only place that defines which data-* attributes and ARIA wiring you can style against. Skim it once; you will refer back to it every time a state style "doesn't apply".

First use case: a single dropdown in an existing authenticated layout. It exercises everything a real integration needs — composing root/trigger/panel subcomponents, styling via utility classes, and confirming the JS bundle resolves through Vite. Get that one component visually correct before adding dialogs, tabs or flyouts to a live page.

Implementation Patterns

Compose subcomponents, don't look for a monolithic tag. Every component ships as an x-hui:: root plus named parts (trigger/button, panel, item, backdrop, thumb, etc.). You are responsible for the markup between them and for all classes. Treat it like a Blade-native Radix: the structure is the API.

{{-- shape, not copy-paste — confirm exact subcomponent names in docs/dropdown.md --}}
<x-hui::dropdown>
    <x-hui::dropdown.button class="rounded px-3 py-2 hover:bg-gray-100">
        Actions
    </x-hui::dropdown.button>

    <x-hui::dropdown.panel class="absolute z-20 mt-1 rounded border bg-white shadow">
        @foreach ($actions as $action)
            <x-hui::dropdown.item :href="route('tasks.show', $action)" class="block px-3 py-2 text-sm">
                {{ $action->name }}
            </x-hui::dropdown.item>
        @endforeach
    </x-hui::dropdown.panel>
</x-hui::dropdown>

Build one design-system wrapper per component. Because the package is unstyled, your app will accumulate long class strings at every call site. Add your own thin wrappers (resources/views/components/app/dropdown.blade.php) that forward $attributes and hardcode your tokens. That keeps the vendor component swappable and means a redesign touches one file, not forty.

Style state through data attributes, not through class toggling. The JS writes state onto the root/parts (data-open, data-active, data-disabled, aria-expanded, aria-selected). Write your state CSS once against those attributes:

.hui-panel { display: none; }
[data-open] .hui-panel { display: block; }
[data-disabled] { opacity: .5; pointer-events: none; }

In Tailwind, this maps to data-[open]:block style variants — but only if the attribute is on the element that also carries your utility classes. Verify per component.

Server-driven content is the whole point. Populate dropdown items, tabs and flyout links from Eloquent collections and gate them with @can in the Blade loop. Dialogs and flyouts can be rendered conditionally per page rather than globally, which keeps initial HTML small and avoids a stack of hidden modals on every route.

Forms: add your own inputs. The toggle and range slider are visual control shells; they do not replace native form semantics. Include a hidden or visually-hidden input with name=/value= inside the control and keep it in sync, and confirm in the browser that a disabled control does not submit a stale value. For Livewire, drive that hidden input with wire:model and wrap the component in wire:ignore so DOM morphing does not strip the JS-owned attributes.

Dialog placement: render dialogs at the end of the layout (after the main content, before </body>), never inside a transform, overflow: hidden, or low z-index container. Use overflow, maxWidth, closeOnClick, closeOnBgClick rather than hand-rolling a backdrop — the package exposes a dedicated modal backdrop component since 1.2.0.

Flyout as navigation: the inline sidebar variant is rendered visible before JS runs (fixed in 1.5.2), so use that mode for admin sidebars where you cannot tolerate a flash of hidden nav. Overlay mode is for narrow viewports.

Progressive enhancement where it matters: the panel content is real Blade HTML, so it is indexable and present in the source. Keep the content server-rendered; do not fetch it over the wire just because the component is "headless".

Testing: feature tests can assertSee the rendered ARIA attributes and state — cheap regression protection for markup changes on upgrade. Anything keyboard-driven (arrow keys in tabs/dropdowns, Esc on tooltips, focus trap in dialogs) needs a real browser test (Dusk/Playwright); assert on the resulting focus target and open state, not on pixel classes.

Gotchas and Tips

  • Asset paths live in vendor/. Vite must be able to read them. In Docker or CI where npm ci runs before composer install, the CSS/JS import silently fails or the build breaks with a resolution error. Install PHP deps first, or add the vendor directory to server.fs.allow in vite.config.js. After a composer update that bumps the package, restart the dev server — Vite caches the resolved module graph.
  • Tailwind version decides the import syntax. Use @import ... layer(base) on Tailwind v4 so preflight and the component reset do not fight; on v3 the plain @import is what you want. Wrong layer ordering shows up as panels that render open on first paint, then snap shut.
  • "No JS dependencies" ≠ no JS. Without hui.js you get static markup: nothing toggles, aria-expanded never updates, Esc does nothing. The first debugging step for "it doesn't open" is always: check the browser console and confirm the import made it into the bundle output.
  • Attribute naming has churned. The changelog shows a data-active duplicate typo fix, a tablist → sentence-case rename, and a reverted value property on toggle. Pin an exact version in composer.json (this is a 1.x package with active API movement) and grep your stylesheets and Blade overrides for data-* attributes on every upgrade.
  • Accessibility is a moving target here too. 1.5.4 removed invalid ARIA roles from the disclosure components — evidence that ARIA correctness is fixed reactively. Do a keyboard pass plus a screen-reader spot check on the components you actually ship rather than trusting the "accessible" claim wholesale.
  • Never hand-write the state attributes. Setting data-open, aria-expanded, or aria-controls yourself will collide with the JS and produce stuck-open panels or duplicated toggling. If you need an initially-open state, use the documented prop (opened-by-default support landed in 1.1.0), not an attribute on the tag.
  • $attributes merging on your wrappers. When you build an x-app::dropdown wrapper, spread {{ $attributes }} on the root — but be aware that class will merge while other attributes can override. Never let a call site inject data-* keys; restrict what you forward.
  • Global key handlers conflict. A page-level keydown listener for a command palette or search shortcut will fire while a dialog is open. Gate your handlers on the presence of an open dialog or check event.defaultPrevented, otherwise Esc closes two things at once.
  • Livewire/Alpine coexistence. Do not pair these components with Alpine x-data on the same markup — two systems writing the same attributes is an unfixable bug. After a Livewire navigation or morph, components rendered inside the morphed region lose behaviour: wire:ignore them, or re-render the whole block with wire:key so the JS re-binds.
  • Debugging is devtools-first. Inspect the root element for its state attributes and compare against resources/js/hui.ts. If the attribute changes but nothing visually happens, the bug is in your CSS selector specificity or a missing class on the panel element, not in the package.
  • Ecosystem support is thin (3 stars, zero dependents). There is no StackOverflow corpus. Budget time to read vendor/schaefersoft/laravel-headless-ui/resources/ when behaviour is undocumented, and expect to send upstream PRs for real bugs. The upside is the whole surface area is small enough to read in an afternoon — do that before committing, because the docs are per-component and thinner than the code.
Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
se7enxweb/flysystem
lampwick/laravel
jeslage/statamic-emojis
micahdshackelford/rmap-laravel
draftsmaninc/draftsman
square-bit/laravel-volition
himito/laravel-mailman
htinlynn/genius-helper
maco-studios/hephaestus
elmogy/larafast
xeviant/laravel-paystack
gorilladash/laravel-website-sdk
martialbe/flysystem-oss
chondal/3312client
jeroen-g/flickr
opscale-co/nova-genbi
itsjustvita/laravel-retention
tequilarapido/trackit
gregsphotography/site-builder-filament
cabaoctavio/laravel-query-log