Component basics | The Aurelia 2 Docs
Component basics
Components are the building blocks of Aurelia applications. This guide covers creating, configuring, and using components effectively.
Components are the core building blocks of Aurelia applications. Each component typically consists of:
A TypeScript class (view model)
Component Naming
Component names must include a hyphen (e.g., user-card, nav-menu) to comply with Web Components standards. Use a consistent prefix like app- or your organization's initials for better organization.
Creating Your First Component
The simplest way to create a component is with convention-based files:
<div class="user-card">
<h3>${name}</h3>
<p>${email}</p>
</div>Aurelia automatically pairs user-card.ts with user-card.html by convention, creating a <user-card> element you can use in templates.
Use the @customElement decorator for explicit configuration:
import { customElement } from 'aurelia';
@customElement({
name: 'user-card',
template: `
<div class="user-card">
<h3>\${name}</h3>
<p>\${email}</p>
</div>
`
})
export class UserCard {
name = 'John Doe';
email = '[email protected]';
}For simple naming, use the shorthand syntax:
Key @customElement options:
Template Configuration:
Importing external HTML templates with bundlers
When a component imports an .html file, the bundler must deliver that file as a plain string. Otherwise tools such as Vite, Webpack, and Parcel try to parse the file as an entry point and emit errors like [vite:build-html] Unable to parse HTML; parse5 error code unexpected-character-in-unquoted-attribute-value or "template" is not exported by src/components/product-name-search.html.
Configure your bundler using the option that best matches your stack:
Vite / esbuild (default Aurelia starter), Parcel 2, Rollup +
@rollup/plugin-string– append?rawto the import so the bundler treats the file as text:Add a matching declaration so TypeScript understands these imports (the query string can be reused for other text assets):
Webpack 5 – mark
.htmlfiles asasset/source(or keep usingraw-loader). After that you can import without a query parameter:Other bundlers – use the equivalent “treat this file as a string” hook (e.g., SystemJS
textplugin).
Once the bundler understands .html files as text, both npm start and npm run build can reuse the same component source without inline templates. Keep the import pattern consistent across the project so contributors immediately know which loader configuration applies.
Dependencies:
Alternative Creation Methods
Static Configuration:
Programmatic (mainly for testing):
Create simple components with just HTML:
Usage:
Components that handle DOM manipulation through third-party libraries:
Global Registration (in main.ts):
Local Import (in templates):
Render component content without wrapper tags:
Or configure inline:
Use Sparingly
Containerless components lose their wrapper element, which can complicate styling, testing, and third-party library integration.
Components follow a predictable lifecycle. Implement only the hooks you need:
Components accept data through bindable properties:
See Bindable Properties for complete configuration options.
Enable Shadow DOM for complete style and DOM encapsulation:
Shadow DOM is useful for:
Complete style isolation (styles won't leak in or out)
Creating reusable components with predictable styling
Using native
<slot>elements for content projectionBuilding design systems and component libraries
See the Shadow DOM guide for detailed configuration, styling patterns, and best practices.
Transform markup before compilation:
Apply Aurelia to existing elements:
Watch for property changes:
Child Element Observation
Attribute Capture:
Aliases:
Single Responsibility: Each component should have one clear purpose
Type Safety: Use interfaces for complex data structures
Composition: Favor composition over inheritance
Use
attached()for DOM-dependent initializationClean up subscriptions in
detaching()Prefer
@watchover polling for reactive updatesConsider Shadow DOM for style isolation
Mock dependencies properly
Test lifecycle hooks and bindable properties
Write tests for error scenarios
See Testing Components for detailed guidance.
Components form the foundation of Aurelia applications. Start with simple convention-based components and add complexity as needed. The framework's flexibility allows you to adopt patterns that fit your project's requirements while maintaining clean, maintainable code.
Last updated
- Creating Your First Component
- Component Configuration
- Configuration Options
- Importing external HTML templates with bundlers
- Alternative Creation Methods
- HTML-Only Components
- Viewless Components
- Using Components
- Containerless Components
- Component Lifecycle
- Bindable Properties
- Advanced Features
- Shadow DOM
- Template Processing
- Enhancing Existing DOM
- Reactive Properties
- Child Element Observation
- Component Configuration
- Best Practices
- Component Design
- Performance
- Testing
@customElement('user-card')
export class UserCard {
// Component logic
}import template from './custom-template.html?raw';
@customElement({
name: 'data-widget',
template, // External file
})
export class DataWidget {}
@customElement({
name: 'inline-widget',
template: '<div>Inline template</div>',
})
export class InlineWidget {}
@customElement({
name: 'viewless-widget',
template: null,
})
export class ViewlessWidget {}import template from './product-name-search.html?raw';declare module '*.html?raw' {
const content: string;
export default content;
}// webpack.config.cjs
module.exports = {
module: {
rules: [
{ test: /\.html$/i, type: 'asset/source' },
],
},
};import template from './product-name-search.html';
declare module '*.html' {
const content: string;
export default content;
}import { ChildComponent } from './child-component';
@customElement({
name: 'parent-widget',
dependencies: [ChildComponent] // Available without <import>
})export class UserCard {
static $au = {
type: 'custom-element',
name: 'user-card'
};
}import { CustomElement } from '@aurelia/runtime-html';
const MyComponent = CustomElement.define({
name: 'test-component',
template: '<span>\${message}</span>'
});<bindable name="status"></bindable>
<bindable name="message"></bindable>
<span class="badge badge-\${status}">\${message}</span><import from="./status-badge.html"></import>
<status-badge status="success" message="Complete"></status-badge>import { bindable, customElement } from 'aurelia';
import * as nprogress from 'nprogress';
@customElement({
name: 'progress-indicator',
template: null
})
export class ProgressIndicator {
@bindable loading = false;
loadingChanged(newValue: boolean) {
newValue ? nprogress.start() : nprogress.done();
}
}import Aurelia from 'aurelia';
import { UserCard } from './components/user-card';
Aurelia
.register(UserCard)
.app(MyApp)
.start();<import from="./user-card"></import>
<!-- or with alias -->
<import from="./user-card" as="profile-card"></import>
<user-card user.bind="currentUser"></user-card>
<profile-card user.bind="selectedUser"></profile-card>import { customElement, containerless } from 'aurelia';
@customElement({ name: 'list-wrapper' })
@containerless
export class ListWrapper {
// Component logic
}@customElement({
name: 'list-wrapper',
containerless: true
})
export class ListWrapper {}export class UserProfile {
constructor() {
// Component instantiation
}
binding() {
// Before bindings are processed
}
bound() {
// After bindings are set
}
attached() {
// Component is in the DOM
}
detaching() {
// Before removal from DOM
}
}import { bindable, BindingMode } from 'aurelia';
export class UserCard {
@bindable user: User;
@bindable isActive: boolean = false;
@bindable({ mode: BindingMode.twoWay }) selectedId: string;
userChanged(newUser: User, oldUser: User) {
// Called when user property changes
}
}<user-card
user.bind="currentUser"
is-active.bind="userIsActive"
selected-id.two-way="selectedUserId">
</user-card>import { customElement, useShadowDOM, shadowCSS } from 'aurelia';
@customElement({
name: 'isolated-widget',
template: '<div class="widget"><slot></slot></div>',
dependencies: [
shadowCSS(`
.widget {
border: 1px solid var(--widget-border, #ddd);
padding: 16px;
}
`)
]
})
@useShadowDOM({ mode: 'open' })
export class IsolatedWidget {
// Styles and DOM are fully encapsulated from outside
}import { customElement, processContent, INode } from 'aurelia';
@customElement({ name: 'card-grid' })
export class CardGrid {
@processContent()
static processContent(node: INode) {
// Transform <card> elements into proper markup
const cards = node.querySelectorAll('card');
cards.forEach(card => {
card.classList.add('card-item');
// Additional transformations...
});
}
}import { resolve, Aurelia } from 'aurelia';
export class DynamicContent {
private readonly au = resolve(Aurelia);
async enhanceContent() {
const element = document.getElementById('server-rendered');
await this.au.enhance({
host: element,
component: { data: this.dynamicData }
});
}
}import { watch, bindable } from 'aurelia';
export class ChartWidget {
@bindable data: ChartData[];
@bindable config: ChartConfig;
@watch('data')
@watch('config')
onDataChange(newValue: any, oldValue: any, propertyName: string) {
this.updateChart();
}
}import { children, slotted } from 'aurelia';
export class TabContainer {
@children('tab-item') tabItems: TabItem[];
@slotted('tab-panel') panels: TabPanel[];
tabItemsChanged(newItems: TabItem[]) {
this.syncTabs();
}
}import { capture, customElement } from 'aurelia';
@customElement({ name: 'flex-wrapper' })
@capture() // Captures all unrecognized attributes
export class FlexWrapper {}import { customElement } from 'aurelia';
@customElement({
name: 'primary-button',
aliases: ['btn-primary', 'p-btn']
})
export class PrimaryButton {}import { bindable, resolve } from 'aurelia';
import { ILogger } from '@aurelia/kernel';
interface User {
id: string;
name: string;
email: string;
}
export class UserProfile {
@bindable user: User;
private readonly logger = resolve(ILogger);
attached() {
this.logger.info('Profile loaded', { userId: this.user.id });
}
}