Attribute Directives #
Use an ordinary binding for an attribute value, ? for conditional presence,
or : to pass a value to a child DOM property.
Simple Dynamic Attributes #
Use {{}} inside an attribute value to bind it to a state value:
<a href="{{url}}">{{linkText}}</a>
<img src="{{imageUrl}}" alt="{{imageAlt}}" />
Example #
State:
{
"url": "https://example.com",
"linkText": "Visit Example"
}
Output:
<a href="https://example.com">Visit Example</a>
A whole-binding attribute is omitted when its value is missing or null.
Empty strings, 0, and false remain attribute values; use ? when presence
should depend on truthiness. Mixed attributes keep their static text.
Boolean Attributes (? prefix) #
Prefix an attribute name with ? to make it conditional. The attribute is only rendered if the bound expression evaluates to a truthy value:
<button ?disabled={{isDisabled}}>Submit</button>
<input type="checkbox" ?checked={{isSelected}} />
The value must be a pure binding: either a single path or an expression
accepted by <if condition="...">. Plain text and mixed static/dynamic values
cause the attribute to be silently dropped.
Prefer Expressions Over Mirror Observables #
Derive presence from existing state rather than maintaining parallel
isFirst, isLast, or isSelected observables:
<!-- ✅ Good: derive from existing state -->
<button ?disabled="{{currentIndex == 0}}">Prev</button>
<button ?disabled="{{currentIndex == lastIndex}}">Next</button>
<option ?selected="{{item.id == selectedId}}">{{item.name}}</option>
<!-- ❌ Avoid: mirror observables that duplicate derivable state -->
<!-- @observable isPrevDisabled, isNextDisabled, isItemSelected — every
update path now has to remember to recompute them. -->
<button ?disabled="{{isPrevDisabled}}">Prev</button>
The expression updates when a referenced value changes; a separate flag would need to be kept in sync.
Expressions compare a dotted path against a literal or another dotted path. There is no arithmetic: {{currentIndex == items.length - 1}} is read as a comparison against a state key literally named items.length - 1, which never resolves, so the attribute is silently never rendered. Supply a precomputed lastIndex instead. A bare .length is fine on its own ({{items.length > 0}}).
Dotted Paths #
Boolean attributes support dotted path notation to access nested state:
<div ?hidden={{layout.isPinned}}>Pinned content</div>
Valid and Invalid Examples #
<!-- ✅ Valid: pure handlebars expression -->
<button ?disabled={{isDisabled}}>Click</button>
<!-- ✅ Valid: dotted path -->
<input ?checked={{form.agreed}} />
<!-- ✅ Valid: comparison expression (preferred over mirror observables) -->
<button ?disabled={{currentIndex == 0}}>Prev</button>
<option ?selected={{item.id == selectedId}}>{{item.name}}</option>
<!-- ❌ Invalid: plain value (attribute silently dropped) -->
<button ?disabled="true">Click</button>
<!-- ❌ Invalid: mixed content (attribute silently dropped) -->
<input ?checked="Hello {{name}}" />
Example #
State:
{
"isDisabled": true,
"isSelected": false
}
Template:
<button ?disabled={{isDisabled}}>Submit</button>
<input type="checkbox" ?checked={{isSelected}} />
Output:
<button disabled>Submit</button>
<input type="checkbox" />
Complex Attributes (: prefix) #
Prefix a name with : to write a child DOM property, including structured
values that do not fit in a string attribute:
<my-component :config="{{settings}}"></my-component>
<my-component :prop1="{{val1}}" :prop2="{{val2}}"></my-component>
For client-created trees, initial property bindings are applied before the
child's connectedCallback. Later parent updates flow through the same binding.
See Property Bindings.
Mixed Attributes #
When an attribute value contains a mix of static text and dynamic expressions, each part is resolved independently:
<input value="hello {{world}}" />
<use href="#icon-{{iconName}}" />
Example #
State:
{
"world": "WebUI"
}
Output:
<input value="hello WebUI" />
Combining Attribute Types #
You can mix static, dynamic, boolean, and complex attributes on the same element:
<my-component
id="comp"
:config="{{settings}}"
?enabled="{{isEnabled}}"
>
</my-component>
Static attributes (like id="comp") are passed through as-is. Dynamic, boolean, and complex attributes are resolved from state at render time.