zicht/htmldev-bundle
A Symfony bundle from Zicht that adds HTML development tooling to your app, providing helpers and debug utilities to streamline template work and speed up front-end iteration.
The bundle is a Symfony integration for Zicht's "living styleguide" tooling: it renders Twig partials (patterns) with sample data in a dev-only route so you can develop markup and CSS in isolation from the application.
Install as a dev dependency
composer require --dev zicht/htmldev-bundle
Treat it as dev tooling. If it is only in require-dev, CI jobs that render the styleguide need composer install without --no-dev.
Register the bundle for dev/test only
Symfony 4+ (config/bundles.php):
return [
// ...
Zicht\HtmlDevBundle\ZichtHtmlDevBundle::class => ['dev' => true, 'test' => true],
];
On older Symfony, add it to registerBundles() inside the if (in_array($this->getEnvironment(), ['dev', 'test'])) block of AppKernel.
Import the routes in the dev routing config only
config/routes/dev/htmldev.yaml:
zicht_htmldev:
resource: "@ZichtHtmlDevBundle/Resources/config/routing.xml"
prefix: /styleguide
Keeping this in config/routes/dev/ means the styleguide does not exist in production at all.
Configure template paths and the base layout
Inspect the available options rather than guessing:
bin/console config:dump-reference zicht_html_dev
Typically you point the bundle at one or more Twig paths containing your patterns and at a base template (the layout the styleguide page extends). Configure it as a Twig namespace as well if your patterns use @patterns/... includes.
Create your first pattern
Put a partial and its sample data next to each other, e.g. templates/patterns/button/button.html.twig plus whatever data file/definition the bundle expects for that pattern. Then add it to the styleguide index and visit http://localhost:8000/styleguide in the dev environment.
First real use case
Take an existing button/card/alert that is currently inlined in a page template, move the markup into templates/patterns/..., define the data variants you actually use (primary/secondary, with/without icon, long labels), and verify them in the styleguide. Then replace the inline markup in the application template with an {% include %}/{% embed %} so the styleguide and the app render the same file.
Where to look first: the bundle README and Resources/config/ (routing and service definitions), config:dump-reference, and the bundle's own templates under vendor/zicht/htmldev-bundle/Resources/views/ to see how patterns are discovered and rendered. Because the package has minimal adoption, read the source early — documentation is likely thin.
Single source of truth in templates/patterns/
The most valuable workflow is not "a styleguide next to the app" but "the styleguide renders the app's own partials." Structure:
templates/
patterns/
button/button.html.twig
card/card.html.twig
card/card--compact.html.twig
page/home.html.twig {# includes @patterns/card/card.html.twig #}
Register templates/patterns as a Twig namespace so the app can include patterns without brittle relative paths:
twig:
paths:
'%kernel.project_dir%/templates/patterns': patterns
Then both the styleguide and the app use {% include '@patterns/card/card.html.twig' with { card: card } %}.
Variants as data, not duplicated markup
Define a base template and drive variants from sample data (state, size, modifiers). Keep variant definitions declarative so a designer can add a state without touching Twig control flow. If the bundle uses per-pattern data files, keep them next to the template and name them consistently — predictability matters more than the specific convention.
Use embed and macros for composable patterns
For patterns with slots, prefer {% embed %} over include with concatenated HTML strings:
{% embed '@patterns/card/card.html.twig' with { card: card } %}
{% block card_footer %}
{{ include('@patterns/button/button.html.twig', { label: 'Read more' }) }}
{% endblock %}
{% endembed %}
Macros are the cleanest option for leaf components (buttons, icons, badges) because they take typed-looking arguments and have no output escaping surprises around whitespace.
Sample data should look like production data
If patterns accept entities or view models, define styleguide-only fixture objects (or use Doctrine fixtures/factories) instead of raw arrays. This catches the common failure where the styleguide renders {{ user.name }} against a plain array but the real entity exposes getName() differently. Keep the fixtures in a dedicated namespace, e.g. App\Styleguide\Fixtures, and never let production services depend on them.
Base layout hygiene
Point the bundle at a styleguide-specific base template that loads the same CSS/JS as the app (via encore_entry_link_tags/asset()) but skips analytics, CSRF, session-dependent chrome, and anything that requires a logged-in user. If you want the styleguide to sit inside the real chrome, use the app layout but stub out user-dependent blocks.
Navigation and grouping
Use subdirectories to create logical groups (forms, navigation, content). Order matters in a styleguide — name files with a numeric or alphabetical prefix if the bundle sorts by filename. A useful index makes the difference between a tool people use and one they forget.
Extending the styleguide page
If you need a custom index, global header, or extra panels, look for the bundle's controller/service definitions and either decorate the service or override the bundle's templates:
templates/bundles/ZichtHtmlDevBundle/
Override only the presentation; leave pattern discovery to the bundle.
CI smoke test
A living styleguide rots the moment a pattern breaks. Add a cheap functional test that requests the styleguide index (and, if discoverable, each pattern route) in the test environment:
public function testStyleguideRenders(): void
{
$client = static::createClient();
$client->request('GET', '/styleguide');
$this->assertResponseIsSuccessful();
}
This turns "we broke a partial" into a red build rather than a bug found on staging.
Keep it out of the container in production
Because the bundle is registered only in dev/test, production container compilation and autoloading are unaffected. Don't be tempted to enable it in prod "just for a quick look" — the styleguide renders arbitrary sample data and may expose internal markup.
composer why-not zicht/htmldev-bundle). Pin the version in composer.lock and be prepared to vendor/fork it. Check whether the bundle uses deprecated Symfony APIs before upgrading Symfony.config/bundles.php but importing routes unconditionally (or vice versa) gives confusing "service not found" or 404 errors. Both belong in dev/test.bin/console cache:clear (and check whether the bundle has its own cache warmer). In development, confirm you're editing the directory that is actually registered — a missing or mistyped Twig path is the most common cause of "empty styleguide."strict_variables. With strict variables enabled (common in dev), a pattern that renders fine in the app can explode in the styleguide because sample data is missing a key. This is a feature: it surfaces hidden assumptions. Fix the fixture, not the strictness.url() references in CSS and manually written <img src> paths can resolve differently from the app's routes. Always use asset() and your asset manifest, and make sure the dev build is compiled (yarn dev/npm run dev) before judging the styleguide.twig.paths are separate. An include that works via a filesystem path may fail via @patterns/... if the namespace isn't registered. Register namespaces explicitly and use them consistently everywhere./styleguide prefix can collide with an app route or a firewall rule. If you use an access-control rule for ^/, verify the dev route isn't blocked or redirected to a login page.{%- -%} deliberately and verify the pattern in the styleguide and in a real page; the surrounding layout can change rendering subtly.templates/bundles/ZichtHtmlDevBundle/ and service decoration over editing vendor/. If you need behavior the bundle doesn't expose, fork it into src/ or a small internal bundle — you'll need the fork option eventually given the lack of upstream maintenance.templates/patterns/ directory. Keeping patterns in plain Twig with namespaced includes (rather than in bundle-specific constructs) makes that replacement trivial.How can I help you explore Laravel packages today?