Most of the Ember work on our desks this year has a version number attached: get to the current LTS, finish Octane, get Embroider building. The template tag sits slightly to the side of that queue, and teams keep asking where it belongs. Short answer: it is the direction the framework is going, it is available now on any reasonably current version, and it is the one modernization you can adopt one file at a time with no coordination cost. That combination makes it unusual, and worth understanding before you decide when to start.
What actually changes
Today a component is two files that find each other by filename convention: app/components/invoice-row.js and app/components/invoice-row.hbs. The template can reference any component in the resolver's namespace, whether or not anything in your code imports it. That resolution is implicit, global, and invisible to tooling.
With the template tag, the template moves inside a single .gjs or .gts file and the things it uses are imported:
import Component from '@glimmer/component';
import { tracked } from '@glimmer/tracking';
import { on } from '@ember/modifier';
import CurrencyAmount from './currency-amount';
export default class InvoiceRow extends Component {
@tracked expanded = false;
toggle = () => { this.expanded = !this.expanded; };
<template>
<tr {{on "click" this.toggle}}>
<td>{{@invoice.number}}</td>
<td><CurrencyAmount @cents={{@invoice.totalCents}} /></td>
</tr>
</template>
}
That is the whole idea. Components, helpers, and modifiers become imported values rather than resolved names. Built-ins come from real modules — on from @ember/modifier, fn and concat from @ember/helper, LinkTo from @ember/routing.
The consequences are mundane in the best way. "Go to definition" works from the template. Renaming a component is a refactor your editor can perform. An unused component shows up as an unused import instead of as a file nobody dares delete. And a bundler can finally see which components are actually reachable, which is the part that matters if Embroider and tree-shaking are on your roadmap.
Prerequisites, honestly stated
This is not a first step. Before converting anything:
- Be on Octane idioms in the files you are converting. The template tag is a Glimmer-component feature. Classic components with
didInsertElement, mixins, and two-way bound properties need converting first — our Octane post covers that order. - Be on a recent LTS. Template tag support has been in the mainline toolchain for a while now, but the ergonomics — editor support, linting, type checking — are markedly better the closer you are to current. If you are three LTS lines back, upgrade first.
- Have the tooling installed and verified. The build support, the ESLint and template-lint configuration, the Prettier plugin, and the language-server integration in whatever editors your team uses. Convert one trivial component and confirm the whole loop works — lint, format, test, autocomplete — before touching anything real. A half-configured toolchain makes the new format feel worse than the old one, and that impression is hard to undo.
- Have tests that run. Conversion is mechanical, but mechanical changes at scale still need a net.
Use the codemod, then read the diff
Hand-converting components is a waste of senior time. The community codemod for template tag conversion handles the bulk transformation: merging the .hbs into the class file, adding imports for the components and helpers the template referenced, and removing the old file. Run it on one directory at a time and review the output.
What needs human attention afterwards is almost always resolution that the codemod could not see through. Components invoked dynamically by name, anything reached through a string that ends up in {{component}}, helpers provided by addons with unusual export shapes. These are exactly the places where implicit resolution was hiding something, so treat each one as a small finding rather than an annoyance. Fix the dynamic invocation to pass a component value instead of a name, and the code gets better as well as converted.
Route templates and the app/templates tree are a separate question and generally come later; start with components, where the payoff per file is highest.
Convert in the order your team benefits
We sequence conversions the same way we sequence most modernization work — by where the team spends its time, not by directory listing:
- A pilot slice. One small, self-contained feature area. Convert it fully, ship it, and let the team live with it for a sprint. The goal is a review conversation, not throughput.
- Files you are already editing. Make conversion part of touching a component for feature work. This spreads the change across the team and keeps the diff attached to work that is being reviewed anyway.
- The design-system components. High reuse, low churn, and the place where import-based references pay off most across the rest of the app.
- Everything else, opportunistically. There is no deadline. The two formats coexist in the same app indefinitely; a
.gjscomponent can render a classic two-file component and vice versa.
This matters: the template tag is not a migration with a cutover date. It is a format you can hold at any percentage of adoption. Teams that treat it as a project to be finished create pressure they do not need; teams that treat it as a default for new work converge anyway.
If you write TypeScript, the payoff is larger
Typing a two-file component means typing the class and hoping the template agrees. In a .gts file the template is inside the type-checked unit: arguments, yielded values, and the components you import are all checked against real signatures. Teams already invested in TypeScript get the most out of adopting the template tag, and it is a reasonable argument for starting sooner. Teams with no TypeScript get the tooling and bundling benefits and none of the type checking, which is still worth having but is a smaller prize.
When to wait
We have told clients to leave this alone, and the reasons repeat:
- The app is not on Octane yet. Do that work first; converting classic components to the template tag in one motion produces diffs nobody can review.
- An upgrade or a build migration is mid-flight. Two structural changes at once makes bisecting a failure miserable. Finish one.
- The app is on a confirmed path off Ember. Converting components you will delete in three quarters is a poor use of the budget — though if the migration is long and the team is still shipping features in Ember daily, a light touch on the files they edit most can still pay for itself.
- The team is one person on partial capacity. Format changes have a real cost in review attention, and that person's attention is better spent on LTS currency.
Otherwise, the honest assessment is that this is low-risk, incrementally adoptable, aligned with where the framework is heading, and pleasant to work in. Install the tooling, convert one slice, and make it the default for new components. The rest can take as long as it takes.
If you want a view on where the template tag sits relative to your upgrade and build work, that ordering question is exactly what an assessment answers.