t-att-[name] — Single Attribute
Replace any static attribute with t-att-[name] and set its value to a JavaScript expression. The expression is evaluated in the component's scope.
<!-- src and alt from props -->
<img t-att-src="props.avatarUrl" t-att-alt="props.userName + ' avatar'"/>
<!-- href built from expression -->
<a t-att-href="'/products/' + props.productId">View product</a>
<!-- Conditional disabled -->
<button t-att-disabled="state.isLoading || !state.isValid">Submit</button>
<!-- Tabindex from state -->
<div t-att-tabindex="state.isInteractive ? 0 : -1"></div>
<!-- data-* attributes -->
<div t-att-data-id="item.id" t-att-data-category="item.category"></div>
<!-- Input type from props -->
<input t-att-type="props.inputType" t-att-placeholder="props.placeholder"/>
For boolean HTML attributes, t-att-disabled="false" removes the attribute entirely (correct behavior). t-att-disabled="true" adds disabled="disabled". You never need to set disabled="false" in HTML — absence of the attribute means not disabled. OWL handles this correctly for all boolean attributes.
t-att-class — Conditional CSS Classes
t-att-class is the most commonly used dynamic attribute in OWL. It accepts three forms:
Object syntax (recommended)
<!-- Each key is a class name, each value is a boolean expression -->
<div t-att-class="{
'card': true,
'card--active': state.isActive,
'card--error': state.hasError,
'card--loading': state.isLoading,
'card--large': props.size === 'large'
}"></div>
<!-- Mix with static class attribute -->
<button class="btn" t-att-class="{
'btn--primary': props.variant === 'primary',
'btn--secondary': props.variant === 'secondary',
'btn--disabled': props.disabled
}">
<t t-esc="props.label"/>
</button>
String expression
<!-- Build class name string dynamically -->
<span t-att-class="'badge badge--' + props.status"/>
<!-- Renders: class="badge badge--active" (or "badge--pending", etc.) -->
<!-- Ternary -->
<div t-att-class="state.isOpen ? 'panel panel--open' : 'panel panel--closed'"/>
t-att-style — Inline Styles
t-att-style accepts either a CSS string or a style object:
<!-- String form -->
<div t-att-style="'background-color: ' + props.color + '; opacity: ' + state.opacity"/>
<!-- Object form (keys are camelCase CSS property names) -->
<div t-att-style="{
backgroundColor: props.color,
opacity: state.opacity,
transform: 'translateX(' + state.offsetX + 'px)',
display: state.visible ? 'block' : 'none'
}"/>
<!-- Progress bar width -->
<div class="progress-bar">
<div
class="progress-fill"
t-att-style="{ width: state.progress + '%' }"
/>
</div>
<!-- Avatar with dynamic background image -->
<div class="avatar" t-att-style="{
backgroundImage: 'url(' + props.avatarUrl + ')',
width: props.size + 'px',
height: props.size + 'px'
}"/>
t-att — Multiple Attributes from Object
When you need to set many attributes at once from a computed object, use t-att (without a suffix):
<!-- All attributes from an object expression -->
<input t-att="inputAttrs"/>
class DynamicInput extends Component {
get inputAttrs() {
return {
type: this.props.type || "text",
placeholder: this.props.placeholder || "",
disabled: this.state.isLoading,
maxlength: this.props.maxLength,
autocomplete: "off",
};
}
static template = xml`
<input t-att="inputAttrs"/>
`;
}
Accessible ARIA Attributes
ARIA attributes are critical for accessibility and are commonly dynamic — they should reflect current state:
<!-- Accordion button -->
<button
t-att-aria-expanded="state.isOpen ? 'true' : 'false'"
t-att-aria-controls="'panel-' + props.id"
t-on-click="toggle"
>
<t t-esc="props.title"/>
</button>
<div
t-att-id="'panel-' + props.id"
t-att-aria-hidden="state.isOpen ? 'false' : 'true'"
t-if="state.isOpen"
>
<t t-slot="default"/>
</div>
<!-- Loading spinner -->
<div
role="status"
t-att-aria-label="state.isLoading ? 'Loading…' : 'Content loaded'"
t-att-aria-busy="state.isLoading ? 'true' : 'false'"
></div>
<!-- Tab panel -->
<button
role="tab"
t-att-aria-selected="props.activeTab === props.tabId ? 'true' : 'false'"
t-att-tabindex="props.activeTab === props.tabId ? 0 : -1"
>
<t t-esc="props.label"/>
</button>
Real-World Component Patterns
class Button extends Component {
static template = xml`
<button
t-att-class="{
'btn': true,
'btn--primary': props.variant === 'primary',
'btn--secondary': props.variant === 'secondary',
'btn--danger': props.variant === 'danger',
'btn--loading': props.loading,
'btn--icon-only': !props.label
}"
t-att-disabled="props.disabled || props.loading"
t-att-type="props.type || 'button'"
t-att-aria-busy="props.loading ? 'true' : 'false'"
t-att-aria-label="props.ariaLabel || props.label"
t-on-click="props.onClick"
>
<span t-if="props.loading" class="spinner" aria-hidden="true"/>
<span t-if="props.icon" t-esc="props.icon" aria-hidden="true"/>
<span t-if="props.label" t-esc="props.label"/>
</button>
`;
static props = {
label: { type: String, optional: true },
variant: { type: String, optional: true },
type: { type: String, optional: true },
disabled: { type: Boolean, optional: true },
loading: { type: Boolean, optional: true },
icon: { type: String, optional: true },
ariaLabel: { type: String, optional: true },
onClick: { type: Function, optional: true },
};
static defaultProps = {
variant: "primary",
disabled: false,
loading: false,
onClick: () => {},
};
}
📋 Summary
t-att-[name]="expression"sets any attribute dynamically — works for all HTML attributes includingsrc,href,disabled,tabindex,data-*,aria-*.t-att-class="{className: boolExpr}"— object syntax adds/removes classes conditionally. OWL merges with any staticclassattribute on the same element.t-att-class="'base ' + conditionalPart"— string expression for dynamic class names (BEM modifier pattern).t-att-style="{camelCaseProp: value}"— object syntax for inline styles. Key names are camelCase JavaScript property names.t-att="{attr1: val1, attr2: val2}"— set multiple attributes from a computed object in one directive.- For boolean attributes (
disabled,required,checked): a falsy expression removes the attribute entirely — the correct HTML behavior. - Always add dynamic ARIA attributes to interactive components for accessibility.
🏋️ Exercise
Build an accessible Accordion component using dynamic attributes:
- Props:
items— array of{ id, title, content }. - State:
openId— the ID of the currently open panel (ornull). - For each item render a
<button>header and a content panel. Uset-att-aria-expanded,t-att-aria-controls,t-att-id, andt-att-aria-labelledbyfor full ARIA compliance. - Use
t-att-classwith object syntax to add anis-openclass to the active panel header and content. - Use
t-att-styleto animate the max-height of the content panel (0 when closed, a fixed value when open). - Clicking an open item closes it; clicking another opens it and closes the previous one.
Frequently Asked Questions
Yes — OWL merges them. The static class attribute always applies; t-att-class adds or removes classes on top of it. For example, class="btn" t-att-class="{ 'btn--active': isActive }" always has the btn class and conditionally has btn--active. This is the standard pattern for base classes + modifier classes.
In the resulting HTML, they are equivalent — both mean "not disabled". OWL removes attributes whose expression evaluates to false, null, or undefined. This means t-att-disabled="state.isLoading" correctly removes the disabled attribute when isLoading is false, and adds it when true. You never need a separate t-if to conditionally add boolean attributes.
A static style attribute can only hold a fixed string — you cannot embed JavaScript expressions in it. t-att-style lets the style values come from state, props, or computed values. For example, a progress bar's width must change dynamically: t-att-style="{ width: state.progress + '%' }". Prefer CSS classes over inline styles when possible — use t-att-style only for values that genuinely cannot be expressed with predefined classes (like exact pixel positions, user-defined colors, or computed widths).