---
title: "@query"
description: "Query and cache references to elements within your host DOM."
group: Decorators
order: 7
---
# @query
The `@query` decorator provides a convenient way to query (and optionally cache) references to elements within your host DOM. It creates a getter that performs the query on every access by default, or once and caches the result if you pass `cache: true`.
It works on both `RadiantElement` and `RadiantController`.
## Usage
```typescript
import { RadiantElement, customElement, query } from '@ecopages/radiant';
@customElement('user-profile')
export class UserProfile extends RadiantElement {
// Query by data-ref (recommended)
@query({ ref: 'avatar' }) avatar!: HTMLImageElement;
// Query by CSS selector
@query({ selector: 'button.action' }) submitButton!: HTMLButtonElement;
// Query all matching elements
@query({ ref: 'item', all: true }) items!: HTMLElement[];
}
```
## Parameters
| Parameter | Type | Required | Description |
| :-------- | :--- | :------- | :---------- |
| `selector` | `string` | One of `selector`/`ref` | CSS selector to match elements. |
| `ref` | `string` | One of `selector`/`ref` | Value of `data-ref` attribute to match. |
| `all` | `boolean` | No | Query for all matching elements (default: `false`). |
| `cache` | `boolean` | No | Cache the query result (default: `false`). |
| `scope` | `'light' \| 'shadow' \| 'both'` | No | Which DOM tree to search (default: `'light'`). |
## Feature Highlight
### Caching Behavior
By default, queries are **not** cached — each access re-runs the lookup. For a stable reference that never needs to be re-resolved, pass `cache: true` so repeated access skips the query.
```typescript
@query({ ref: 'stable-el', cache: true })
stableEl!: HTMLElement;
```
### Shadow DOM Scope
By default, queries run against the host's light DOM. Use the `scope` option to query inside a shadow root or both trees.
| Scope | Searches |
| :---- | :------- |
| `'light'` (default) | Host element only |
| `'shadow'` | `host.shadowRoot` only |
| `'both'` | Light DOM first, then shadow root |
```typescript
@query({ selector: '.inner', scope: 'shadow' }) shadowEl!: HTMLElement;
@query({ selector: '.anywhere', scope: 'both', all: true }) allEls!: HTMLElement[];
```
### Type Safety
Use TypeScript's non-null assertion operator (`!`) for required elements, or the optional chaining operator (`?`) if the element might not be present.
```typescript
@query({ ref: 'required-el' }) element!: HTMLElement;
@query({ ref: 'optional-el' }) optional?: HTMLElement;
```
## `@query` vs JSX Bindings
`@query` is for reading DOM you do not directly control from `render()` — content projected into a slot by a consumer, DOM enhanced by a `RadiantController`, or a live `Element` handle needed by a third-party imperative API (positioning libraries, focus management, observers).
If the host does **not** override `render()`, copy the field with [@bindTo](/docs/decorators/bind-to) instead of querying a node only to write into it.
If the element is one your own `render()` JSX produces, prefer a binding instead of querying it back out to write into it:
```tsx
// Avoid — re-fetching a host-rendered node just to write into it
@query({ ref: 'count' }) countEl!: HTMLElement;
@onUpdated('count')
syncCount() {
this.countEl.textContent = String(this.count);
}
override render() {
return ;
}
```
```tsx
// Prefer — bind the value directly at the JSX position
override render() {
return {this.$.count};
}
```
The binding patches just that text node when `count` changes; the query-and-write version does the same job with more code, an extra DOM read, and no benefit. Reach for `@query` when there's no reactive value to bind — you need the node itself, not a value at that position.
## Learn More
- Use `@query({ all: true })` when you want every matching element instead of only the first one.
- [@querySlot](/docs/decorators/query-slot) - Query projected elements assigned to a render-owning RadiantElement slot.
- [RadiantElement](/docs/components/radiant-element) - Learn about `getRef` and other utility methods.