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

Ux Extras Laravel Package

krystal-sf/ux-extras

krystal-sf/ux-extras provides practical UX helpers and UI enhancements for Laravel/PHP applications, helping teams build polished, consistent interfaces faster with reusable components and less custom code. It streamlines common interface tasks while keeping the developer experience simple.

View on GitHub
Deep Wiki
Context7

Getting Started

Reality check first. Despite the PHP/Laravel catalogue tagging, krystal-sf/ux-extras is a Symfony UX package ("Extra UX Components for Krystal Symfony Projects"). There is no Laravel service provider, no auto-discovery, no config publish tag, and no facade. composer require in a Laravel app will either fail or — worse — install Symfony UX transitive dependencies that register nothing and do nothing. Treat the repository as a source of portable assets and patterns, not as a dependency.

What is actually usable from a Laravel 10/11 + Blade + Alpine/Livewire + Vite stack:

Layer Portable?
Twig components / form themes No — read as reference, rewrite in Blade
Symfony bundle wiring, Twig extensions No
Stimulus controllers (assets/controllers/) Yes — register via your own Stimulus bootstrap
CSS / design tokens Yes — vendor into resources/css/

Where to look first, in order:

  1. LICENSE — resolve before you copy a single line. Krystal is a hosting provider; this reads as internal tooling published publicly. If the license is unclear or restricts internal commercial reuse, stop.
  2. assets/controllers/ — this is the money. Inventory every controller, note which ones are non-trivial (modals, toasts, date pickers, autocomplete) vs thin wrappers.
  3. assets/styles/ or src/Resources/public/ — grab tokens and base component CSS.
  4. src/ — skim the Twig components purely to understand behaviour contracts (what markup they expect, what events they emit). Do not try to port the PHP.
  5. Last commit date and tagged releases — a single branch dump is a different risk profile than a tagged release.

First use case — port one controller, end to end. Pick the highest-value, lowest-dependency controller (typically a disclosure/dropdown/modal). Copy it into resources/js/controllers/, register it with your Stimulus bootstrap, and drive it from a Blade component. This validates the entire pipeline (Vite aliasing, Stimulus version, markup contract) in half a day.

git clone https://gitlab.com/krystal-sf/ux-extras.git /tmp/ux-extras
cd /tmp/ux-extras && git rev-parse HEAD   # record this SHA
cp assets/controllers/modal_controller.js resources/js/controllers/
cp -r assets/styles/tokens.css resources/css/vendor/ux-extras/
// resources/js/bootstrap.js
import { Application } from '@hotwired/stimulus'
const app = Application.start()
app.register('modal', (await import('./controllers/modal_controller.js')).default)
{{-- resources/views/components/modal.blade.php --}}
<div data-controller="modal" data-modal-open-value="{{ $open ? 'true' : 'false' }}">
    <button data-action="modal#open">{{ $trigger }}</button>
    <dialog data-modal-target="dialog">{{ $slot }}</dialog>
</div>

If it doesn't work inside an hour, that's your signal — the payoff is too small relative to port cost.

Implementation Patterns

Vendor-and-fork, never depend. The default pattern is: fork the repo into your own namespace (or straight copy into resources/), record the upstream SHA in a VENDORED.md so you stay diffable against upstream, and own the code from day one. Do not add it to composer.json.

resources/js/controllers/vendor/ux-extras/   # copied Stimulus controllers
resources/css/vendor/ux-extras/              # copied CSS/tokens
resources/views/components/ux/               # your Blade wrappers
VENDORED.md                                  # upstream URL + commit SHA + ported files

Blade wrapper over Stimulus controller. Keep the Stimulus controller as the behaviour and the Blade component as the API surface. This lets you swap or rewrite the controller later without touching product templates.

// app/View/Components/Ux/Alert.php
class Alert extends Component
{
    public function __construct(
        public string $type = 'info',
        public bool $dismissible = true,
    ) {}
}
{{-- resources/views/components/ux/alert.blade.php --}}
<div
    role="alert"
    @class(['ux-alert', "ux-alert--{$type}"])
    @if($dismissible) data-controller="ux-dismiss" @endif
>
    {{ $slot }}
    @if($dismissible)
        <button data-action="ux-dismiss#dismiss" aria-label="Dismiss">&times;</button>
    @endif
</div>

Keep controllers individually importable. Don't build a single bundle. Each controller should be its own module so unused components don't inflate the Vite build. Use Vite's glob import to register only what you actually ship:

// resources/js/stimulus.js
import { Application } from '@hotwired/stimulus'
const app = Application.start()

const controllers = import.meta.glob('./controllers/**/*_controller.js', { eager: true })
for (const [path, mod] of Object.entries(controllers)) {
    const name = path.match(/([^/]+)_controller\.js$/)[1]
    app.register(name, mod.default)
}

Translating a Twig component as reference. When a component is worth porting, work backwards from the Twig template to understand the contract, then rebuild natively. Translate:

  • <twig:Component attr="x"> → <x-component attr="x">
  • Twig {% block %} → Blade {{ $slot }} / named slots via {{ $header }}
  • Twig {{ prop }} → Blade {{ $prop }}
  • Twig filters like |trans → {{ __($key) }}
  • Twig form themes → don't try. Symfony form theming is bound to Symfony's FormView object. Rewrite as plain Blade partials driven by Laravel's validation error bag.

