# Resolver widget
URL: https://docs.arcns.io/widget/

> The drop-in <arcns-resolve> web component from @arcns/resolve-widget, with the script-tag and framework snippets, the attribute table, the events, the render states, theming, and accessibility.

Testnet

`<arcns-resolve>` is a web component that does the whole resolve-and-render step for a recipient field, a search box or an explorer row: the user types `@alice`, `alice.arc` or `alice.circle` (or pastes a `0x…` address) and the element shows the address, the **namespace**, the **registry** it was read from and the **verified** flag, then tells your code through an event. It wraps [`@arcns/resolve`](/quickstart/) and follows the [wallet](/wallet/) recipe exactly; use it when you would rather not write that UI yourself. Package: `@arcns/resolve-widget`, MIT, `0.1.0-testnet.N` on the `testnet` dist-tag.

> **Rule**
> The widget never shows a bare address. Every answer carries its namespace badge, the registry short-address and the verification badge, and a mixed shape such as `@alice.arc` is refused before any read. Resolution is single-registry and authoritative: `.arc` and `.circle` names resolve only against the FortiBlox ArcNS registry, so there is never a second registry's answer to weigh against it.

## Drop it in [#drop-it-in]

> **Not on npm yet**
> As of 2026-09-14, the `@arcns` npm scope has not been created — neither `@arcns/resolve-widget` nor `@arcns/resolve` is on the npm registry, so the CDN URL and `npm install` command below are the shape the install takes once they publish, not something that resolves today. Both packages are fully built in the `sdk-widget/` and `sdk/` directories of the (private) [repository](https://forgejo.selfhsted.com/fortiblox/fortiblox-arcns); until the first testnet release lands, that source is the only way to get the widget. To integrate today with no package at all, call [the HTTP API](/api/) directly.

One script tag, one element, once published. The IIFE build bundles the SDK and the parts of `viem` it needs; nothing else is loaded.

```html
<script src="https://cdn.jsdelivr.net/npm/@arcns/resolve-widget@testnet/dist/arcns-resolve.iife.js"></script>

<arcns-resolve rpc-url="https://rpc.testnet.arc.io"></arcns-resolve>

<script>
  document.addEventListener("arcns-resolve", (e) => {
    const { name, namespace, registry, address, verified } = e.detail;
    // enable "Send" only now, with all four on screen
  });
  document.addEventListener("arcns-error", (e) => {
    // e.detail.code: not-found · no-record-for-chain · no-primary · ambiguous · tld-retired · rpc-error · …
  });
</script>
```

With a bundler, once published:

**npm**

```bash
npm install @arcns/resolve-widget@testnet @arcns/resolve@testnet viem
```

**pnpm**

```bash
pnpm add @arcns/resolve-widget@testnet @arcns/resolve@testnet viem
```

**yarn**

```bash
yarn add @arcns/resolve-widget@testnet @arcns/resolve@testnet viem
```

**bun**

```bash
bun add @arcns/resolve-widget@testnet @arcns/resolve@testnet viem
```

```ts
import { defineArcnsResolve } from "@arcns/resolve-widget";
defineArcnsResolve(); // idempotent; a no-op during server-side rendering
```

`@arcns/resolve` and `viem` are peer dependencies of the ESM build.

## What it shows [#what-it-shows]

| State     | On screen                                                                                                                                                                                                                                  |
| --------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| idle      | a hint naming the accepted shapes                                                                                                                                                                                                          |
| invalid   | the rule the input breaks (`A hyphen cannot start or end a name.`), or that `@name.arc` is two different names. Nothing is looked up while the user is still typing.                                                                       |
| loading   | `Resolving alice.arc…`; the region is `aria-busy`                                                                                                                                                                                          |
| resolved  | namespace badge (`@handle`, `.arc`, `.circle`), the canonical name, **Verified** or **Unverified**, the checksummed address with a copy button, and `registry 0x0a1D…2FE4 · ArcNS`; `· ETH record` when the coin-type-60 fallback answered |
| not-found | `alice.arc is not registered.`, or `@arcns is registered but has no ARC address record. Nothing to send to.`, or `0x0b94…209D has no primary name (…)`                                                                                     |
| error     | `Couldn't read the chain. Try again.` with **Retry**; a retired TLD; a missing configuration                                                                                                                                               |

The state is mirrored to a `state` attribute on the element, so a page can style `arcns-resolve[state="resolved"]`. The status area reserves its height, so moving between states never shifts the content below it.

### `verified` on the badge [#verified-on-the-badge]

Forward (name to address): **Verified** means the owner proved control of that address for that coin type on chain, `verified(node, coinType)` on the ArcNS resolver. **Unverified** means a record exists but nobody proved it: the badge is a warning and its tooltip says so. The name's controller is never shown as a payment address.

Reverse (address to name): a primary is returned only after it forward-confirms, so the badge is always **Verified** and the tooltip reads "forward-confirmed". An address whose primary no longer confirms is not-found, never the stale name. This is the same meaning the [API](/api/) gives `addr.verified` and the reverse route's `verified: true`; the full definition is on [Verified records](/verified/).

### Single registry, one answer [#single-registry-one-answer]

`.arc` and `.circle` lookups resolve only against the FortiBlox ArcNS registry — single-registry, winner-take-all (CEO decision 2026-09-09), the same rule the [SDK](/quickstart/#single-registry-authoritative) and the [API](/api/) apply. There is no `resolveAll()`, no second registry to compare against, and no per-row **Use** button: a name this registry does not hold is a plain `not-found`, exactly like any other unregistered name. An earlier build of the widget rendered a multi-registry `ambiguous`/`conflict` state with `candidates` and `picked` fields on the events; all of that has been removed. `ambiguous` now names only the mixed-shape refusal below.

## Attributes [#attributes]

Every attribute has a camelCase property (`rpc-url` is `rpcUrl`).

| Name | Type | Description | Default |
| --- | --- | --- | --- |
| `value` | `string` | initial input; the property reads and writes the field, and writing resolves at once |  |
| `mode` | `"auto" \| "resolve" \| "reverse"` | `resolve` (names only), `reverse` (addresses only), `auto` (a `0x` + 40 hex address is reversed, anything else is a name) | auto |
| `rpc-url` | `string` | JSON-RPC endpoint for the SDK | the chain's default |
| `chain-id` | `number` | chain to read; one the SDK ships a deployment for, or give an RPC | 5042002 |
| `api-url` | `string` | when set, lookups use `GET {api-url}/resolve/:name` and `/reverse/:address` instead of the RPC; the public instance, `https://api.arcns.io`, is free only for first-party ArcNS origins and `X-API-Key` holders — see below |  |
| `chain` | `"ARC" \| "ETH" \| "BTC" \| "SOL" \| "X1"` | which record to show; `ARC` falls back to the coin-type-60 record the way the API does | ARC |
| `namespaces` | `string` | which namespaces the field accepts; a name outside the list is `invalid` with the namespace named | handle,arc,circle |
| `placeholder` | `string` | input placeholder | per mode |
| `label` | `string` | accessible label, visually hidden | Name or address |
| `readonly` | `boolean` | the field cannot be typed into; set `value` from code |  |
| `theme` | `"auto" \| "light" \| "dark"` | `light`, `dark`, or follow `prefers-color-scheme` | auto |
| `debounce` | `number` | quiet milliseconds before a lookup; `0` looks up on every keystroke; Enter looks up at once | 300 |

Properties only: `resolver` (a shared `@arcns/resolve` `Resolver`; overrides `rpc-url` and `chain-id`), `state`, `result`, `focus()`, `refresh()`.

Reads through `api-url` are free for the ArcNS sites (`arcns.io`, `app.arcns.io`, `docs.arcns.io`) and for callers presenting an ArcNS-issued `X-API-Key`; every other origin gets `402 Payment Required` and is charged per call over x402 — see [Agents: pay per read with x402](/api/#agents-pay-per-read-with-x402). The widget has no x402 client built in, so a `402` from a non-first-party `api-url` renders as a plain `error` state, not a retry or a payment prompt. If you're embedding on a site other than the ones above, use `rpc-url` instead (the SDK reads the chain directly and is always free) or front `api-url` with your own paying proxy.

## Events [#events]

Both bubble and are composed, so one listener on `document` covers every instance.

| Event           | `detail`                                                                                                                                                                                 |
| --------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `arcns-resolve` | `{ input, mode, name, namespace, registry, product, address, verified, chainId, coinType? }`                                                                                             |
| `arcns-error`   | `{ input, code, message, reason?, status? }`; `code` is one of `not-found`, `no-record-for-chain`, `no-primary`, `ambiguous`, `tld-retired`, `rpc-error`, `not-configured`, `http-error` |

Invalid input while typing emits nothing; the mixed shape `@name.arc` does emit `ambiguous`, because it is a decision rather than a typo. A lookup the user has typed past emits nothing at all: `fetch` requests are aborted and RPC answers are discarded.

## Frameworks [#frameworks]

The element is plain DOM. Register it once on the client, then use the tag.

**React**

```tsx
import { useEffect, useRef } from "react";
import { defineArcnsResolve, type ArcnsResolveDetail, type ArcnsResolveElement } from "@arcns/resolve-widget";

export function Recipient({ onAddress }: { onAddress: (d: ArcnsResolveDetail) => void }) {
  const ref = useRef<ArcnsResolveElement>(null);
  useEffect(() => {
    defineArcnsResolve();
    const el = ref.current!;
    const handler = (e: CustomEvent<ArcnsResolveDetail>) => onAddress(e.detail);
    el.addEventListener("arcns-resolve", handler);
    return () => el.removeEventListener("arcns-resolve", handler);
  }, [onAddress]);
  return <arcns-resolve ref={ref} rpc-url="https://rpc.testnet.arc.io" mode="resolve" />;
}
```

No React dependency in the package. React 19 passes custom-element attributes through as written; on React 18 declare the tag in `JSX.IntrinsicElements` and keep the `addEventListener` above.

**Vue**

```vue
<script setup lang="ts">
import "@arcns/resolve-widget/auto";
const onResolve = (e: CustomEvent) => console.log(e.detail);
</script>
<template>
  <arcns-resolve rpc-url="https://rpc.testnet.arc.io" @arcns-resolve="onResolve" />
</template>
```

Set `compilerOptions.isCustomElement = (tag) => tag.startsWith("arcns-")` so Vue leaves the tag alone.

**Svelte**

```svelte
<script lang="ts">
  import "@arcns/resolve-widget/auto";
</script>
<arcns-resolve rpc-url="https://rpc.testnet.arc.io" on:arcns-resolve={(e) => console.log(e.detail)} />
```

**No markup**

```ts
import { mount } from "@arcns/resolve-widget";
const field = mount("#recipient", {
  mode: "resolve",
  resolver: arcns,                 // your app's shared @arcns/resolve instance, optional
  onResolve: (d) => enableSend(d),
  onError: (d) => disableSend(d.code),
});
field.setValue("alice.arc");
field.destroy();
```

## Theming [#theming]

Styles live in the shadow root and read custom properties from the page. Each defaults to the ArcNS brand token for the active theme, and every text and background pair in both themes meets WCAG AA.

```css
arcns-resolve {
  --arcns-bg: #fffefc;
  --arcns-fg: #1b1816;
  --arcns-fg-muted: #6b635c;
  --arcns-border: rgba(27, 24, 22, 0.18);
  --arcns-accent: #b93d0a;
  --arcns-ok: #1f7a3a;
  --arcns-warn: #a12d5e;
  --arcns-radius: 12px;
  --arcns-font: "Geist", system-ui, sans-serif;
  --arcns-font-mono: "Geist Mono", ui-monospace, monospace;
  --arcns-status-min-height: 6.25rem;
}
```

Also `--arcns-surface`, `--arcns-fg-secondary`, `--arcns-accent-soft`, `--arcns-accent-strong` (accent text on the tinted namespace badge; `#35c5ff` in dark, because the base accent falls below AA on the tint), `--arcns-accent-contrast`, `--arcns-ok-soft`, `--arcns-warn-soft`. CSS parts for deeper changes: `root`, `field`, `input`, `status`, `result`, `namespace`, `verified`, `address`, `copy`, `meta`.

## Accessibility [#accessibility]

A real `<label>` (the `label` attribute, visually hidden); the status area is `role="status" aria-live="polite"` and referenced from the input by `aria-describedby`; `aria-invalid` and `aria-busy` follow the state; badges carry text, with the verification meaning in the tooltip and read to screen readers; every control is a `<button>` with a name; the input is 44 px tall; one `:focus-visible` ring; `prefers-reduced-motion` collapses the spinner.

## Questions [#questions]

Questions: [support@arcns.io](mailto:support@arcns.io).
