Ember Components — Reusable UI Building Blocks
In this tutorial, you will learn about Ember Components. We cover key concepts, practical examples, and best practices to help you master this topic.
Ember components are reusable UI elements that encapsulate template, JavaScript logic, and styles. Modern Ember uses Glimmer components with tracked properties for reactive updates, arguments for data passing, and actions for event handling.
What You'll Learn
You will learn how to create Glimmer components, pass data with @args, use @tracked for reactivity, handle events with actions, and compose components into complex interfaces.
Why It Matters
Components are the building blocks of modern Ember applications. They enforce Encapsulation, prevent style leakage, and make UI code reusable across routes and applications.
Real-World Use
A design system has 40+ components: Button, Card, Modal, Table, Form inputs, and Navigation. Each component is independently tested, documented, and reused across the entire application.
flowchart TD
A[Component] --> B[.hbs Template]
A --> C[.js Logic]
A --> D[.css Styles]
B --> E[{{@args}}]
C --> F[@tracked]
C --> G[@action]
B --> H[{{yield}}]
Creating a Component
Use Ember CLI to generate components:
ember generate component ui-button
This creates:
app/components/ui-button.hbs— templateapp/components/ui-button.js— JavaScript (optional)app/components/ui-button.css— styles (optional)
Basic Component with Args
Args are values passed to the component. Access them with @ in the template.
{{! app/components/user-avatar.hbs }}
<div class="user-avatar" title={{@user.name}}>
{{#if @user.avatarUrl}}
<img src={{@user.avatarUrl}} alt={{@user.name}} loading="lazy" />
{{else}}
<div class="avatar-initials">
{{initials @user.name}}
</div>
{{/if}}
</div>
// app/components/user-avatar.js
import Component from '@glimmer/component';
export default class UserAvatarComponent extends Component {
get initials() {
return this.args.user.name
.split(' ')
.map(n => n[0])
.join('')
.toUpperCase();
}
}
Tracked Properties
Use @tracked for reactive state that triggers re-renders when changed.
// app/components/counter.js
import Component from '@glimmer/component';
import { tracked } from '@glimmer/tracking';
import { action } from '@ember/object';
export default class CounterComponent extends Component {
@tracked count = 0;
@tracked history = [];
@action
increment() {
this.count++;
this.history = [...this.history, 'increment'];
}
@action
decrement() {
this.count--;
this.history = [...this.history, 'decrement'];
}
@action
reset() {
this.count = 0;
this.history = [];
}
get isPositive() {
return this.count > 0;
}
get isNegative() {
return this.count < 0;
}
}
{{! app/components/counter.hbs }}
<div class="counter {{if this.isPositive 'positive'}} {{if this.isNegative 'negative'}}">
<button type="button" {{on "click" this.decrement}}>-</button>
<span class="count">{{this.count}}</span>
<button type="button" {{on "click" this.increment}}>+</button>
<button type="button" {{on "click" this.reset}}>Reset</button>
</div>
{{#if this.history.length}}
<ul class="history">
{{#each this.history as |entry|}}
<li>{{entry}}</li>
{{/each}}
</ul>
{{/if}}
Actions with Arguments
Pass actions from parent to child components.
// app/components/todo-item.js
import Component from '@glimmer/component';
import { action } from '@ember/object';
export default class TodoItemComponent extends Component {
@action
toggleComplete() {
if (this.args.onToggle) {
this.args.onToggle(this.args.todo.id);
}
}
@action
delete() {
if (this.args.onDelete) {
this.args.onDelete(this.args.todo);
}
}
}
{{! app/components/todo-item.hbs }}
<li class="todo-item {{if @todo.completed 'completed'}}">
<input
type="checkbox"
checked={{@todo.completed}}
{{on "change" this.toggleComplete}}
/>
<span class="title">{{@todo.title}}</span>
<button type="button" {{on "click" this.delete}} aria-label="Delete">
×
</button>
</li>
Component with Yield (Content Projection)
Use {{yield}} to project content into components.
// app/components/modal-dialog.js
import Component from '@glimmer/component';
import { tracked } from '@glimmer/tracking';
import { action } from '@ember/object';
export default class ModalDialogComponent extends Component {
@tracked isOpen = false;
@action
open() {
this.isOpen = true;
}
@action
close() {
this.isOpen = false;
if (this.args.onClose) {
this.args.onClose();
}
}
}
{{! app/components/modal-dialog.hbs }}
{{#if this.isOpen}}
<div class="modal-backdrop" {{on "click" this.close}}>
<div class="modal-content" {{on "click" (fn this.preventDefault)}}>
<div class="modal-header">
<h2>{{@title}}</h2>
<button type="button" {{on "click" this.close}}>×</button>
</div>
<div class="modal-body">
{{yield}}
</div>
</div>
</div>
{{/if}}
Component Lifecycle
// app/components/lifecycle-demo.js
import Component from '@glimmer/component';
import { tracked } from '@glimmer/tracking';
export default class LifecycleDemoComponent extends Component {
@tracked data = null;
constructor(owner, args) {
super(owner, args);
console.log('1. constructor — component created');
}
get processedData() {
console.log('2. getter — computing derived data');
return this.args.rawData ? this.args.rawData.toUpperCase() : '';
}
willDestroy() {
console.log('3. willDestroy — cleaning up');
super.willDestroy();
}
}
Contextual Components
Components can be passed around as values:
{{! app/components/data-table.hbs }}
<table>
<thead>
<tr>
{{#each @columns as |column|}}
<th>{{column.label}}</th>
{{/each}}
</tr>
</thead>
<tbody>
{{#each @rows as |row|}}
<tr>
{{#each @columns as |column|}}
<td>
{{#if column.component}}
<column.component @data={{get row column.key}} />
{{else}}
{{get row column.key}}
{{/if}}
</td>
{{/each}}
</tr>
{{/each}}
</tbody>
</table>
Common Mistakes
- Modifying
@argsdirectly. Args are read-only. Use tracked properties for internal state and call actions to modify parent state. - Not using
@trackedon reactive properties. Without@tracked, changes do not trigger re-renders. The UI becomes stale. - Creating components without a
.jsfile when not needed. Template-only components are valid and simplify code. Do not create a JS file unless you need logic. - Mutating arrays and objects instead of replacing them.
@trackeddetects assignment, not mutation. Usethis.items = [...this.items, newItem]instead ofthis.items.push(newItem). - Over-nesting component hierarchies. Keep component trees 3-4 levels deep. Deeper hierarchies are hard to debug and slow to render.
Practice Questions
- What is the difference between
@argsand@tracked? - How do you call a parent action from a child component?
- What is the purpose of
{{yield}}? - How do you handle DOM events in a component?
- Challenge: Create a
DataTablecomponent that accepts columns and rows as args, supports sorting by column click, and has aloadingstate. Each column can have a custom cell renderer component.
FAQ
Mini Project
Build a component library with: (1) Button — variants (primary, secondary, danger), sizes, loading state, disabled state. (2) Card — title, subtitle, image, actions slot, footer. (3) FormField — label, input, error message, hint text. (4) Toast — success, error, warning variants with auto-dismiss. Each component must be independently usable with documented args.
What's Next
Now that you understand components, learn Ember Component Lifecycle for lifecycle hooks. Then explore Ember Helpers for template transformations.
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro