The :attrs dynamic attribute enables us to create wrapper components that proxy all their attributes to an inner component:
<c-outer
class="outer-class"
:count="42"
:enabled="False">
Content passed to inner component
</c-outer>
{% cotton outer class="outer-class" :count="42" :enabled="False" %}
Content passed to inner component
{% endcotton %}
<c-inner :attrs="attrs">{{ slot }}</c-inner>
{% cotton inner :attrs="attrs" %}{{ slot }}{% endcotton %}
{{ class }} <!-- "outer-class" -->
{{ count }} <!-- 42 -->
{{ enabled }} <!-- False -->
{{ slot }} <!-- Content passed to inner component -->
The attributes are passed through to the inner component with their original types preserved (strings, numbers, booleans, lists, etc.), making this pattern ideal for creating higher-order components.
This pattern is particularly useful for Django form fields, where you might create a component hierarchy that passes Django widget attributes through multiple layers while adding labels, error handling and styling at each level.
Say you want one styled text input you can drop in anywhere, while still passing through any native input attribute (type, placeholder, required, even hx-*). Declare the bits the component owns in <c-vars /> and proxy everything else straight onto the inner <input>:
<c-vars label />
<label class="block">
<span>{{ label }}</span>
<input {{ attrs }} class="border rounded px-3 py-2 w-full" />
</label>
{% cotton:vars label %}
<label class="block">
<span>{{ label }}</span>
<input {{ attrs }} class="border rounded px-3 py-2 w-full" />
</label>
Every caller now gets the label and styling for free, and anything else they pass lands on the actual input:
<c-input label="Email" name="email" type="email" placeholder="you@example.com" required />
<c-input label="Username" name="username" maxlength="20" />
{% cotton input label="Email" name="email" type="email" placeholder="you@example.com" required %}{% endcotton %}
{% cotton input label="Username" name="username" maxlength="20" %}{% endcotton %}
label is consumed by the component because it's declared in <c-vars />, while name, type, placeholder, required and maxlength all proxy through to the <input> untouched.