Attribute passthrough. Until now, no component in either engine forwarded an attribute it did not itself declare — hx-*, @click, x-*, data-*, aria-*, a bare id all silently vanished. The component rendered, styled correctly, and read correctly at the call site; it just did not behave. On an interactive element that is the whole point, so this was tracked as a bug (#70), not a feature request, and #70 stays open — this release covers the "interactive set" (the eight components a caller is most likely to wire up with client-side behaviour), not all 21.
Upgrading: fully additive. attrs defaults to {} everywhere it was added, so every existing call site renders byte-identically. No migration.
Added — attrs passthrough on Button, Select, Textarea, FormField, Icon, Badge, Box and CheckboxGroup (#70, #72, #73, #76, #77, #82)
- One mechanism, one implementation.
attrs={}on the{#def}/<c-vars>, and a singlecf_ui_render_attrs(component, attrs)call ({% cf_ui_render_attrs %}in cotton) placed just before the target element's closing>. Every component routes through the onecf_ui.primitives.render_attrsimplementation — there is no per-component escaping or validation logic to drift. - A declared per-component reserved set, not a blanket block-list.
RESERVED_ATTRSinprimitives.pylists the attribute names each component already owns (classeverywhere;type/hrefonButton;role/aria-label/aria-hiddenonIcon;id/nameon the form controls) and raisesPrimitiveConfigErroron a collision rather than lettingattrssilently overwrite a prop the component computed itself. CheckboxGroupis the one exception to "the control, not the wrapper." It renders N<input type="checkbox">elements fromchoices, so there is no single control forattrsto target — it lands on the outer wrapper element (field/mb-3/form-control/ui form/fieldset, per theme) instead, and this is documented as a deliberate departure fromSelect/Textarea/FormField's convention, not an oversight.- JinjaX's own reserved name for this is
_attrs, notattrs— the bareattrs=kwarg is JinjaX's built-in extra-kwargs collector and gets silently overwritten on every render under a realCatalog, no error. This surfaced as a docs bug (#78):docs/primitives.mdshowed:attrs="{...}"for JinjaX call sites, which compiles, runs, and discards the dict — a call site that reads correctly and does nothing. Fixed to:_attrs="{...}", with a callout explaining the collision. - The collision guard has a known gap, documented rather than hidden. When a reserved name is also one of a component's own declared props (
type/hrefonButton,nameon the form controls), JinjaX's own argument filtering routes the value into that prop beforerender_attrsever runs — the guard never fires. This is a JinjaX-level limitation, not a cf-ui defect to patch around.
Fixed — the cotton integration tier asserted nothing about real Cotton compilation (#79)
tests/integration/cotton_appnever registereddjango_cottoninINSTALLED_APPS. Every<c-cf.*>tag in that tier reached the response as literal, uncompiled text, so its assertions on rendered output were checking a raw template string, not django-cotton's actual compiler output — silently, with every test green. Fixed by registeringdjango_cottonand re-verifying every existing integration assertion still holds against real compiled output.
Notes
- #70 remains open. The data-driven components —
Table,Pagination,Breadcrumb,Progress— and the remaining structural/content components still drop undeclared attributes; a follow-up phase covers them.
Full changelog: https://github.com/fsecada01/component-framework-ui/blob/master/CHANGELOG.md#040--2026-08-19