Custom attributes
Learn how to build and enhance Aurelia 2 custom attributes, including advanced configuration, binding strategies, and accessing the host element.
Custom attributes in Aurelia empower you to extend and decorate standard HTML elements by embedding custom behavior and presentation logic. They allow you to wrap or integrate existing HTML plugins and libraries, or simply enhance your UI components with additional dynamic functionality. This guide provides a comprehensive overview—from basic usage to advanced techniques—to help you leverage custom attributes effectively in your Aurelia 2 projects.
Table of Contents
Introduction
Custom attributes are one of the core building blocks in Aurelia 2. Similar to components, they encapsulate behavior and style, but are applied as attributes to existing DOM elements. This makes them especially useful for:
Decorating elements with additional styling or behavior.
Wrapping third-party libraries that expect to control their own DOM structure.
Creating reusable logic that enhances multiple elements across your application.
Creating template controllers that control the rendering of content.
Creating a Basic Custom Attribute
At its simplest, a custom attribute is defined as a class that enhances an element. Consider this minimal example:
When you apply a similar pattern using CustomElement instead, you are defining a component. Custom attributes are a more primitive (yet powerful) way to extend behavior without wrapping the entire element in a component.
Example: Red Square Attribute
This custom attribute adds a fixed size and a red background to any element it is applied to:
Usage in HTML:
The <import> tag ensures that Aurelia's dependency injection is aware of your custom attribute. When applied, the <div> will render with the specified styles.
Custom Attribute Definition Approaches
Aurelia 2 provides multiple approaches for defining custom attributes. For most user scenarios, you'll use either the convention-based or decorator-based approach:
Convention-Based Approach
Classes ending with CustomAttribute are automatically recognized as custom attributes:
The attribute name is derived from the class name (red-square in this case).
Decorator-Based Approach (Recommended)
Use the @customAttribute decorator for explicit control and better IDE support:
Static Definition Approach (Framework Internal)
For completeness, the framework also supports defining attributes using a static $au property. This approach is primarily used by the framework itself to avoid conventions and decorators, but is available if needed:
Explicit Custom Attributes
To gain finer control over your attribute's name and configuration, Aurelia provides the @customAttribute decorator. This lets you explicitly define the attribute name and even set up aliases.
Explicit Attribute Naming
By default, the class name might be used to infer the attribute name. However, you can explicitly set a custom name:
Attribute Aliases
You can define one or more aliases for your custom attribute. This allows consumers of your attribute flexibility in naming:
Now the attribute can be used interchangeably using any of the registered names:
Single Value Binding
For simple cases, you might want to pass a single value to your custom attribute without explicitly declaring a bindable property. Aurelia will automatically populate the value property if a value is provided.
Usage:
To further handle changes in the value over time, you can define the property as bindable:
Usage with dynamic binding:
Bindable Properties and Change Detection
Custom attributes often need to be configurable. Using the @bindable decorator, you can allow users to pass in parameters that change the behavior or style dynamically.
Binding Modes
Bindable properties support different binding modes that determine how data flows:
Available binding modes:
BindingMode.toView(default): Data flows from view model to viewBindingMode.fromView: Data flows from view to view modelBindingMode.twoWay: Data flows both waysBindingMode.oneTime: Data is set once and never updated
Primary Bindable Property
You can mark one property as primary, allowing simpler binding syntax:
With a primary property defined, you can bind directly:
Bindable Interceptors and Type Coercion
You can intercept and transform values being set on bindable properties using the set option, or leverage Aurelia's built-in type coercion system:
Built-in Type Coercers:
Number: Converts strings to numbers ("123"→123)String: Converts values to strings (123→"123")Boolean: Converts values to booleans ("true"→true,""→false)BigInt: Converts to BigInt valuesCustom functions: Any function that accepts a value and returns a transformed value
Advanced Coercion Example:
Options Binding for Multiple Properties
When you have more than one bindable property, you can use options binding syntax to bind multiple properties at once. This powerful syntax supports complex expressions, binding behaviors, and value converters:
Basic Options Binding
Advanced Options Binding Features
Escaping Special Characters
Use backslashes to escape colons in URLs or other values:
Disabling Multi-Binding Parsing
For attributes that need to handle complex strings without parsing:
Advanced Bindable Configuration
You can also define bindables in the static definition or decorator:
Or using the static $au approach:
Lifecycle Hooks
Custom attributes support a comprehensive set of lifecycle hooks that allow you to run code at different stages of their existence:
created(controller): Called after the attribute instance is createdbinding(initiator, parent): Called before data binding beginsbind(): Called when data binding begins (simplified version)bound(initiator, parent): Called after data binding is completeattaching(initiator, parent): Called before the element is attached to the DOMattached(initiator): Called after the element is attached to the DOMdetaching(initiator, parent): Called before the element is detached from the DOMunbinding(initiator, parent): Called before data binding is removedunbind(): Called when data binding is removed (simplified version)
Example: Using Lifecycle Hooks
Aggregated Change Callbacks
Custom attributes provide powerful batching capabilities for handling multiple property changes efficiently:
Accessing the Host Element
A key aspect of custom attributes is that they work directly on DOM elements. To manipulate these elements (e.g., updating styles or initializing plugins), you need to access the host element. Aurelia provides a safe way to do this using dependency injection with INode.
Note: While you can also use resolve(Element) or resolve(HTMLElement), using INode is safer in environments where global DOM constructors might not be available (such as Node.js).
Finding Related Custom Attributes
In complex UIs, you might have multiple custom attributes working together (for example, a dropdown with associated toggle buttons). Aurelia offers the CustomAttribute.closest function to traverse the DOM and locate a related custom attribute. This function can search by attribute name or by constructor.
Example: Searching by Attribute Name
Example: Searching by Constructor (Type-Safe)
If you want to search based on the attribute's constructor (for stronger typing), you can do so:
Practical Use Case: Coordinated Form Validation
Usage:
Important Notes
DOM Traversal:
closest()walks up the DOM tree, checking each ancestor elementMultiple Matches: Returns the first (closest) matching attribute found
Error Handling: Throws an error if searching by constructor for a class without an attribute definition
Performance: Efficient DOM traversal, but cache results if called frequently
Type Safety: Constructor-based searches provide better TypeScript support
Template Controller Custom Attributes
Custom attributes can also function as template controllers, which control the rendering of content. Template controllers are similar to built-in directives like if.bind and repeat.for.
Creating a Template Controller
Usage:
You can also use the static definition approach:
Advanced Configuration Options
Custom attributes support several advanced configuration options:
No Multi-Bindings
By default, custom attributes support multiple bindings (attr="prop1: value1; prop2: value2"). You can disable this:
Dependencies
You can specify dependencies that should be registered when the attribute is used:
Container Strategy (Template Controllers Only)
For template controller custom attributes, you can specify the container strategy to control service isolation:
Container Strategy Options:
'reuse'(default): Child views share the parent's containerMore memory efficient
Services are singleton across parent and child views
Faster view creation
'new': Creates a new container for child viewsProvides service isolation
Each child view gets its own service instances
Useful for plugin systems or complex nested scenarios
When to Use Container Isolation:
Definition Metadata Reference
Decorators and conventions eventually funnel into a PartialCustomAttributeDefinition, defined in @aurelia/runtime-html. Knowing every field on that definition unlocks advanced behaviors without sprinkling ad-hoc logic through your class. The table below summarizes the metadata you can provide (either via decorators or by calling CustomAttribute.define()/CustomAttributeDefinition.create() yourself):
name
string
The canonical attribute name. When omitted, Aurelia infers it from the class name.
aliases
string[]
Additional attribute names that should map to the same implementation. Use when you need both awesome-slider and awesomeSlider.
bindables
Record<string, PartialBindableDefinition> or array
Declares bindable inputs. You can mix shorthand strings with full objects { name, mode, callback, attribute }.
defaultBindingMode
BindingMode or string
Overrides the default for all bindables (unless a bindable explicitly sets its own mode).
isTemplateController
boolean
Marks the attribute as a template controller so Aurelia replaces the host element with the controller’s view.
noMultiBindings
boolean
Treats the attribute value as a single literal string instead of prop: value pairs. Useful for URLs and DSL-like syntaxes.
watches
IWatchDefinition[]
Registers @watch entries without decorators—great for framework-level attributes or generated code.
dependencies
Key[]
Additional registrations to install when the attribute definition is added to a container (for example, helper services).
containerStrategy
'reuse' | 'new'
Controls whether template controllers reuse the parent container or spin up an isolated child container.
Inspecting definitions at runtime
CustomAttributeDefinition.getDefinition(MyAttribute) returns the normalized definition object—including inferred defaults and decorator metadata. That makes it simple to write tooling or plugin code that reacts to attribute settings:
Creating attributes without decorators
When you need to generate attributes dynamically (for example, inside a plugin) call CustomAttribute.define or CustomAttributeDefinition.create with a PartialCustomAttributeDefinition:
Because all of these options live on the definition, you keep your constructor and lifecycle hooks focused on runtime behavior while the metadata decides how the attribute integrates with the templating pipeline.
Watch Integration
Custom attributes can integrate with Aurelia's @watch decorator for advanced property observation:
Integrating Third-Party Libraries
Often, you'll want to incorporate functionality from third-party libraries—such as sliders, date pickers, or custom UI components—into your Aurelia applications. Custom attributes provide an excellent way to encapsulate the integration logic, ensuring that the third-party library initializes, updates, and cleans up properly within Aurelia's lifecycle.
When to Use Custom Attributes for Integration
DOM Manipulation: Many libraries require direct access to the DOM element for initialization.
Lifecycle Management: You can leverage Aurelia's lifecycle hooks (
attached()anddetached()) to manage resource allocation and cleanup.Dynamic Updates: With bindable properties, you can pass configuration options to the library and update it reactively when those options change.
Example: Integrating a Hypothetical Slider Library
Consider a third-party slider library called AwesomeSlider that initializes a slider on a given DOM element. Below is an example of how to wrap it in a custom attribute.
In place of our hypothetical AwesomeSlider library, you can use any third-party library that requires DOM manipulation such as jQuery plugins, D3.js, or even custom UI components.
Best Practices
Separation of Concerns
Keep your custom attribute logic focused on enhancing the host element, and avoid heavy business logic. Custom attributes should be presentational or behavioral enhancements, not data processing units.
Performance
Minimize DOM manipulations: Cache style properties and batch updates when possible
Use
propertiesChanged: For multiple property changes, batch updates to reduce DOM thrashingLifecycle hook timing: Use appropriate hooks for initialization
constructor(): Basic setup, non-DOM operationsattached(): DOM-dependent initialization, third-party library setupdetaching(): Cleanup before DOM removal
Memory Management
Clean up event listeners: Always remove event listeners to prevent memory leaks
Dispose third-party instances: Call proper cleanup methods for external libraries
Weak references: Use WeakMap/WeakSet for object references when appropriate
Error Handling
Graceful degradation: Handle initialization failures gracefully
Validation: Validate bindable property values
Logging: Use Aurelia's logging system for debugging
Testing
Write comprehensive unit tests covering lifecycle hooks, property changes, and edge cases:
Documentation and Maintainability
Document public APIs: Clearly document bindable properties and their expected types
Use meaningful names: Choose descriptive names for attributes and properties
Provide usage examples: Include HTML usage examples in comments
Type everything: Use strong TypeScript typing for better IDE support
Type Safety Best Practices
Advanced Features Summary
Computed Bindables with Getters
Custom attributes support getter-based bindables for computed properties:
Bindable Inheritance
Bindable properties properly inherit from parent classes:
Error Handling and Lifecycle Management
Last updated
Was this helpful?