mitopp/bootstrap-twig-components-bundle
A Symfony bundle that turns Bootstrap UI elements into reusable Twig components. Build buttons, alerts, modals, forms, and more with clean, expressive tags instead of repetitive HTML, keeping templates consistent, readable, and easy to maintain across your projects.
1. Install and register
composer require mitopp/bootstrap-twig-components-bundle
There is almost certainly no Flex recipe for a 0-star bundle, so register it manually in config/bundles.php:
return [
// ...
Mitopp\BootstrapTwigComponentsBundle\MitoppBootstrapTwigComponentsBundle::class => ['all' => true],
];
2. Load Bootstrap itself. The bundle ships markup, not CSS/JS. Without Bootstrap 5 CSS in your layout the components will render unstyled <div>s, and interactive ones (modal, dropdown, collapse) will do nothing:
<link href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css" rel="stylesheet">
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/js/bootstrap.bundle.min.js" defer></script>
3. Where to look first. Spend ten minutes in vendor/mitopp/bootstrap-twig-components-bundle/ before writing any code:
README.md — the actual component list and call signatures (treat this as the source of truth, not this assessment)src/Twig/ — the AbstractExtension subclasses; each public method is a Twig function/filter nametemplates/ or Resources/views/ — the Twig partials behind each component; this tells you exactly which Bootstrap version and classes are targetedDependencyInjection/ — config tree, i.e. the mitopp_bootstrap_twig_components: key and its optionstests/ — the most reliable usage examples you will get4. First use case. The intended day-to-day win is replacing repeated bootstrap boilerplate with one call. Typical shape:
{# before #}
<div class="alert alert-warning alert-dismissible fade show" role="alert">
Your trial expires in 3 days.
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
</div>
{# after #}
{{ bs_alert('Your trial expires in 3 days.', { variant: 'warning', dismissible: true }) }}
Confirm the real function name with bin/console debug:twig — that command lists every function, filter and test the bundle registered, and is the fastest way to discover the API.
Wrap, don't scatter. Don't call bundle functions directly from 40 templates. Add one thin layer of your own so a Bootstrap major-version bump or a bundle swap is a one-file change:
{# templates/components/_btn.html.twig #}
{{ bs_button(label, {
variant: variant|default('primary'),
size: size|default(null),
attributes: attributes|default({})
}) }}
Compose atoms into your own components. The bundle gives you buttons/alerts/badges/cards/icons; your app gives you <twig:ProductCard> or a product_card.html.twig macro that consumes them. Keep business logic out of the bundle's props — pass already-computed values.
Icons (new in v0.4.0). The Icon component is the newest and least battle-tested surface, and also the one you'll touch most, so put it behind your own wrapper immediately rather than calling it in hundreds of places:
{# templates/components/_icon.html.twig #}
{{ bs_icon(name, { size: size|default(null), class: class|default(null) }) }}
Two things to settle once, globally, before you use it anywhere: (a) open the partial and confirm whether it emits an inline <svg> or a font-based <i class="bi bi-*"> — the former needs no extra assets, the latter needs the Bootstrap Icons CSS/font shipped; (b) check the DependencyInjection/ tree for a default icon-set or prefix option and set it in config/packages/ instead of repeating it per call. Then use it to fill the icon-only buttons you already have:
{{ bs_button(bs_icon('trash'), {
variant: 'danger',
attributes: { 'aria-label': 'Delete' }
}) }}
Interactive components need a lifecycle owner. Modals, dropdowns, offcanvas and collapse are inert markup plus data-bs-* attributes. Two workable integrations:
data-bs-toggle="modal" data-bs-target="#confirm-delete". Fill content via a Turbo/Stimulus controller or a small fetch to a fragment route.new bootstrap.Modal(el) so you control show/hide and cleanup after Turbo navigation (otherwise backdrops stick around).Forms. Point Symfony's form theme at the bundle's markup where sensible, then render widgets inside components:
{{ bs_card({
title: 'Sign in',
body: form_start(form) ~ form_widget(form) ~ bs_button('Log in', {variant: 'primary'})
}) }}
Be careful: if the bundle escapes input, concatenating pre-rendered form_* HTML will need |raw on your side, or an explicit html prop.
Layouts and page chrome. Use nav/navbar/breadcrumb components in base.html.twig and section templates; pass the active route in from the controller or from app.request.attributes.get('_route'). Icons work well here too — a bs_icon('chevron-right') inside a breadcrumb separator beats hand-written markup.
Override, don't fork. Symfony resolves bundle templates from templates/bundles/<BundleName>/…. Copy the specific partial you want to change into that path, adjust the markup (e.g. add aria-*, swap a class for your own utility, or normalise the icon partial to your chosen icon set), and let the rest of the bundle stay upgradeable. Prefer this over vendoring.
Testing. Add one Twig render test per component you actually use, and one specifically for the icon output format since that's the newest code:
$html = $this->renderView('components/_btn.html.twig', ['label' => 'Save', 'variant' => 'danger']);
$this->assertStringContainsString('btn-danger', $html);
$icon = $this->renderView('components/_icon.html.twig', ['name' => 'check']);
$this->assertMatchesRegularExpression('/<svg|bi bi-check/', $icon);
These catch silent breakage on minor upgrades, which is the main risk with a low-adoption dependency.
Treat the dependency as unmaintained-by-default. Zero stars and zero dependents means no community triage, no security advisories, no upgrade path. At v0.4.0 you are also firmly pre-1.0, so any minor bump may break you — pin a tight constraint ("mitopp/bootstrap-twig-components-bundle": "0.4.*" or a commit hash), commit composer.lock, and read the source of every component you adopt before shipping it. If the markup is thin — and it usually is — consider copying the templates into your app instead of depending on the package at all; you lose nothing but composer update surprises.
The Icon component is the new arrival — verify it twice. Release notes claim docs, tests and a template landed, and the test suite is your best reference for the exact signature (name? set? size? class? attributes?). Check specifically: does it sanitise or pass through the icon name (a passed-through name interpolated into a class attribute is an injection shape if it ever reaches |raw)? Does it hardcode bi-prefixed classes, locking you to Bootstrap Icons even if you use Font Awesome? Does it accept an aria-*/title attribute for meaningful icons? Anything you don't like, patch via the template override path — not by editing vendor/.
Bootstrap 4 vs 5 mismatch is the #1 failure mode. The package name says "Bootstrap" without a version. Check the templates for data-toggle (v4) vs data-bs-toggle (v5), form-group/input-group-append (v4) vs input-group-text (v5), ml-*/mr-* (v4) vs ms-*/me-* (v5). Mixed output against your asset version produces components that render but don't respond to clicks.
Attribute escaping. Helper functions that accept arbitrary attribute arrays are a stored-XSS vector if you pass user-controlled values as attribute names. Pass user data only as content, and rely on Twig autoescaping — never |raw a value you didn't construct.
Class merging. Most implementations append attributes.class to a default class list, but some overwrite. Verify with a quick render (bs_button('x', {attributes: {class: 'w-100'}})) and confirm you get btn btn-primary w-100, not just w-100. This is worth re-checking on the new Icon component too, since it was written after the rest.
Debugging checklist:
bin/console debug:twig | grep -i bs_ # list registered functions/filters, incl. the new icon one
bin/console debug:container --tag=twig.extension
bin/console cache:clear && rm -rf var/cache/dev/twig # after overriding templates
If a component outputs nothing, it's almost always (a) bundle not registered, (b) wrong function name, or (c) a null required prop silently swallowed. For icons specifically, "renders nothing" usually means either an unknown icon name or a missing icon font/CSS rather than a wiring problem.
Configuration quirks. Check whether the config key matches the bundle name (mitopp_bootstrap_twig_components) and whether it defaults things like icon set, CSS framework version, or default button variant. Set those globally in config/packages/ rather than repeating per-call arguments.
Extension points worth knowing: decorate the bundle's Twig extension service to add your own defaults, register a global (twig.globals) for variant maps or an icon-name allowlist, or override templates via the templates/bundles/ path. Avoid subclassing the extension — service decoration survives updates better.
Accessibility is on you. Community Bootstrap component wrappers frequently omit aria-label on icon-only buttons, role="alert" on alerts, and aria-expanded on toggles. The Icon component makes this worse by default, because it becomes trivial to build a button whose entire label is a decorative <svg> with no accessible name — always pair it with aria-label or visually-hidden text. Audit the emitted markup once with your browser devtools and patch via template override.
Don't expect JavaScript, translations, or bundled assets. No Stimulus controllers, no trans filters. And although v0.4.0 adds icons, nothing in the release notes says it ships the Bootstrap Icons stylesheet or font files — assume it doesn't, and load them yourself if the icon partial turns out to be font-based. If you need localisation or JS behaviour, layer it in your own wrapper components — that's the natural place for your team's conventions anyway.
How can I help you explore Laravel packages today?