The build fails with:
✘ [ERROR] NG8001: 'app-user-card' is not a known element:
1. If 'app-user-card' is an Angular component, then verify that it is included in the '@Component.imports' of this component.
2. If 'app-user-card' is a Web Component then add 'CUSTOM_ELEMENTS_SCHEMA' to the '@Component.schemas' of this component to suppress this message. [plugin angular-compiler]
src/app/team.ts:7:4:
7 │ <app-user-card [user]="member" />
╵ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~The compiler met a tag with a dash in its name, looked for a component with that selector among everything this template is allowed to use, and found nothing. Angular 19 and 20 print the same text. If the host component is declared in an NgModule, the hints change to “verify that it is part of this module” and '@NgModule.schemas'. It usually arrives together with NG8002 errors for every input you bound on that tag, which disappear once the element is known.
Quick fix checklist
- Standalone host: add the child’s class to the host’s
importsarray (and the TypeScriptimportline). - Compare the tag with the child’s
selectorexactly:app-user-cardvsapp-usercardvsapp-user-cards. - NgModule host: the child must be declared in the same module, or exported by a module that this module imports.
- Child is standalone but listed in
declarations: move it toimports(the build says NG6008). - Child is not standalone but listed in a standalone component’s
imports: import its module instead (the build says NG2011). - Do not reach for
CUSTOM_ELEMENTS_SCHEMAunless the tag is a real Web Component. - In tests, check what the test module imports, declares or stubs.
Before you start
Know whether each component involved is standalone. Since Angular 19 every component is standalone unless it says standalone: false. A standalone template’s scope is its own imports array; an NgModule component’s scope is its module’s declarations plus the exports of every module that module imports. The standalone imports note walks through this in detail. Have the child component’s file open so you can read its real selector.
Why it happens
Angular compiles each template against a fixed list of directives and components, not against the whole app. That is a deliberate design choice: it lets the compiler type-check inputs, tree-shake unused components and keep each component self-describing. When the compiler sees <app-user-card>, it matches the tag against the selectors of everything in scope. No match means one of these:
- The component is not in scope. The standalone host never imported it, or the NgModule chain does not export it to this module.
- The selector does not match. A typo, a renamed component, a prefix changed in
angular.json, or a component whose selector is an attribute ([appTooltip]) being used as a tag. - The wrong thing is in scope. A standalone component in
declarations, or a non-standalone component inimports. These now produce their own errors first:
✘ [ERROR] NG6008: Component UserCard is standalone, and cannot be declared in an NgModule. Did you mean to import it instead?
✘ [ERROR] NG2011: The component 'OldBadge' appears in 'imports', but is not standalone and cannot be imported directly.
It's declared in the 'WrongModule' NgModule, but is not exported. Consider exporting it and importing the NgModule instead.The compiler only treats tags with a dash as candidates for Web Components, which is why hint 2 mentions CUSTOM_ELEMENTS_SCHEMA. For a tag without a dash, hint 2 suggests NO_ERRORS_SCHEMA instead.
Step-by-step walkthrough
Step 1: Read the location and the hint wording
The file path and underline tell you which template failed, so the fix goes in that component, not in the child. The hint wording tells you which world you are in: '@Component.imports' means a standalone host; “part of this module” means an NgModule host.
Step 2: Confirm the selector
Open the child and copy its selector rather than retyping it:
import { Component, input } from '@angular/core';
@Component({
selector: 'app-user-card',
template: `<p>{{ user() }}</p>`,
})
export class UserCard {
readonly user = input.required<string>();
}If the selector is [appUserCard] or app-user-card[compact], it only matches attributes or specific combinations, and a plain <app-user-card> tag will not match.
Step 3: Fix the scope (standalone)
import { Component } from '@angular/core';
import { UserCard } from './user-card';
@Component({
selector: 'app-team',
imports: [UserCard],
template: `
@for (member of members; track member) {
<app-user-card [user]="member" />
}
`,
})
export class Team {
members = ['Ada', 'Grace'];
}Import the class itself. Your editor’s auto-import will happily pull a same-named class from another library or a stale file, so check the import path too.
Step 4: Fix the scope (NgModule)
A component declared in one module is private to it until exported:
@NgModule({
declarations: [UserCard],
imports: [CommonModule],
exports: [UserCard], // without this, other modules cannot use <app-user-card>
})
export class SharedModule {}
@NgModule({
declarations: [TeamPage],
imports: [CommonModule, SharedModule], // TeamPage's template can now use <app-user-card>
})
export class TeamModule {}Exports are not transitive through AppModule: importing SharedModule in AppModule does nothing for components declared in TeamModule. Each declaring module imports what its own templates need. To consume a standalone component from an NgModule, put it in that module’s imports.
Worked scenario
A team adds a status badge to an existing NgModule-based dashboard. The badge is a new standalone component; the dashboard is declared in DashboardModule.
// status-badge.ts
@Component({
selector: 'app-status-badge',
template: `<span class="badge">{{ status() }}</span>`,
})
export class StatusBadge {
readonly status = input('ok');
}
// dashboard.module.ts (broken)
@NgModule({
declarations: [DashboardPage, StatusBadge],
imports: [CommonModule],
})
export class DashboardModule {}The build fails with NG6008. A developer moves StatusBadge into AppModule’s imports instead, and now gets:
✘ [ERROR] NG8001: 'app-status-badge' is not a known element:
1. If 'app-status-badge' is an Angular component, then verify that it is part of this module.Diagnosis: DashboardPage’s scope is DashboardModule, and AppModule’s imports do not reach it. The fix is to import the standalone component where the declaring module can see it:
@NgModule({
declarations: [DashboardPage],
imports: [CommonModule, StatusBadge],
})
export class DashboardModule {}Common mistake
Adding CUSTOM_ELEMENTS_SCHEMA to make it go away. The schema tells the compiler “any dashed tag and any property on it might be a Web Component, trust me”. Verified on Angular 20: with the schema on the host, <app-usercard [user]="x" /> (missing dash) compiles without a single error and renders an empty <app-usercard> element. You lose the typo check and the input type check for every dashed tag in that template. Use the schema only on the specific components that render real custom elements (Lit, Stencil, Shoelace and similar), and keep it out of shared components. NO_ERRORS_SCHEMA is broader still and also hides unknown properties on native elements.
Importing the child into AppModule or app.config.ts. Template scope is per component (or per declaring module). There is no global registration for components.
Copying imports arrays wholesale. Importing every shared component everywhere “just in case” hides scope mistakes and makes the dependency graph meaningless. Import what the template uses.
Verify the behavior
ng build (or a running ng serve) is the main check, because NG8001 is a compile-time error: a clean build proves every tag in AOT-compiled templates resolves.
Tests are different, because they are JIT-compiled by default and report the problem at runtime as NG0304. Standalone components under test bring their own imports, so the real children render; to isolate a component, swap the child for a stub:
import { Component, input } from '@angular/core';
import { TestBed } from '@angular/core/testing';
import { Team } from './team';
import { UserCard } from './user-card';
@Component({ selector: 'app-user-card', template: `stub {{ user() }}` })
class UserCardStub {
readonly user = input('');
}
describe('Team', () => {
it('renders one card per member', () => {
TestBed.overrideComponent(Team, {
remove: { imports: [UserCard] },
add: { imports: [UserCardStub] },
});
const fixture = TestBed.createComponent(Team);
fixture.detectChanges();
expect(fixture.nativeElement.querySelectorAll('app-user-card').length).toBe(2);
expect(fixture.nativeElement.textContent).toContain('stub Ada');
});
});For components declared through TestBed.configureTestingModule({ declarations: [...] }), a missing child throws NG0304: 'app-user-card' is not a known element (used in the 'Legacy' component template) when errorOnUnknownElements is on (CLI-generated setups enable it). For a standalone component that forgot an import, ng test on Angular 20 only logs the NG0304 to the console and keeps going, so assert on rendered output as above rather than relying on the error.
Interview exercise
“Your teammate fixed NG8001 by adding schemas: [CUSTOM_ELEMENTS_SCHEMA] to a component. The build is green. What do you say in code review?”
Answer and reasoning
Ask whether the tag is a real Web Component. If it is an Angular component, the schema did not fix anything: it told the compiler to stop checking dashed tags in that template. The component is still not in scope, so Angular will not instantiate it; the page renders an empty custom element, its inputs become plain DOM properties, and no error appears in development or production. Worse, every future typo in that template is now silent too.
The correct fix is to bring the component into scope (add it to imports, or export it from its module and import that module) and confirm the selector. If the template mixes real Web Components with Angular components, keep the schema but move the custom elements into a small wrapper component that owns the schema, so the rest of the templates stay fully checked. Mentioning that the check exists because templates compile against an explicit scope (for type-checking and tree-shaking) shows you understand why the rule exists.