Design tokens as a thin layer. Vendor the CSS custom properties into resources/css/vendor/ux-extras/tokens.css and import them once in app.css. Map them into Tailwind's theme config if you use Tailwind, so your components and the vendored tokens share one source of truth.

// tailwind.config.js
theme: {
    extend: {
        colors: {
            primary: 'var(--ux-color-primary)',
            surface: 'var(--ux-color-surface)',
        },
    },
},

Isolate behind your own namespace. Everything you port lives under app/View/Components/Ux/ and resources/js/controllers/vendor/. Product code never imports a ux-extras path. When you eventually replace a controller with an Alpine equivalent (which is often the right end state in a Laravel shop), the swap is a one-line change in the Blade component.

Stimulus vs Alpine decision. You already have Alpine/Livewire. Porting a Stimulus controller only makes sense when the behaviour is genuinely non-trivial (focus traps, drag-and-drop, complex keyboard navigation). For anything Alpine can express in ten lines, rewrite it in Alpine and delete the port. This is the highest-leverage judgment call in the whole exercise.

Gotchas and Tips

Do not composer require this. No Laravel service provider means no auto-registration; worse, Symfony UX transitive dependencies (symfony/ux-twig-component, symfony/stimulus-bundle) assume a Symfony kernel and an AssetMapper or Encore asset pipeline. In the best case they do nothing; in the worst they confuse your Vite build. The default Laravel install path is not viable — vendor the JS/CSS and move on.

Silent no-op is the primary failure mode. A team adds the package, assumes components are live, and nothing registers. Always verify by inspecting the built JS bundle (npm run build && grep modal public/build/) or the network tab. If the controller isn't in the bundle, nothing is happening.

Importmap vs Vite bare specifiers. If the package uses importmap-style specifiers (import x from 'stimulus' without ./), Vite needs an alias — resolve.alias: { stimulus: '@hotwired/stimulus' } — or you'll get a "failed to resolve" at build time. This is the most common port failure and it doesn't show up until you build.

Stimulus 2 vs 3 API drift. Older controllers use this.data.get('x'), static targets = [] with data-target; newer ones use this.xValue, static values = {} with data-x-value. Check the package's package.json for the pinned Stimulus version before porting, and confirm your app matches. Mixing v2-written controllers into a v3 app produces confusing "target not found" errors.

License ambiguity is a hard blocker. Krystal is a hosting provider and this is their internal tooling. Resolve the license before copying code into a commercial codebase — redistributing or open-sourcing your fork can be blocked by an ambiguous license. If you can't identify the license in the first five minutes, the evaluation is over.

Upstream Symfony/Twig bumps will not break your fork — but they'll also not help you. Once you vendor the JS/CSS, upstream churn only matters when you want new components from the package. Track the SHA quarterly if you expect to pull more; ignore it otherwise. There is no upgrade path.

Twig form themes are a trap. Of everything in the package, Symfony form themes look the most reusable and are the least portable. They're tied to FormView and form_widget internals. Budget zero time on them; rebuild your input/select/textarea error states directly against Laravel's $errors bag.

Debugging checklist when a ported controller misbehaves:

  1. php artisan view:clear and npm run dev — cached Blade + stale Vite is the usual culprit.
  2. Inspect the element — is data-controller="name" present? Is data-action on the correct child?
  3. console.log(app.controllers) in your Stimulus bootstrap — is the controller registered?
  4. Check the controller's static targets / static values against your Blade markup — attribute names must match exactly (data-{identifier}-target vs data-{identifier}-{name}-value).
  5. Only then suspect Stimulus version mismatch.

Extension points are yours to invent. The package has no plugin API, no event bus, no config schema you can rely on across versions. If you need customisation hooks, add them to your Blade wrapper and your own copy of the controller. That's the point of vendoring — you gain full extension capacity at the cost of owning the diff.

Scope the effort and cap it. Zero stars, zero dependents, no community, no SLA. This is orphan code the moment you touch it. Budget a one-to-two-day audit (LICENSE → controller inventory → port/rewrite/discard classification) followed by a single three-to-five-day spike on one component. If the spike reveals that the controller is a thin wrapper over a library you could adopt directly (most are — the wrapper is the least valuable part), adopt the library instead. If it reveals genuine complexity you'd otherwise rebuild badly, vendor that one component and stop.

Build-in-house is the stronger default in a Laravel shop. Blade + Alpine + Livewire cover the overwhelming majority of what "UX extras" packages provide, and first-party kits (Filament, Livewire's own components, Blade UI Kit) are maintained against a stack you actually run. krystal-sf/ux-extras clears the bar only when a specific component — usually an interaction-heavy one like a focus-trapping modal or a keyboard-navigable combobox — is genuinely non-trivial to reproduce and worth the ownership cost.

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.
microsoft/fluentui-system-icons
mmccook/php-json-canonicalization-scheme
middlewares/firewall
qubiqx/montapacking
nyoncode/laravel-package-console
pnscripts/filament-broadquill-lite
pnscripts/laravel-sentfold-lite
vleap/mx-sdk-laravel-internal
twidpay/utils
htetmyathlaing/scout-elasticsearch-driver
artisan-toolbox/front-awesome
ls/client-assistant
users-au/laravel-client
cgv/localized-resource
noith/settings
kda/laravel-package
kda/filament-dynamic-navigation
bypersoft/signbox
pairus/product-data
fayne/laravel-filament-cms-core