Which selector forms can an Angular @Component selector use, and when would you pick an attribute selector such as button[app-primary]?
answer
- a subset of CSS
- no combinators, only :not()
- matched once, at compile time
- keep the native element's behaviour
basics
~20 sAn Angular component selector may be an element name, an attribute with an optional exact value, a class, a combination, a comma list or :not(), never a combinator. An attribute selector like button[app-primary] keeps the native button's behaviour.
solid answer
~40 sAngular supports a subset of CSS for component selectors: element names, `[attr]` and `[attr=value]`, classes, combinations like `button[app-primary]`, comma-separated lists and `:not()`. Combinators, other pseudo-classes and attribute operators such as `^=` are not supported; matching is decided per element, at compile time, never across element boundaries. An element can match only one component; since v22 two matches are the compile error NG8023. Use a prefixed element name like `app-user-card` by default. Use an attribute selector when the component enhances a native element whose behaviour you want to keep: on `<button app-primary>` the consumer still gets focus, keyboard activation, `type="submit"`, `disabled` and ARIA attributes. The cost is that a forgotten import gives no error.
code
ts · 26 linesimport { Component, input, signal } from '@angular/core';
@Component({
selector: 'button[app-primary]',
template: `<ng-content />`,
host: {
'class': 'btn btn-primary',
'[attr.aria-busy]': 'busy()',
},
})
export class PrimaryButton {
busy = input(false);
}
@Component({
selector: 'app-draft-editor',
imports: [PrimaryButton],
template: `
<button app-primary type="submit" [busy]="saving()" aria-label="Save draft">
Save
</button>
`,
})
export class DraftEditor {
saving = signal(false);
}go deeper
Recall the element, attribute and class forms and the app- prefix convention; know that ng is reserved for Angular.
Explain compile-time static matching, why combinators are unsupported, and the one-component-per-element rule with NG8023 in v22.
Justify attribute selectors for native-element components by the accessibility and form behaviour they preserve, and name the silent-missing-import trade-off.
Weigh a design system's selector conventions: element wrappers versus attribute components, variant inputs versus several colliding components, and how to lint for them.
## What a component selector is Every Angular component has a `selector` in its `@Component` metadata. It is a **CSS selector** that tells the compiler which elements in other components' templates should become instances of this component. The element that matches is the component's **host element**, and the component's template renders inside it. Two properties of selector matching explain most interview follow-ups: - **It is static.** Angular matches selectors at **compile time**, against the components listed in the template's `imports`. Adding an attribute or a class at runtime, through a binding or a DOM API, never creates a component. - **It is exclusive.** An element can match **exactly one** component selector. Since **v22** the compiler reports **NG8023** (*multiple components match node*) when two imported components match the same element; the runtime equivalent is **NG0300**, *selector collision*. Selectors are also **case-sensitive**. ## The supported forms Angular supports a limited subset of CSS: | Form | Example | Matches | |---|---|---| | Type (element) | `app-user-card` | `<app-user-card>` | | Attribute | `[app-primary]` | any element carrying that attribute | | Attribute with value | `button[type="reset"]` | exact value match only, with `=` | | Class | `.menu-item` | elements with that class in the template | | Combined | `button[app-primary]` | a `<button>` with the attribute | | `:not()` | `[dropzone]:not(textarea)` | narrows another selector | | List | `drop-zone, [dropzone]` | either form | What is **not** supported: - combinators of any kind: descendant (`form button`), child (`form > button`), sibling; - attribute operators other than `=`, such as `^=` or `*=`; - pseudo-classes other than `:not`, and pseudo-elements; - namespaces. Combinators are ruled out by the matching model: Angular decides per element, from that element's own tag, attributes and classes, and never looks across element boundaries. The other restrictions are simply the subset Angular chose to implement. ## When an attribute selector is the right choice Most components should use a **custom element name** with a project prefix, such as `app-user-card`. The CLI uses `app-` by default, and the `ng` prefix is reserved for Angular itself. An element selector has a safety net: if the component is not imported, the build fails with NG8001. An **attribute selector** is the right choice when the component enhances a **native element** whose built-in behaviour you want to keep. The classic case is a design-system button: ```ts import { Component, input } from '@angular/core'; @Component({ selector: 'button[app-primary]', template: `<ng-content />`, styles: `:host { font-weight: 600; }`, host: { 'class': 'btn btn-primary', '[attr.aria-busy]': 'busy()', }, }) export class PrimaryButton { busy = input(false); } ``` A consumer writes `<button app-primary type="submit" aria-label="Save draft">Save</button>`. Because the host element *is* the native `<button>`, the consumer keeps, for free: 1. keyboard activation and focus behaviour; 2. form submission through `type="submit"`; 3. the `disabled` attribute and its semantics; 4. any ARIA attribute, such as `aria-label`, without the component re-exposing it as an input. A wrapper element such as `<app-primary-button>` rendering its own inner `<button>` would have to forward every one of those by hand, and it adds an extra non-interactive element around the real button. ## The trade-offs of attribute selectors - **No safety net.** An unknown attribute is legal HTML, so if the consumer forgets to import `PrimaryButton`, the button renders as a plain native button with no error. Binding an input on it, such as `[busy]="saving()"`, does surface NG8002, because `busy` is not a property of `<button>`. - **Naming.** The docs recommend lowercase, dash-case attribute names for components, with the same project prefix. (Directive attribute selectors conventionally use camelCase.) - **Collisions.** Two attribute-selector components on the same element are an error, which is why a design system usually exposes variants as inputs of one component rather than as several components. ## The `host` field The `host` object in the metadata binds properties, attributes, classes, styles and events on the host element. For an attribute-selector component that element is the native button itself, so `host: { 'class': 'btn btn-primary' }` styles the real button. The docs recommend the `host` object over the `@HostBinding` and `@HostListener` decorators, which remain for backwards compatibility.
- Does adding the app-primary attribute to a button at runtime create the component?No. Angular matches selectors statically when it compiles the template. Setting an attribute or class later, through a binding or a DOM API, changes the DOM but never instantiates or removes a component. To create one from code you use the programmatic creation APIs instead.
- Why can't a component selector be written as form > button?Angular component selectors do not support combinators. The compiler decides whether a component applies by looking at one element's own tag, attributes and classes; it does not reason about ancestors or siblings. If behaviour depends on context, express it with an input or with dependency injection from the parent.
- Why use lowercase dash-case for a component's attribute selector?The Angular docs recommend lowercase, dash-case attribute names with the project prefix for components, such as `app-primary`, which also reads like the element-name convention. Component selectors are case-sensitive, so a consistent convention avoids a mismatch between the selector and the attribute authors type.
saying these in an interview costs you the question
- A component selector can use any CSS selector, including descendant combinators.
- Adding the matching attribute at runtime turns an element into the component.
- Two components matching one element are both created and share the host.
- Attribute selectors are only for directives, never for components.
- Forgetting to import an attribute-selector component always fails the build.