yungifez/april-ui
April UI is a Laravel/PHP UI toolkit for building modern, responsive interfaces with reusable components and minimal setup. It helps teams ship clean, maintainable dashboards and admin panels faster.
April UI is a Blade component library with a shadcn-style vocabulary, so the mental model transfers almost directly if you've used shadcn/ui before: you compose components with Tailwind classes and let Alpine own the interactive behavior. The difference is that everything is server-rendered Blade rather than React.
Install is the usual two steps: pull it in with Composer, then make its views and compiled assets available to your app.
composer require yungifez/april-ui
php artisan vendor:publish --tag=april-ui-assets # confirm tag names in the README
Then do three things in this order:
april prefix via its service provider, so <april:button> works out of the box. Verify with a single component before going further.content globs (v3) or an @source directive (v4). Without this, the classes in the component markup are never generated.First use case: pick one screen with a form and a confirmation flow — a settings page or a "create record" modal. Rebuild it with <april:card>, <april:button>, <april:dialog> and the input components. That exercises the three things you need to learn: slot composition, class merging, and Alpine scope. Where to look first: the published preview/docs site for the component list, then vendor/yungifez/april-ui/resources/views/components/ for the actual markup — the source is the real documentation for anything the docs site skips.
Compose with slots, not with named-slot soup. The package's own convention is worth adopting in your wrappers: default slot for the primary content (<april:button>Save</april:button>), a named slot for one auxiliary region (title, footer), and dedicated child components for collections. Anything repeated — sidebar menu items, list rows — should be child components composed through the parent's default slot, not a named slot holding a blob of markup. This keeps your own abstractions aligned with the library's.
Never edit vendor views; override via classes. Attribute forwarding plus twMerge means <april:button class="w-full bg-emerald-600"> should win over the component's defaults. Prefer this over publishing views, because published views become your maintenance burden on every upgrade. If you find yourself wanting to publish three or four views to change paddings, you probably want a thin wrapper component in resources/views/components/ instead.
Treat public Alpine methods as the API, and events as the safer API. The components expose Alpine data factories with init and public methods. You can call them from your own x-data, but Alpine resolves bare method names to the nearest enclosing scope, which makes cross-component calls fragile in nested layouts. For anything programmatic — open a dialog after a fetch, close a sheet on save — prefer $dispatch from your code and let the component listen, or use component-specific method names rather than generic ones.
Livewire works, with the usual caveats. These are Blade + Alpine components, so wire:model, wire:click, and wire:loading all compose fine. Use wire:key on any component rendered inside a loop, otherwise Alpine state gets reused across rows and you'll see dropdowns opening on the wrong item. For dialogs driven by Livewire validation, keep the dialog's open state in Alpine and let the server response drive it via a dispatched event rather than re-rendering the whole modal.
Tables own the query, but you own the defaults. The table component layers user-chosen sorting on top of an Eloquent/query builder you hand it. As of v1.3.6 the user's chosen sort is applied before the builder's own orderBy, so a ->orderBy('name') you set up as a sensible default no longer silently defeats a column click. Keep writing that default order — it's the fallback for the initial page load — but stop reaching for reorder()/removing orderBy as a workaround on v1.3.5 and below.
Theming happens at the token layer. shadcn-inspired libraries typically define semantic CSS variables (--background, --foreground, --primary) and reference them from Tailwind. Set those in your app stylesheet rather than overriding individual components. Same for dark mode: configure the strategy once, in one place, and confirm it matches what the package expects.
Layout components (sidebar) are the exception to local scoping. The docs call this out explicitly: the sidebar reader can place its trigger, rail, and menu buttons anywhere on the page, so everything goes through a shared receiver. Practically, that means your trigger can live in a topbar far from the sidebar markup. Don't assume proximity.
Tailwind not scanning vendor views is the #1 failure mode. Symptoms: layout is right but colors/spacing are flat. Fix the content/@source config, not the component.
Asset tags used to be baked in at view compile time — fixed in v1.3.6. If you're pinned at v1.3.5 or below, the <script>/<link> tags emitted by the package were captured when the compiled view was generated, so republishing assets, bumping the package, or changing asset config could leave you serving stale bundles until you ran php artisan view:clear. That was a genuinely confusing failure mode: correct files on disk, correct config, browser still loading the old hash. v1.3.6 builds the tags per request, so upgrading makes that class of bug go away — but clearing compiled views after any asset change is still a cheap habit to keep.
Class overrides may land on the wrapper, not the control. If class="h-9" on an input does nothing, inspect the DOM — attribute forwarding sometimes targets the outer element. Reach for class on the inner element via a published view only as a last resort.
twMerge doesn't resolve everything. Conflicting utilities merge; arbitrary values, prose classes, and custom plugin utilities often don't. When an override silently loses, stop guessing and check the rendered class list.
Nested interactive components are the sharpest edge. Dropdown inside dialog inside sidebar means many Alpine scopes resolving the same short names. The library names its own methods specifically to avoid this (closeDropdownMenu vs closeSub), and you should do the same in your wrappers. If the wrong thing closes, you've hit scope resolution, not a bug in the package.
Optional dependencies load per page. The rich-text editor (TipTap) is the example: it's loaded only on pages that use it. If the component renders but the toolbar is dead, check the network tab for the missing bundle before reading component source.
Pin the version and re-check after upgrades. Release Please + conventional commits means fix:/feat: land in patch/minor releases frequently — v1.3.6 is a two-line patch that nevertheless changes the precedence of your table's orderBy and the timing of asset tag generation. Pin to a tag, read the changelog on bump, and re-run your visual smoke test — class names and Alpine method names are the things most likely to shift, and sort/ordering semantics are the thing most likely to change quietly.
Budget for reading source. At ~365 stars and an active release cadence, this is a young library: the docs site lags the code, and component APIs will move. vendor/yungifez/april-ui/resources/views/ and the matching JS files under the package's resources/js/ are the ground truth. Ten minutes there beats an hour of guessing.
Debugging tip: install the Alpine DevTools extension and inspect the component root's data. You'll immediately see which scope owns the method being called and what state is actually set — that resolves most "the dropdown won't open" reports faster than any amount of Blade spelunking.
How can I help you explore Laravel packages today?