# <pc-node>

The `<pc-node>` tag binds to a node inside the hierarchy that a [`<pc-model>`](https://developer.playcanvas.com/user-manual/web-components/tags/pc-model.md) instantiated, and declares overrides against it. It is how you adjust what a GLB was authored with, without editing the GLB: hide a node, move it, give it a component, or parent new content under it.

Where [`<pc-entity>`](https://developer.playcanvas.com/user-manual/web-components/tags/pc-entity.md) *creates* an entity, `<pc-node>` *references* one that the model already created. Its `name` is a lookup, never a rename.

For worked examples of the common adjustments — hiding, re-posing, reskinning, attaching content and adding components — see [Loading Models](https://developer.playcanvas.com/user-manual/web-components/loading-models.md#adjusting-what-you-loaded).

:::note[Usage]

* It must be a descendant of a [`<pc-model>`](https://developer.playcanvas.com/user-manual/web-components/tags/pc-model.md), either directly or nested inside another `<pc-node>`.
* It can have 0..n nested [`<pc-node>`](https://developer.playcanvas.com/user-manual/web-components/tags/pc-node.md) children, which resolve their own `name` within the bound node's subtree.
* It can have 0..n [`<pc-entity>`](https://developer.playcanvas.com/user-manual/web-components/tags/pc-entity.md) children, which are created and parented under the bound node — attachment points for new content.
* It can have the same component tags as a [`<pc-entity>`](https://developer.playcanvas.com/user-manual/web-components/tags/pc-entity.md) — [`<pc-collision>`](https://developer.playcanvas.com/user-manual/web-components/tags/pc-collision.md), [`<pc-light>`](https://developer.playcanvas.com/user-manual/web-components/tags/pc-light.md), [`<pc-script>`](https://developer.playcanvas.com/user-manual/web-components/tags/pc-script.md) and the rest — which add that component to the bound node. A component the node already has, such as the `render` of a mesh node, is not added a second time: the tag warns and does nothing.

:::

## Attributes

| Attribute | Type | Default | Description |
| --- | --- | --- | --- |
| `enabled` | Boolean | *authored* | Overrides the node's enabled state |
| `index` | Number | - | Which match to bind when `name` matches more than one node, 0-based in depth-first order. Required when the name is ambiguous, optional otherwise |
| `material-overrides` | String | *authored* | Overrides the material assignments of the bound node's render component, as a JSON object mapping selectors to `<pc-material>` ids. See [Overriding Materials](https://developer.playcanvas.com/user-manual/web-components/tags/pc-node.md#overriding-materials) |
| `name` | String | - | Name of the node to bind, looked up within the enclosing `<pc-model>` (or `<pc-node>`) |
| `position` | Vector3 | *authored* | Overrides the node's local position as "X Y Z" values |
| `rotation` | Vector3 | *authored* | Overrides the node's local rotation as "X Y Z" Euler angles in degrees |
| `scale` | Vector3 | *authored* | Overrides the node's local scale as "X Y Z" values |
| `tags` | String | *authored* | Overrides the node's tags, as a comma-separated list |

:::note[Overrides, not defaults]

Everything except `name` and `index` is an *override*, so `<pc-node>` reads its absent attributes differently from every other tag. An attribute that is present replaces the authored value; an attribute that is absent leaves it alone. Removing one at runtime — or assigning `null` to the matching JavaScript property — restores the value the model was authored with, rather than the engine default. That is why the table above has no concrete defaults: the default *is* whatever the GLB says.

An override replaces the authored value; it does not compose with it. `position="0 1 0"` puts the node at a local Y of 1, whatever it was exported at.

:::

## Finding the Node

`name` matches on the node names in the loaded hierarchy, and has to match exactly one node in the search scope. Nesting one `<pc-node>` inside another scopes the inner search to the outer node's subtree, which is the simplest way to reach a node whose name is only unique locally.

When a name is not unique within the search scope, the element binds nothing and warns with the paths of every candidate, so you can pick one with `index`:

```none
pc-node 'Wheel' is ambiguous in model 'car' - specify index: [0] Body/Wheel_FL/Wheel, [1] Body/Wheel_FR/Wheel
```

Binding nothing is deliberate: guessing would silently decorate the wrong node, and a re-export that introduced a duplicate name would break a document that used to work.

The other resolution failures warn in the same way — a name that matches nothing (with a node name within two edits of it as a typo hint, when there is one), an `index` beyond the number of matches, and a node that another `<pc-node>` has already bound. In each case the element binds nothing and never becomes ready.

An element only becomes ready once it is bound, and its descendants wait with it. If the model reloads, or the element retargets because you changed `name`, it re-resolves and re-applies its overrides, components and attached content against the new node.

## Overriding Materials

`material-overrides` reskins part of a model without editing the GLB. Its value is a JSON object mapping selectors to [`<pc-material>`](https://developer.playcanvas.com/user-manual/web-components/tags/pc-material.md) ids, so wrap it in single quotes — JSON needs the double ones for itself:

```html
<pc-model asset="car">
    <pc-node name="Body" material-overrides='{"name:CarPaint": "candy-red", "index:3": "smoked-glass"}'></pc-node>
</pc-model>
```

Both selectors address the mesh instances of the bound node's render component:

| Selector | Selects |
| --- | --- |
| `name:X` | Every mesh instance whose material is named `X` |
| `index:N` | Mesh instance `N`, numbered from 0 in the order the render component lists them |

The mapping is sparse: an assignment that no rule matches keeps the material the model was authored with. Where rules of both kinds cover the same mesh instance, `index:` wins — so you can replace a material everywhere it appears by name, then pin the one exception by index.

Use [`<pc-model>`'s `hierarchy()`](https://developer.playcanvas.com/user-manual/web-components/tags/pc-model.md#inspecting-the-hierarchy) to discover the names and indices a node offers. Material names are runtime labels rather than unique identifiers — glTF allows duplicates, the engine calls an unnamed material `Untitled`, gives a primitive authored without a material its shared `defaultGlbMaterial`, and adds `-flatShaded` to the name of a copy it makes for a primitive without normals — so reach for `index:` whenever a name is not distinct.

Names are matched against the assignments captured when the mapping first applied. A rule therefore never matches a material that another rule put there, and renaming a material afterwards cannot change what it selects. Removing the attribute puts every captured assignment back, as does assigning `null` to the `materialOverrides` property or setting an empty `{}`.

The target is the render component the model was authored with. A `<pc-node>` bound to a node that has none warns and changes nothing, and a render component that a child [`<pc-render>`](https://developer.playcanvas.com/user-manual/web-components/tags/pc-render.md) added is never the target.

Rules are validated one at a time, and an invalid one is ignored while the rest of the mapping still applies. Each of these warns: a selector with neither prefix, an `index:` that is not a non-negative integer, an index past the last mesh instance, a name that matches no assignment (the warning lists the names that are there), and an id that resolves to no `<pc-material>`. A value that is not a JSON object at all — malformed JSON, or an array — warns and is treated as absent, restoring the captured assignments rather than leaving the previous mapping in force.

A `<pc-material>` added to the document *after* a mapping referenced it is not picked up on its own. Assign the mapping again once the element exists and the rule resolves.

## Events

`<pc-node>` dispatches the same [pointer events](https://developer.playcanvas.com/user-manual/web-components/tags/pc-entity.md#events) as [`<pc-entity>`](https://developer.playcanvas.com/user-manual/web-components/tags/pc-entity.md), fired when the pointer is over the bound node's geometry. Binding a node is what makes it a pick target, so a `<pc-node>` is also how you make one part of a model interactive. A hit on a part that no `<pc-node>` fronts targets the [`<pc-model>`](https://developer.playcanvas.com/user-manual/web-components/tags/pc-model.md) instead.

| Event | Description |
| --- | --- |
| `click` | Fired when a primary pointer button is pressed and then released over the node. |
| `pointercancel` | Fired on the node a press began over when the browser cancels the press, for example because a touch became a scroll. No `click` follows. |
| `pointerdown` | Fired when a pointer button is pressed over the node. |
| `pointerenter` | Fired when the pointer moves onto the node or an entity below it, having been over none of them. Does not bubble. |
| `pointerleave` | Fired when the pointer moves off the node and every entity below it. Does not bubble. |
| `pointermove` | Fired when the pointer moves over the node. |
| `pointerout` | Fired when the pointer moves off the node. `relatedTarget` is the element it moved onto. |
| `pointerover` | Fired when the pointer moves onto the node. `relatedTarget` is the element it came from. |
| `pointerup` | Fired when a pointer button is released over the node. |

Like every DOM event, these propagate through the element tree, not through the model's node hierarchy. A hit on geometry below the bound node targets this `<pc-node>` unless a nearer `<pc-node>` fronts it. In that case the event reaches this one only if the nearer `<pc-node>` is nested inside it in the markup; if the two are siblings, it bubbles straight to the `<pc-model>`.

The inline `onclick` and `onpointer*` attributes work here exactly as they do on [`<pc-entity>`](https://developer.playcanvas.com/user-manual/web-components/tags/pc-entity.md), including [how a click resolves](https://developer.playcanvas.com/user-manual/web-components/tags/pc-entity.md#clicks) when the press and release land on different geometry.

## Example

This GLB instantiates two mesh nodes — `play` (the orange shell, its logo cut out of each face) and `canvas` (the dark inner box you see through the cutouts) — alongside an empty `Light` and `Camera` left over from its export. [`hierarchy()`](https://developer.playcanvas.com/user-manual/web-components/tags/pc-model.md#inspecting-the-hierarchy) is how you discover that. The `<pc-node>` binds `play` and swaps its authored orange for blue via `material-overrides`. Try binding `canvas` instead, or add `enabled="false"` to hide the shell entirely. Drag to orbit:

```html live-example
<pc-app>
    <pc-asset src="https://cdn.jsdelivr.net/npm/playcanvas@2.22.6/scripts/esm/camera-controls.mjs"></pc-asset>
    <pc-asset src="https://developer.playcanvas.com/assets/playcanvas-cube.glb" id="cube"></pc-asset>
    <pc-material id="repaint" name="Repaint" diffuse="#4a9eff"></pc-material>
    <pc-scene>
        <pc-entity name="camera" position="0 0 3">
            <pc-camera clear-color="#1d1f2b"></pc-camera>
            <pc-script>
                <pc-script-instance name="cameraControls" enable-pan="false" zoom-range="1.5 6"></pc-script-instance>
            </pc-script>
        </pc-entity>
        <pc-entity name="light" rotation="45 30 0">
            <pc-light intensity="2"></pc-light>
        </pc-entity>
        <pc-model asset="cube">
            <!-- Bind the node named "play" and swap in the repaint material -->
            <pc-node name="play" material-overrides='{"index:0": "repaint"}'></pc-node>
        </pc-model>
    </pc-scene>
</pc-app>
```

## JavaScript Interface

You can programmatically create and manipulate `<pc-node>` elements using the [NodeElement API](https://api.playcanvas.com/web-components/classes/NodeElement.html).

Alongside `entity`, which is the node it bound, the element reports how resolution went. `state` is `"pending"` while it has nothing to bind (no name yet, or the model has not instantiated), `"bound"` once it has, and `"missing"`, `"ambiguous"` or `"duplicate"` when resolution failed. `path` is the `/`-separated path of the bound node below the search scope, or `null` while unbound. Together they let you assert a document's bindings rather than reading the console:

```javascript
import { whenReady } from '@playcanvas/web-components';

const node = await whenReady('pc-node[name="Roof"]');
console.log(node.state, node.path); // 'bound' 'Body/Roof'
```

The `materialOverrides` property is the mapping described in [Overriding Materials](https://developer.playcanvas.com/user-manual/web-components/tags/pc-node.md#overriding-materials), as an object rather than a JSON string:

```javascript
const body = await whenReady('pc-node[name="Body"]');
body.materialOverrides = { 'name:CarPaint': 'candy-red' };
body.materialOverrides = null; // back to the authored materials
```

The element stores a frozen copy of what you assign, so mutating your object afterwards changes nothing — assign a new mapping to change one. Property writes do not reflect back to the `material-overrides` attribute, which follows how the other override properties behave.

## See Also

* [`<pc-model>`](https://developer.playcanvas.com/user-manual/web-components/tags/pc-model.md) — the model whose node is bound
* [`<pc-material>`](https://developer.playcanvas.com/user-manual/web-components/tags/pc-material.md) — materials a node can substitute via `material-overrides`
* [`<pc-entity>`](https://developer.playcanvas.com/user-manual/web-components/tags/pc-entity.md) — attaches new content under a node
* [Loading Models](https://developer.playcanvas.com/user-manual/web-components/loading-models.md) — finding the nodes inside a loaded model

Examples: [Product Viewer](https://playcanvas.github.io/web-components/examples/#product-viewer.html), [Ragdoll](https://playcanvas.github.io/web-components/examples/#ragdoll.html) and [Vehicle Physics](https://playcanvas.github.io/web-components/examples/#vehicle-physics.html).
