combat-ui / core-bundle
A bundle containing UI elements usable in Symfony that use the Combat UI framework
Package info
github.com/Combat-Marketing/combat-ui-symfony-bundle
Type:symfony-bundle
pkg:composer/combat-ui/core-bundle
Requires
- php: ~8.4.0 || ~8.5.0
- symfony/config: ^7.4 || ^8.0
- symfony/dependency-injection: ^7.4 || ^8.0
- symfony/http-kernel: ^7.4 || ^8.0
- symfony/twig-bundle: ^7.4 || ^8.0
- symfony/webpack-encore-bundle: ^2.1.1
- symfony/yaml: ^7.4 || ^8.0
- twig/twig: ^3.21
Requires (Dev)
- codeception/codeception: ^5.3.5
- codeception/module-asserts: ^3.2
- codeception/module-symfony: ^3.8
- codeception/stub: ^4.3
- phpstan/phpstan: 2.1.33
- phpstan/phpstan-symfony: ^2.0.9
- phpunit/phpunit: ^10.5 || ^11.5 || ^12.0
- symfony/framework-bundle: ^7.4 || ^8.0
- symfony/runtime: ^7.4 || ^8.0
README
The official Symfony integration for Combat UI — a lightweight, themeable front-end framework built on custom elements (<cui-button>, <cui-modal>, <cui-tabs>, …) and a utility-free CSS layer.
This bundle turns the framework into first-class Twig building blocks:
{% cui 'hero' with { eyebrow: 'New', title: 'Ship faster', lead: 'Combat UI in Twig.' } %}
{% cui_slot 'actions' %}
{% cui 'button' with { variant: 'primary', href: '/docs' } %}Read the docs{% endcui %}
{% endcui_slot %}
{% cui_slot 'media' %}<img src="/img/screenshot.png" alt="">{% endcui_slot %}
{% endcui %}
It is deliberately CMS-agnostic: it only needs Twig and Webpack Encore. CMS-specific integrations (such as the OpenDXP bundle) are thin wrappers on top of this one.
What you get
- A
{% cui %}Twig tag — render any Combat UI component, section, or layout by name, pass props withwith {…}, and fill slots with the tag body and nested{% cui_slot %}tags. - A
cui_component()function — the same renderer in expression form, handy for one-liners and dynamic component names. - A curated template library — 25+ ready-made Twig templates for components (cards, forms, modal, navbar, pagination, tabs, …), page sections (hero, CTA, page intro), and every-layout-style layout primitives (stack, cluster, grid, sidebar, switcher, cover, …). All of them overridable per-app via Symfony's standard bundle template override.
- Generic element rendering — any Combat UI custom element without a curated template still renders as a proper
<cui-*>tag with props mapped to attributes. cui_attrs()— a safe, expressive HTML attribute renderer (boolean attributes, conditional class lists, style maps,data-*expansion, full escaping).cui_assets()— one function that outputs the framework's CSS/JS tags from a prebuilt Webpack Encore build that ships with the bundle. Zero front-end tooling required to get started.cui_theme_script()— a tiny inline script that restores the visitor's light/dark preference before first paint, preventing theme flash.- Configurable component defaults — set project-wide default props per component in YAML.
Requirements
| Dependency | Version |
|---|---|
| PHP | 8.4 or 8.5 |
| Symfony | 7.4 or 8.x |
| Twig | ^3.21 |
| symfony/webpack-encore-bundle | ^2.1 |
Installation
composer require combat-ui/core-bundle
Register the bundle if you don't use Flex auto-registration:
// config/bundles.php return [ // ... CombatUI\Bundle\CoreBundle\CombatUICoreBundle::class => ['all' => true], ];
Publish the prebuilt assets:
php bin/console assets:install
Then load the framework in your base layout:
{# templates/base.html.twig #} <!DOCTYPE html> <html> <head> {{ cui_assets() }} </head> ... </html>
That's it — the bundle automatically registers a combat_ui Encore build pointing at its prebuilt assets (installed to public/bundles/combatuicore/build), so cui_assets() works out of the box without touching your own Encore setup.
Usage
The {% cui %} tag
Render a component by name. Props go in a with map; the tag body becomes the component's default slot:
{% cui 'button' with { variant: 'primary', href: '/signup' } %}
Sign up
{% endcui %}
Named slots are filled with the nested {% cui_slot %} tag:
{% cui 'hero' with { title: 'Launch' } %}
{% cui_slot 'media' %}<img src="/img/rocket.jpg" alt="">{% endcui_slot %}
<p>Everything outside a cui_slot lands in the default slot.</p>
{% endcui %}
The cui_component() function
The same renderer as an expression. A plain string as the third argument is escaped and used as the default slot:
{{ cui_component('button', { variant: 'primary' }, 'Click me') }}
{{ cui_component(card_type ~ '-card', item.props) }}
How names resolve
{% cui 'name' %} resolves the name in this order — first match wins:
@CombatUICore/components/<name>.html.twig— curated component wrappers@CombatUICore/sections/<name>.html.twig— page sections@CombatUICore/layouts/<name>.html.twig— layout primitives- A generic
<cui-name>element, if the name is a known Combat UI custom element — props become HTML attributes (snake_case→kebab-case), extras can be added via theattrprop.
An unknown name throws a clear RuntimeError listing the known elements. The cui- prefix is optional: 'cui-button' and 'button' are equivalent.
Because resolution goes through the @CombatUICore Twig namespace, any shipped template can be overridden the standard Symfony way — drop your version in:
templates/bundles/CombatUICoreBundle/components/button.html.twig
What's in the box
Components (templates/components/)
article-card, article-filter, button, case-card, contact-card, contact-methods, content-card, disclosure, event-card, feature-card, field, figure, form, itinerary-item, location-card, logo-item, media-card, media-full, media-overlay, modal, navbar, org-chart, pagination, person, stat, tabs, vacancy-card
Sections (templates/sections/)
hero, cta, page-intro, section
Layouts (templates/layouts/) — composable primitives in the spirit of Every Layout:
stack, cluster, grid, sidebar, split, switcher, center, cover, frame, container, prose
{% cui 'stack' with { gap: 'var(--cui-space-l)' } %}
{% cui 'grid' with { min: '18rem' } %}
{% for item in items %}
{% cui 'content-card' with item.props %}{% endcui %}
{% endfor %}
{% endcui %}
{% endcui %}
Generic custom elements — rendered without a curated wrapper:
calendar, carousel, code, cookie-banner, day-planner, map, reveal, scroll-stage, sidenav, theme-toggle, toast-region, tree
Each shipped template documents its props and slots in a comment at the top of the file — that's the per-component reference.
cui_attrs() — HTML attribute rendering
Used throughout the shipped templates and available for your own:
<div{{ cui_attrs({ id: props.id|default(null), {# null/false attributes are dropped #} disabled: props.disabled|default(false), {# true renders as a boolean attribute #} class: ['cui-card', { 'is-active': props.active }], {# arrays + conditional class maps #} style: { '--gap': props.gap|default(null) }, {# style maps, empty values dropped #} data: { variant: props.variant }, {# expands to data-variant="…" #} }) }}>
Values are HTML-escaped, and invalid attribute names throw instead of rendering — so props can never smuggle markup into your attributes.
Assets and theming
cui_assets() renders the <link> and <script defer> tags for the Combat UI build, prefixed (by default) with the theme-restore script:
{{ cui_assets() }} {# default build & entry #}
{{ cui_assets('my-entry', 'my_build') }} {# explicit entry / Encore build #}
cui_theme_script() is also available standalone. It reads cui-theme from localStorage and sets data-theme on <html> synchronously, so a returning dark-mode visitor never sees a light flash.
Component defaults
Set project-wide default props per component; explicit props in the template always win:
# config/packages/combat_ui_core.yaml combat_ui_core: component_defaults: button: variant: primary hero: align: center
Configuration reference
combat_ui_core: # Webpack Encore integration used by cui_assets() encore: # Name of the Encore build cui_assets() reads from. # 'combat_ui' (the default) is auto-registered by the bundle; # set a different name to take over asset building yourself. build_name: combat_ui # Entry point rendered by cui_assets() entry: combat-ui # Directory containing the build's entrypoints.json. # Defaults to where assets:install puts the bundle's prebuilt assets. build_path: '%kernel.project_dir%/public/bundles/combatuicore/build' # Prefix cui_assets() output with the theme-restore script theme_guard: true # Default props merged into every render of the given component component_defaults: {}
Bringing your own build
The prebuilt assets are great for getting started, but for tree-shaking or bundling Combat UI with your own front-end code you can point the bundle at your own Encore build:
- Add
@combat-ui/coreto your app'spackage.jsonand create an entry that imports it. - Register that build in
webpack_encore.builds(or reuse your default build). - Point the bundle at it:
combat_ui_core: encore: build_name: app # the bundle stops auto-registering its own build entry: app
Testing & quality
vendor/bin/codecept build # generate actor classes vendor/bin/codecept run # run the Codeception suites vendor/bin/phpstan # static analysis
CI runs the suites against PHP 8.4/8.5 × Symfony 7.4/8.x.
Related packages
@combat-ui/core— the underlying front-end framework (custom elements + CSS)- Combat UI OpenDXP Bundle — CMS integration for OpenDXP, built on top of this bundle
License
Released under the MIT License.
Copyright © 2026 Combat Jongerenmarketing en -communicatie B.V.