laravel-frontend-presets/bulma
A Laravel frontend preset that replaces the default scaffolding with Bulma, providing ready-made auth views, layouts, and styling so you can quickly build responsive Laravel applications using the Bulma CSS framework.
Treat this as a starter kit you copy once, not a long-lived dependency — with one important update. v3.0.4 ("Fixed Preset Command dependency") addresses the single biggest problem in earlier releases: the package's PresetCommand previously leaned on framework wiring that disappeared when php artisan preset was removed in Laravel 7. The package now ships the command dependency itself, so php artisan preset bulma is expected to work on modern Laravel again.
Verify before trusting it — this is a one-line release note, not a changelog:
composer require laravel-frontend-presets/bulma:^3.0 --dev
php artisan list | grep -i preset # should now show `preset`
Install (Laravel 5.5–6.x):
composer require laravel-frontend-presets/bulma --dev
php artisan preset bulma
npm install && npm run dev
Install (Laravel 7+, v3.0.4+):
composer require laravel-frontend-presets/bulma --dev
php artisan preset bulma
npm install && npm run dev
If the command still doesn't register (stale composer.lock, package discovery cache, or an older tag resolved), fall back to copying stubs:
php artisan vendor:publish --provider="LaravelFrontendPresets\BulmaPreset\BulmaPresetServiceProvider" 2>/dev/null
# or hand-copy from the repo's resources/ tree
npm install -D bulma sass
Then in resources/sass/app.scss (or resources/css/app.css on Vite-based apps):
@import 'bulma/bulma';
Where to look first after running the preset:
| File | What changed |
|---|---|
webpack.mix.js |
Sass entry point, removed Bootstrap bootstrap.js bits |
package.json |
bulma, sass, laravel-mix deps |
resources/sass/app.scss |
@import '~bulma/bulma.sass' replaces Bootstrap |
resources/views/layouts/app.blade.php |
Navbar + container markup |
resources/views/auth/*.blade.php |
Login/register/passwords with field, control, input, button is-primary |
First use case: scaffold auth, then restyle one CRUD screen. The preset no longer generates the make:auth wiring, so install laravel/ui first (php artisan ui:auth --views) or use Breeze, then let the preset replace the views. Convert your own index.blade.php to a table is-fullwidth is-striped with a Bulma pagination view. That single pass exercises every part of the preset you'll actually reuse.
Bulma variables use !default, so override them before the import — the one-liner @import 'bulma/bulma' makes that impossible, so switch to the split import:
// resources/sass/app.scss
@import '~bulma/sass/utilities/_all';
$primary: #3d5afe;
$family-sans-serif: 'Inter', system-ui, sans-serif;
$navbar-item-hover-background-color: transparent;
@import '~bulma/bulma';
This is the single highest-value reason to keep the preset's Sass wiring.
Keep the generated layouts/app.blade.php but break out the chunks you'll edit constantly:
{{-- resources/views/layouts/app.blade.php --}}
<body>
@include('layouts.partials.nav')
<section class="section">
<div class="container">
@include('layouts.partials.flash')
@yield('content')
</div>
</section>
</body>
{{-- resources/views/layouts/partials/flash.blade.php --}}
@foreach (['success' => 'is-success', 'error' => 'is-danger', 'warning' => 'is-warning'] as $key => $class)
@if (session($key))
<div class="notification {{ $class }}">
<button class="delete" data-dismiss="notification"></button>
{{ session($key) }}
</div>
@endif
@endforeach
The preset gives you markup, not encapsulation. Wrap it once so validation styling is automatic:
{{-- resources/views/components/field.blade.php --}}
@props(['name', 'label' => null, 'type' => 'text'])
<div class="field">
<label class="label" for="{{ $name }}">{{ $label ?? Str::headline($name) }}</label>
<div class="control">
<input
id="{{ $name }}"
name="{{ $name }}"
type="{{ $type }}"
class="input @error($name) is-danger @enderror"
{{ $attributes->merge(['value' => old($name)]) }}>
</div>
@error($name)
<p class="help is-danger">{{ $message }}</p>
@enderror
</div>
Usage: <x-field name="email" type="email" />. This is the pattern that survives when the preset itself doesn't.
Laravel's default paginator view is Tailwind (8+) or Bootstrap-flavoured. Point it at a Bulma view once in a service provider:
// app/Providers/AppServiceProvider.php
use Illuminate\Pagination\Paginator;
public function boot(): void
{
Paginator::defaultView('vendor.pagination.bulma');
Paginator::defaultSimpleView('vendor.pagination.bulma-simple');
}
{{-- resources/views/vendor/pagination/bulma.blade.php --}}
@if ($paginator->hasPages())
<nav class="pagination is-small" role="navigation">
{{-- Previous --}}
@if ($paginator->onFirstPage())
<a class="pagination-previous" disabled>Previous</a>
@else
<a class="pagination-previous" href="{{ $paginator->previousPageUrl() }}">Previous</a>
@endif
{{-- Next --}}
@if ($paginator->hasMorePages())
<a class="pagination-next" href="{{ $paginator->nextPageUrl() }}">Next</a>
@else
<a class="pagination-next" disabled>Next</a>
@endif
<ul class="pagination-list">
@foreach ($elements as $element)
@if (is_string($element))
<li><span class="pagination-ellipsis">…</span></li>
@endif
@if (is_array($element))
@foreach ($element as $page => $url)
<li>
<a class="pagination-link @if ($page == $paginator->currentPage()) is-current @endif"
href="{{ $url }}">{{ $page }}</a>
</li>
@endforeach
@endif
@endforeach
</ul>
</nav>
@endif
Nothing in the preset toggles the navbar burger, modals, dropdowns, or tabs. Small vanilla script in resources/js/app.js:
document.querySelectorAll('[data-bulma-toggle]').forEach((el) => {
el.addEventListener('click', () => {
const target = document.getElementById(el.dataset.bulmaToggle);
target?.classList.toggle('is-active');
el.classList.toggle('is-active');
});
});
with <button class="navbar-burger" data-bulma-toggle="nav-menu">. Same hook pattern works for .modal (toggle is-active) and .dropdown.
Bulma and Alpine compose cleanly — Alpine just flips the is-active classes:
<div class="dropdown" x-data="{ open: false }" :class="{ 'is-active': open }">
<div class="dropdown-trigger">
<button class="button" @click="open = !open" @click.outside="open = false">Actions</button>
</div>
<div class="dropdown-menu"><!-- ... --></div>
</div>
For Livewire, keep wire:model on the raw input and put Bulma classes on the same element; Livewire doesn't care about surrounding markup.
Bulma unminified is ~200–300 KB. Gate PurgeCSS behind production in Mix:
mix.sass('resources/sass/app.scss', 'public/css')
.options({ purgeCss: mix.inProduction() ? { enabled: true, content: ['resources/views/**/*.blade.php', 'resources/js/**/*.js'] } : false });
Note PurgeCSS must be told about dynamically composed classes (is-$variant, is-$size); add a safelist if you build classes at runtime.
package.json, and webpack.mix.js are still the 2020 originals. Confirm the command shows up with php artisan list | grep preset; if it doesn't, check you resolved ^3.0 and not a cached older tag (composer show laravel-frontend-presets/bulma).preset is still missing after upgrading, run composer dump-autoload and php artisan package:discover. The fix is a dependency on the command class, so a stale provider manifest is the usual culprit.webpack.mix.js, package.json, app.js, app.scss, and auth views get clobbered. Commit or branch before running it. php artisan preset none no longer exists on modern Laravel, so git checkout is your undo.resources/assets/sass; 5.7+ uses resources/sass; 9+ uses resources/css + resources/js with Vite. Stubs written for the older layout will compile into the wrong place or not at all. On Vite apps expect to hand-rewrite webpack.mix.js as vite.config.js input.~ tilde resolution is Mix-only. @import '~bulma/bulma' works in webpack/Mix; with Vite use @import 'bulma/bulma' and let the resolver handle node_modules, or add resolve.alias.node-sass vs sass. The package's package.json may still pin node-sass, which won't build on modern Node. Swap to sass (Dart Sass) and expect deprecation warnings for @import — Bulma 1.0 moved to @use, so pin bulma@^0.9 if you're following the preset's stubs.$primary after @import 'bulma/bulma' does nothing because every variable has !default. Always split the utilities import.<span class="icon"><i class="fas fa-user"></i></span> renders an empty box until you add @fortawesome/fontawesome-free (or inline SVG). This is the most common "the preset is broken" report.auth/*.blade.php stubs assume make:auth routes. Generate with laravel/ui (php artisan ui:auth --views) or Breeze first, then port the Bulma class names onto the generated views; the route/controller wiring comes from the scaffolding, not from this package.PresetCommand resolves preset classes under the LaravelFrontendPresets\ namespace. Subclass the shipped preset and register your own to add files to the copy set — useful for org-wide defaults (logo, favicon, base layout, flash partial) applied across projects. Now that the command registers reliably on 7+, this is a much more viable pattern than it was.npm run dev and read the first error; Mix reports the offending @import path, which is almost always a stale resources/assets/... path or a missing ~. rm -rf node_modules package-lock.json && npm install fixes most version-pin drift from the 2020 manifest.devDependencies era pins, no JS, no icons, ~170 stars. Use it as a one-shot preset or a reference implementation; don't expect Bulma 1.0/Vite-native stubs to arrive.How can I help you explore Laravel packages today?