Aurelia Custom Elements — Reusable UI Components
In this tutorial, you will learn about Aurelia Custom Elements. We cover key concepts, practical examples, and best practices to help you master this topic.
Aurelia custom elements are reusable HTML tags that encapsulate template, logic, and styles. They accept bindable properties, emit custom events, and project content. Custom elements form the basis of component libraries and design systems.
What You'll Learn
You will learn how to create custom elements with bindable properties, handle options, project content, and build reusable UI components.
Why It Matters
Custom elements abstract repeated UI patterns into single tags. A <data-table> element with 20 lines replaces 100+ lines of repetitive table markup. Design systems are built entirely from custom elements.
Real-World Use
A design system includes custom elements like <ui-button>, <ui-card>, <ui-modal>, <ui-table>, and <ui-form-field>. Each element has documented bindable properties and is used across dozens of application pages.
flowchart LR
A[Custom Element] --> B[Bindable Properties]
A --> C[Template]
A --> D[Events]
A --> E[Content Slots]
B --> F[Type]
B --> G[Default]
B --> H[Two-way]
Basic Custom Element
// src/resources/elements/status-badge.ts
import { bindable } from 'aurelia-framework';
export class StatusBadge {
@bindable status = 'info';
@bindable text;
statusChanged(newValue, oldValue) {
console.log(`Status changed from ${oldValue} to ${newValue}`);
}
get badgeClass() {
return `badge-${this.status}`;
}
}
<!-- src/resources/elements/status-badge.html -->
<template>
<span class="badge ${badgeClass}">
${text || status}
</span>
</template>
Usage:
<status-badge status.bind="alert.severity" text.bind="alert.message">
</status-badge>
<status-badge status="success" text="Completed"></status-badge>
Bindable Property Options
import { bindable } from 'aurelia-framework';
export class InputField {
// Default binding mode (one-way)
@bindable label;
// Two-way binding for form inputs
@bindable({ defaultBindingMode: bindingMode.twoWay }) value;
// One-time binding (static, never updates)
@bindable({ defaultBindingMode: bindingMode.oneTime }) placeholder;
// With default value
@bindable type = 'text';
@bindable required = false;
@bindable disabled = false;
// Attribute with change handler
@bindable minLength;
minLengthChanged(newValue, oldValue) {
this.validate();
}
// Optional attribute
@bindable helpText;
}
Button Component Example
// src/resources/elements/ui-button.ts
import { bindable } from 'aurelia-framework';
export class UiButton {
@bindable variant = 'primary';
@bindable size = 'md';
@bindable disabled = false;
@bindable loading = false;
@bindable type = 'button';
@bindable icon;
get buttonClass() {
let classes = [`btn-${this.variant}`, `btn-${this.size}`];
if (this.loading) classes.push('btn-loading');
return classes.join(' ');
}
}
<!-- src/resources/elements/ui-button.html -->
<template>
<button
class="btn ${buttonClass}"
type.bind="type"
disabled.bind="disabled || loading"
click.delegate="$event"
>
<i if.bind="icon" class="icon-${icon}"></i>
<span class="btn-text"><slot></slot></span>
<span if.bind="loading" class="spinner"></span>
</button>
</template>
Content Projection with Slots
Aurelia supports content projection using <slot> (Aurelia 2) or <content> (Aurelia 1).
<!-- ui-card.html — Using slot for content projection -->
<template>
<div class="card">
<div class="card-header">
<slot name="header">
<h3>${title}</h3>
</slot>
</div>
<div class="card-body">
<slot></slot>
</div>
<div class="card-footer" if.bind="showFooter">
<slot name="footer"></slot>
</div>
</div>
</template>
// ui-card.ts
import { bindable } from 'aurelia-framework';
export class UiCard {
@bindable title;
@bindable showFooter = true;
}
Usage with slots:
<ui-card title="User Profile">
<div slot="header">
<h2>Custom Header</h2>
<p class="subtitle">Profile settings</p>
</div>
<!-- Main content (default slot) -->
<form>
<label>Name:</label>
<input value.bind="user.name" />
</form>
<div slot="footer">
<button click.delegate="save()">Save</button>
<button click.delegate="cancel()">Cancel</button>
</div>
</ui-card>
Event Emission from Custom Elements
Custom elements emit events using EventAggregator or DOM custom events.
import { bindable, inject, DOM } from 'aurelia-framework';
import { EventAggregator } from 'aurelia-event-aggregator';
@inject(EventAggregator)
export class DropdownSelect {
@bindable options;
@bindable selected;
constructor(eventAggregator) {
this.ea = eventAggregator;
}
selectOption(option) {
this.selected = option;
// Option 1: Event Aggregator
this.ea.publish('dropdown:selected', option);
// Option 2: DOM Custom Event
this.element.dispatchEvent(DOM.createCustomEvent('select', {
detail: option,
bubbles: true
}));
}
}
Modal Dialog Component
// src/resources/elements/modal-dialog.ts
import { bindable } from 'aurelia-framework';
export class ModalDialog {
@bindable show = false;
@bindable title;
@bindable size = 'md';
@bindable closeOnBackdrop = true;
showChanged() {
if (this.show) {
document.body.classList.add('modal-open');
} else {
document.body.classList.remove('modal-open');
}
}
close() {
this.show = false;
}
onBackdropClick() {
if (this.closeOnBackdrop) {
this.close();
}
}
detached() {
document.body.classList.remove('modal-open');
}
}
<!-- src/resources/elements/modal-dialog.html -->
<template>
<div if.bind="show" class="modal-backdrop" click.delegate="closeOnBackdrop ? close() : ''">
<div class="modal modal-${size}" click.delegate="$event.stopPropagation()">
<div class="modal-header">
<h2>${title}</h2>
<button class="modal-close" click.delegate="close()">×</button>
</div>
<div class="modal-body">
<slot></slot>
</div>
<div class="modal-footer">
<slot name="footer"></slot>
</div>
</div>
</div>
</template>
Data Table Component
// src/resources/elements/data-table.ts
import { bindable } from 'aurelia-framework';
export class DataTable {
@bindable columns;
@bindable rows;
@bindable pageSize = 10;
@bindable sortable = true;
currentPage = 1;
sortColumn = null;
sortDirection = 'asc';
get totalPages() {
return Math.ceil(this.rows.length / this.pageSize);
}
get pagedRows() {
let start = (this.currentPage - 1) * this.pageSize;
return this.rows.slice(start, start + this.pageSize);
}
sort(column) {
if (!this.sortable) return;
if (this.sortColumn === column) {
this.sortDirection = this.sortDirection === 'asc' ? 'desc' : 'asc';
} else {
this.sortColumn = column;
this.sortDirection = 'asc';
}
this.updateRows();
}
updateRows() {
if (this.sortColumn) {
this.rows.sort((a, b) => {
let valA = a[this.sortColumn];
let valB = b[this.sortColumn];
if (valA < valB) return this.sortDirection === 'asc' ? -1 : 1;
if (valA > valB) return this.sortDirection === 'asc' ? 1 : -1;
return 0;
});
}
}
changePage(page) {
this.currentPage = page;
}
}
Common Mistakes
- Not using
@bindabledecorator on properties that need binding. Without@bindable, the property is not accessible from the template as an attribute. - Forgetting to register custom elements globally. Without global registration, each template needs a
requirestatement. - Modifying bound parent data directly. Custom elements should not mutate bound objects. Use events to notify the parent of changes.
- Not implementing
detachedcleanup. Set up DOM listeners and subscriptions inattachedand clean them up indetached. - Over-complicating element APIs. A custom element should have a focused purpose with minimal bindable properties.
Practice Questions
- What does
@bindabledo? - How do you pass content into a custom element?
- How do custom elements communicate changes to parent components?
- What is the purpose of the
changedcallback pattern? - Challenge: Create a
<star-rating>custom element with bindable properties for value (number of stars), max (maximum stars), readonly, and size. Emit achangeevent when the user clicks a star. Support half-star display.
FAQ
Mini Project
Build a component library with these custom elements: (1) <ui-alert> — variants (success, error, warning, info), dismissible, auto-close, icon support. (2) <ui-badge> — color variants, size variants, removable. (3) <ui-avatar> — image, initials fallback, size, status indicator. (4) <ui-progress> — value, max, label, color, animated. Register all globally and create a demo page.
What's Next
Now that you understand custom elements, learn Aurelia Custom Attributes for element behavior. Then explore Aurelia Templating for advanced template features.
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro