Skip to content

Ember Components — Reusable UI Building Blocks

DodaTech Updated 2026-06-28 5 min read

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 — template
  • app/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">
    &times;
  </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}}>&times;</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

  1. Modifying @args directly. Args are read-only. Use tracked properties for internal state and call actions to modify parent state.
  2. Not using @tracked on reactive properties. Without @tracked, changes do not trigger re-renders. The UI becomes stale.
  3. Creating components without a .js file when not needed. Template-only components are valid and simplify code. Do not create a JS file unless you need logic.
  4. Mutating arrays and objects instead of replacing them. @tracked detects assignment, not mutation. Use this.items = [...this.items, newItem] instead of this.items.push(newItem).
  5. Over-nesting component hierarchies. Keep component trees 3-4 levels deep. Deeper hierarchies are hard to debug and slow to render.

Practice Questions

  1. What is the difference between @args and @tracked?
  2. How do you call a parent action from a child component?
  3. What is the purpose of {{yield}}?
  4. How do you handle DOM events in a component?
  5. Challenge: Create a DataTable component that accepts columns and rows as args, supports sorting by column click, and has a loading state. Each column can have a custom cell renderer component.

FAQ

What is a template-only component?

A component without a .js file. It has no logic, only template. Useful for simple presentational components.

Can I use native DOM APIs in components?

Yes, but use {{did-insert}} modifier for DOM access. Avoid direct DOM manipulation in tracked properties.

How do I pass multiple named blocks to a component?

Ember does not support named blocks natively. Use multiple yield or component arguments.

What is the difference between classic and Glimmer components?

Glimmer components are the modern standard. They use tracked properties, no this. in template, and are faster.

Can components have their own CSS?

Yes. Co-locate CSS files with components. Ember's CSS modules or Tailwind supports this.

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