On This Page
The Error
You'll meet this bug in two different costumes depending on when the compiler catches it.
At dev time, in the browser console (ng serve, dev mode), it's a thrown runtime error:
ERROR RuntimeError: NG0304: 'app-button' is not a known element:
1. If 'app-button' is an Angular component, then verify that it is included in the '@Component.imports' of this component.
2. To allow any element add 'NO_ERRORS_SCHEMA' to the '@Component.schemas' of this component.
at createElementRef (core.mjs:...)
at ɵɵelementStart (core.mjs:...)
at AppComponent_Template (app.component.ts:...)
at executeTemplate (core.mjs:...)
at refreshView (core.mjs:...)
at refreshComponent (core.mjs:...)At build time (ng build, or ng build --configuration production), the same underlying problem surfaces as a compile-time diagnostic from ngtsc, and it fails the build outright:
X [ERROR] NG8001: 'app-button' is not a known element:
1. If 'app-button' is an Angular component, then verify that it is
included in the '@Component.imports' of this component.
2. If 'app-button' is a Web Component then add 'CUSTOM_ELEMENTS_SCHEMA'
to the '@Component.schemas' of this component to suppress this message.
src/app/app.component.html:3:1:
3 │ <app-button label="Save"></app-button>
╵ ^
Application bundle generation failed. [0.842 seconds]Same code, same defect, two different presentations — one is a RuntimeError thrown while walking the LView during a dev-mode refresh, the other is a diagnostic emitted by the Ivy template type checker before a single line of JS ships. You'll also see this with a component's own selector name (app-child), with any Angular Material tag (mat-toolbar, mat-icon, mat-form-field are the most common), or with a genuine custom element (<my-web-component>).
If your app is still on NgModules instead of standalone components, the wording is older and doesn't mention @Component at all:
NG8001: 'app-button' is not a known element:
1. If 'app-button' is an Angular component, then verify that it is part of this module.
2. If 'app-button' is a Web Component then add 'CUSTOM_ELEMENTS_SCHEMA' to the '@NgModule.schemas' of this component to suppress this message.That "part of this module" / @NgModule.schemas phrasing predates the standalone-components rewrite of this diagnostic (tracked in angular/angular#45818); the @Component.imports wording above is what every actively supported release (v19+) prints once a component is standalone. Both point at the same root cause: the compiler could not resolve the tag to any directive it knows about.
This article covers NG8001 and its runtime twin NG0304 together, because they're the same failure caught at two different phases — not to be confused with NG8002 (an unknown attribute/property binding like [foo] or ngModel) or NG0300 (two components matching the same selector), which are different bugs with different fixes.
How to Reproduce It
Scaffold a clean standalone app on the current stable:
bash
npx @angular/cli@22 new repro --defaults
cd repropackage.json after install (trimmed):
json
{
"dependencies": {
"@angular/core": "^22.1.0",
"@angular/common": "^22.1.0",
"@angular/platform-browser": "^22.1.0",
"rxjs": "~7.8.0",
"zone.js": "~0.15.0"
},
"devDependencies": {
"@angular/cli": "^22.1.0",
"typescript": "~6.0.0"
}
}Add a tiny standalone child component:
ts
// src/app/app-button.component.ts
import { Component, input } from '@angular/core';
@Component({
selector: 'app-button',
template: `<button type="button">{{ label() }}</button>`,
})
export class AppButtonComponent {
label = input('Click me');
}Now use it from the root component without importing it:
ts
// src/app/app.component.ts
import { Component } from '@angular/core';
@Component({
selector: 'app-root',
imports: [], // <-- AppButtonComponent is missing here
template: `<app-button label="Save"></app-button>`,
})
export class AppComponent {}bash
npm install
npx ng serveOpen the app: the button never renders, and the console shows NG0304: 'app-button' is not a known element, because dev-mode assertions run on every application bootstrap, not just on first compile.
bash
npx ng build --configuration productionThis fails outright with NG8001 and a non-zero exit code, because the production build runs full template type checking and won't emit a bundle with an unresolved element.
Two other shapes of the exact same bug worth knowing, because they show up constantly in the wild:
- NgModule-declared component not exported. If
AppButtonComponentis declared in aSharedModulebut not listed in that module'sexports, any component importingSharedModulegets the "part of this module" wording even though the module is imported — because declaring isn't exporting. - A genuine Web Component. If the tag is a real custom element registered via
customElements.define(...)(a design-system web component, a<lottie-player>, etc.), Angular's compiler has no directive to match it against and reports the same error — the fix here isn't an import, it's a schema (see Section 6). - Test-only failures. angular/angular#66886 documents an Angular 21.1 regression where a
TestBed/Vitest spec that only doesimport type { Foo } from './foo.component'can accidentally pull an unrelated standalone component into AOT compilation for the test build, throwingNG8001in a spec that never even renders that component. If NG8001 only shows up inng test/Vitest runs, check for stray non-typeimports of component files pulled in transitively.
Version Behaviour Matrix
| Version | Behavior |
|---|---|
| v17 | Both codes exist; standalone is opt-in via standalone: true. Wording already mentions @Component.imports for standalone components, @NgModule.schemas/"part of this module" for declared ones. |
| v18 | Unchanged. @angular/build's esbuild-based application builder (stable this release) is what prints the boxed X [ERROR] NG8001: console format shown above; the older webpack builder's format differs slightly in framing but carries the identical message text. |
| v19 | standalone: true becomes the default — omitting standalone no longer means NgModule. Practical effect: far more projects now hit the @Component.imports wording by default, and the "part of this module" wording only appears for components explicitly marked standalone: false. |
| v20 | No change to this diagnostic. Zoneless goes stable in 20.2, which is unrelated to this error — an unknown element fails the same way whether Zone.js is present or not. |
| v21 | No wording change. The TestBed/type-import regression in #66886 (Angular 21.1) is the version-specific gotcha to know about if this error only appears in Vitest. |
| v22 (current, 22.1.x) | Same two codes, same message text. OnPush becoming the default ChangeDetectionStrategy and Signal Forms going stable have no bearing on this error — it's a template-resolution problem, not a reactivity one. |
This error's wording is otherwise version-neutral going back to early Ivy — the meaningful split isn't between Angular versions, it's between standalone components (@Component.imports) and NgModule-declared components (@NgModule.schemas / "part of this module"), and between runtime (NG0304, thrown by ɵɵelementStart during a dev-mode LView refresh) and compile-time (NG8001, emitted by ngtsc's template type checker). Confirmed directly against angular.dev/errors/NG8001 on the current v22.1 docs.
Toolchain note for reproducing this cleanly: v22.0.x requires TypeScript >=6.0.0 <6.1.0 and Node ^22.22.3 || ^24.15.0 || ^26.0.0; v21.x and v20.x both accept TypeScript >=5.8.0/5.9.0 <6.0.0 and an older Node range. A TypeScript mismatch produces a different error ("The Angular Compiler requires TypeScript..."), but it's common to see both errors in the same failed CI run right after a dependency bump — don't let the TS-version message distract you from the actual missing import.
Why It Happens — Surface Level
The template references a tag the compiler can't match to anything: a component you forgot to add to imports, a component declared in a module but never exports-ed, a typo in a selector (<app-buttom>), or a real custom element Angular was never told to allow. Ivy doesn't guess — if it can't prove the tag maps to a directive or a native HTML element, it refuses to compile it silently.
Why It Happens — Under the Hood
Every standalone component compiles down to a call to ɵɵdefineComponent, and part of that compiled definition is a directive matcher — the list of directives/components/pipes the template is allowed to use, built directly from that component's imports array (plus any hostDirectives). This is a closed, per-component set. There is no global registry the way NgModules used to provide one: importing ButtonModule in module A does not make its exported component visible in module B unless B also imports ButtonModule, and with standalone components, importing a component into another component only makes it visible to that one template, full stop. That's a deliberate design change from the NgModule days, where declarations flowed through a transitive imports/exports graph that was notoriously easy to get subtly wrong (declared-but-not-exported, imported-the-wrong-shared-module, etc.) — standalone trades that flexibility for an explicit, statically-analyzable per-component list.
ngtsc, Angular's TypeScript compiler plugin, uses that directive matcher during template type checking: it parses each component's template into an AST, walks every element node, and for each tag checks whether it resolves to (a) a known HTML element per the DOM schema, (b) a directive/component selector present in that matcher, or (c) something explicitly allowed via CUSTOM_ELEMENTS_SCHEMA/NO_ERRORS_SCHEMA. If none of those match, ngtsc emits NG8001 as a compiler diagnostic — this happens during ng build, and in an editor, during ng serve's incremental compilation with strictTemplates (the default for new v17+ projects).
NG0304 is the runtime cousin of the same check. When Angular refreshes a view, ɵɵelementStart (generated inside AppComponent_Template) creates the DOM node and, in dev mode only, the framework runs an assertion pass over the LView's element instructions to confirm every tag either matched a compiled directive or is a valid native element. Production builds strip this assertion (that's ngDevMode being compiled out), which is exactly why you can occasionally ship an app where an unknown dynamic tag never throws in prod but does in ng serve — the compile-time NG8001 check still runs in production builds, but it can only see templates it can statically analyze at build time, so certain dynamically-composed markup slips past it into a build that silently drops the tag instead of erroring.
The #66886 TestBed case is a good illustration of how mechanical this all is: a bare import type { AlarmType } from './alarm.component' shouldn't affect compilation because import type is erased before emit — but a regression in the AOT compiler's dependency analysis for test builds momentarily pulled the component file into the compilation graph anyway, which meant ngtsc tried to resolve that component's own template against its directive matcher as a side effect of compiling an unrelated spec, and threw NG8001 for a component the failing test never touches.
The Fix
Standalone component missing from imports — the common case:
@Component({
selector: 'app-root',
- imports: [],
+ imports: [AppButtonComponent],
template: `<app-button label="Save"></app-button>`,
})
export class AppComponent {}NgModule component declared but not exported:
@NgModule({
declarations: [AppButtonComponent],
- exports: [],
+ exports: [AppButtonComponent],
})
export class SharedModule {}Genuine web component / design-system custom element:
@Component({
selector: 'app-root',
imports: [],
+ schemas: [CUSTOM_ELEMENTS_SCHEMA],
template: `<my-design-system-button label="Save"></my-design-system-button>`,
})
export class AppComponent {}ts
import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';Use CUSTOM_ELEMENTS_SCHEMA only for elements Angular genuinely shouldn't compile (real custom elements registered via customElements.define). Don't reach for it to make a typo go away — that just trades a loud, useful error for a silently-missing button.
What to avoid, and what it costs:
NO_ERRORS_SCHEMA— this is the message's own suggestion, and it's the most dangerous fix on the list: it tells Angular to accept any unknown tag and any unknown attribute binding, project-wide on that component. It doesn't just suppress this error, it suppresses every future typo in that template too. Reasonable temporarily while migrating a huge legacy module; never reasonable as a permanent fix.- Adding the whole feature module "just in case" instead of the specific component — reintroduces the exact transitive-dependency confusion standalone components were built to eliminate, and bloats what that component pulls in for tree-shaking.
- Renaming the selector to match a typo instead of fixing the typo at the call site — fixes this one instance and leaves the next usage broken.
Clean-rebuild recipe, worth running when the error persists after you've clearly added the import (usually a stale .angular/cache hanging onto an old compiled template, or two copies of the component's package):
bash
rm -rf node_modules package-lock.json
npm cache verify
npm install
rm -rf .angular/cache
npx ng cache clean
npm ls @angular/core # confirm exactly one copy
npx ng build --configuration productionBest Practices & The Better Design
Import exactly what a template uses, on the component that uses it — never a barrel re-export of "everything shared," which reintroduces implicit coupling and slows the compiler's dependency analysis. For component libraries and design systems, export a single, well-named standalone component per selector rather than a module that silently re-exports ten others. When a real custom element is unavoidable, scope CUSTOM_ELEMENTS_SCHEMA to the one component that hosts it rather than the app root, so an accidental typo elsewhere in the app still throws.
ts
// The right way: explicit, minimal, and self-documenting
import { Component } from '@angular/core';
import { AppButtonComponent } from './app-button.component';
@Component({
selector: 'app-root',
imports: [AppButtonComponent],
template: `<app-button label="Save"></app-button>`,
})
export class AppComponent {}Prevent It in the Long-Term
Run ng build --configuration production in CI, not just ng serve locally — NG8001 fails the build, so a CI job that only runs ng test/ng serve can merge a PR that's broken in production. Keep strictTemplates: true in tsconfig.json (the v17+ default) so ngtsc catches unknown elements during every incremental compile, not just at build time. Add angular-eslint's template rules to lint on save so a missing import is flagged before you even run the app. If you maintain a shared component library, add a smoke test that imports and renders each exported component in isolation, catching a missing exports entry immediately rather than in a downstream consumer's build. And treat any NO_ERRORS_SCHEMA/CUSTOM_ELEMENTS_SCHEMA usage as something to grep for periodically — it's meant to be narrow and temporary, and it tends to accumulate silently otherwise.
Related: this is the template-resolution failure — distinct from template type-check failures like Property 'x' does not exist on type (a property exists but is mistyped) and from the runtime NG8002/property-binding variant (Can't bind to 'ngModel'), which is its own article.
Learnings
NG8001(build-time) andNG0304(runtime) are the same defect — an element the compiler can't match to any directive — caught at two different phases of the same pipeline.- Standalone components resolve tags from a per-component
importsarray; there's no transitive NgModule-style graph to fall back on, so every component that uses a tag must import it directly. - The "part of this module" wording means the project (or that component) is still NgModule-based; "
@Component.imports" means standalone — same bug, different architecture. - Reach for
CUSTOM_ELEMENTS_SCHEMAonly for genuine custom elements, and never reach forNO_ERRORS_SCHEMAas a permanent fix — it silences every future typo along with this one. - If
NG8001only appears inng test/Vitest, suspect a stray non-typeimport pulling an unrelated component into the test compilation graph (a known Angular 21.1 regression).
CODELZ Newsletter
Join the newsletter to receive the latest updates in your inbox.