Summary
No component in either engine forwards attributes it does not declare. Every theme partial renders a fixed attribute list, so anything the caller adds beyond the declared props — hx-*, @click, x-*, id, data-*, aria-*, name, form — is dropped.
Like #65, this fails silently. The component renders, styles correctly, and reads correctly at the call site. What it does not do is behave. On an interactive component that is the whole point of the element.
Scope
Measured on 0.3.1: 210 templates reference attrs zero times — 105 cotton theme partials and 105 JinjaX components, five themes each, across all 21 components.
So this is not a gap in one component. It is the current contract everywhere.
Reproduction
{% load cotton %}
<c-cf.button variant="primary" hx-post="/enrich/" hx-target="#panel" hx-swap="innerHTML">
Enrich
</c-cf.button>
Rendered (0.3.1, CF_UI_THEME = "bulma"):
<button class="button is-primary" type="button">Enrich</button>
Correct classes, correct element, correct label. The HTMX wiring is gone, so the button does nothing when clicked. Same for <c-cf.icon id="x" data-state="ok">, which loses both attributes.
Why the fix is small
django-cotton already collects undeclared attributes into attrs, and that context survives the wrapper's {% include %} into the theme partial. I verified this by reproducing cf-ui's own wrapper/partial structure locally rather than reading it off the source:
{# probe_widget.html — mimics cf/button.html #}
<c-vars variant="neutral" class="" />
{% include "probe_partial.html" %}
{# probe_partial.html — mimics _themes/bulma/button.html, plus attrs #}
<button class="btn {{ variant }} {{ class }}" {{ attrs }}>{{ slot }}</button>
Called as <c-probe_widget variant="primary" hx-post="/x/" hx-target="#t" @click="go()">Go</c-probe_widget>, that renders:
<button class="btn primary " hx-post="/x/" hx-target="#t" @click="go()">Go</button>
Declared props (variant) are correctly excluded from attrs; everything else comes through. So on the cotton side the change is one {{ attrs }} per partial — the plumbing already works and is simply not being used.
On the JinjaX side the analogous mechanism is the implicit attrs object rather than a declared param. I could not verify that half locally (the consuming repo is Django-only, no JinjaX installed), so treat the Jinja API as needing your confirmation rather than as asserted.
Impact in a real consumer
RankedJobs (fsecada01/Ranked-Jobs) is the only repo declaring cf-ui. Its cotton layer has 29 button-classed tags, and 15 of them — 52% — carry hx-post/hx-target/hx-swap/hx-vals driving the HTMX panel refresh cycle. Repo-wide across all cf-ui-mappable constructs it is 24 of 217 tags.
This is not hypothetical. That repo just merged the first slice of its cf-ui adoption (Ranked-Jobs#247) and had to stop at notification because of this. The adoption ticket's premise was that swapping raw markup for cf-ui components on the bulma theme is visually inert, since the bulma partials emit bulma classes. That premise holds for classes and says nothing about behaviour, which is exactly what this issue is.
The result is that roughly 107 of ~121 mappable constructs in that layer stayed raw — button 27, icon 34, tag 21, table 9, box 8, heading 6 — not because the components are wrong, but because converting them would ship inert markup. That PR pinned the constraint in a test (test_cf_ui_attribute_passthrough.py) so a release that fixes this shows up there as a failing assertion.
Worth noting the failure mode is the same one as #65 and lands in the same place: markup that looks right in review, renders right in the browser, and silently does nothing. #65 cost that repo three months of empty notification boxes. This one is currently costing it the entire rest of its migration.
Proposed fix
Add {{ attrs }} to the outermost element of each cotton theme partial, and the JinjaX equivalent to each .jinja component.
Three design questions I would not want to answer for you:
Which components? Every one is defensible and consistent. A narrower pass covering the interactive set first — button, select, textarea, form-field, checkbox-group, icon, badge, box — would unblock essentially all real usage. The data-driven ones (table, pagination, breadcrumb, progress) benefit less, though id and aria-* still apply to them.
Where multiple elements exist. form-field and checkbox-group render a wrapper plus a control. Caller attributes almost always mean the control, not the wrapper, which is a real decision rather than a mechanical edit — and an argument for handling those separately from the single-element components.
Collision with declared props. <c-cf.button class="x" hx-post="/y/"> must not emit class twice. Cotton excludes declared props from attrs, which the probe above confirms, but it is worth an explicit test per component given class is declared on nearly all of them.
Escaping
Given 0.2.0's autoescape change and the hardening in #36, this deserves care rather than assumption. Attribute values here come from the template author, not from end users — but templates interpolate user data into them freely (hx-vals with a rendered id is the common case in the consumer above), so attrs must escape values on the way out. Cotton's attrs handles this; the JinjaX side should be asserted rather than inferred, since that is precisely the path #36 was about.
Suggested tests
- Per component, per engine: an undeclared attribute reaches the rendered element.
- Per component: a declared prop passed alongside undeclared ones is not duplicated — specifically
class.
- Attribute values containing quotes and
< are escaped, on both engines.
- Boolean/valueless attributes (
disabled, required, x-cloak) survive as bare attributes rather than becoming disabled="" — or, if normalising them is intended, that is pinned deliberately.
Summary
No component in either engine forwards attributes it does not declare. Every theme partial renders a fixed attribute list, so anything the caller adds beyond the declared props —
hx-*,@click,x-*,id,data-*,aria-*,name,form— is dropped.Like #65, this fails silently. The component renders, styles correctly, and reads correctly at the call site. What it does not do is behave. On an interactive component that is the whole point of the element.
Scope
Measured on 0.3.1: 210 templates reference
attrszero times — 105 cotton theme partials and 105 JinjaX components, five themes each, across all 21 components.So this is not a gap in one component. It is the current contract everywhere.
Reproduction
Rendered (0.3.1,
CF_UI_THEME = "bulma"):Correct classes, correct element, correct label. The HTMX wiring is gone, so the button does nothing when clicked. Same for
<c-cf.icon id="x" data-state="ok">, which loses both attributes.Why the fix is small
django-cotton already collects undeclared attributes into
attrs, and that context survives the wrapper's{% include %}into the theme partial. I verified this by reproducing cf-ui's own wrapper/partial structure locally rather than reading it off the source:Called as
<c-probe_widget variant="primary" hx-post="/x/" hx-target="#t" @click="go()">Go</c-probe_widget>, that renders:Declared props (
variant) are correctly excluded fromattrs; everything else comes through. So on the cotton side the change is one{{ attrs }}per partial — the plumbing already works and is simply not being used.On the JinjaX side the analogous mechanism is the implicit
attrsobject rather than a declared param. I could not verify that half locally (the consuming repo is Django-only, no JinjaX installed), so treat the Jinja API as needing your confirmation rather than as asserted.Impact in a real consumer
RankedJobs (
fsecada01/Ranked-Jobs) is the only repo declaring cf-ui. Its cotton layer has 29 button-classed tags, and 15 of them — 52% — carryhx-post/hx-target/hx-swap/hx-valsdriving the HTMX panel refresh cycle. Repo-wide across all cf-ui-mappable constructs it is 24 of 217 tags.This is not hypothetical. That repo just merged the first slice of its cf-ui adoption (Ranked-Jobs#247) and had to stop at
notificationbecause of this. The adoption ticket's premise was that swapping raw markup for cf-ui components on the bulma theme is visually inert, since the bulma partials emit bulma classes. That premise holds for classes and says nothing about behaviour, which is exactly what this issue is.The result is that roughly 107 of ~121 mappable constructs in that layer stayed raw — button 27, icon 34, tag 21, table 9, box 8, heading 6 — not because the components are wrong, but because converting them would ship inert markup. That PR pinned the constraint in a test (
test_cf_ui_attribute_passthrough.py) so a release that fixes this shows up there as a failing assertion.Worth noting the failure mode is the same one as #65 and lands in the same place: markup that looks right in review, renders right in the browser, and silently does nothing. #65 cost that repo three months of empty notification boxes. This one is currently costing it the entire rest of its migration.
Proposed fix
Add
{{ attrs }}to the outermost element of each cotton theme partial, and the JinjaX equivalent to each.jinjacomponent.Three design questions I would not want to answer for you:
Which components? Every one is defensible and consistent. A narrower pass covering the interactive set first —
button,select,textarea,form-field,checkbox-group,icon,badge,box— would unblock essentially all real usage. The data-driven ones (table,pagination,breadcrumb,progress) benefit less, thoughidandaria-*still apply to them.Where multiple elements exist.
form-fieldandcheckbox-grouprender a wrapper plus a control. Caller attributes almost always mean the control, not the wrapper, which is a real decision rather than a mechanical edit — and an argument for handling those separately from the single-element components.Collision with declared props.
<c-cf.button class="x" hx-post="/y/">must not emitclasstwice. Cotton excludes declared props fromattrs, which the probe above confirms, but it is worth an explicit test per component givenclassis declared on nearly all of them.Escaping
Given 0.2.0's autoescape change and the hardening in #36, this deserves care rather than assumption. Attribute values here come from the template author, not from end users — but templates interpolate user data into them freely (
hx-valswith a rendered id is the common case in the consumer above), soattrsmust escape values on the way out. Cotton'sattrshandles this; the JinjaX side should be asserted rather than inferred, since that is precisely the path #36 was about.Suggested tests
class.<are escaped, on both engines.disabled,required,x-cloak) survive as bare attributes rather than becomingdisabled=""— or, if normalising them is intended, that is pinned deliberately.