# Integration FAQ

Common questions and pitfalls from real-world integrations.

## Composition patterns (React migration view)

### Where are asChild / Slot / Portal equivalents here?

The asChild / Slot / Portal concepts from the React ecosystem solve component-composition problems that grew out of the React model (eliminating library-imposed wrapper elements, mounting content elsewhere). Under Web Components those premises either do not exist or already have equivalents:

- **Swapping the root element (the wrapper-elimination job of asChild / Slot)** — a Web Component itself (its host) *is* a real element in the DOM, so external layouts and selectors act on it directly; there is no library-imposed wrapper to strip. When a component must render as a different semantic element (e.g. a button that behaves as a link), use **attribute-driven root switching**: when `href` is present, buttons/tags render a native `<a>` internally and keep keyboard & disabled semantics (see the oas-button / oas-tag docs).
- **Using an arbitrary element as a trigger** (e.g. the common `Trigger asChild` composition) — the equivalent is the **default slot + a trigger attribute**: overlay components treat the default slot's first child as the trigger (any element / component), and a `trigger` attribute picks the mode (`click` / `hover` / `contextmenu`, …); panel content goes through the `content` attribute or named slots (`title` / `header` / `content` / `footer`, …). Events and ARIA are wired up internally by the component.
- **Mounting content into `body` or another container (Portal / Teleport)** — the equivalent is the **`append-to` system**: overlay/pinned components such as tooltip / popover / modal / drawer accept `append-to="body"` to move the overlay into a target container while preserving style scoping and stacking contexts; removing it returns the node and leaves no orphaned elements.

In short: the composition trio under Web Components is "**attribute-driven root element + named slots + append-to**", which covers the scenarios asChild / Slot / Portal solve — no same-named API is needed when migrating.

### React / Vue: what if an attribute name collides with a native DOM property?

When passing attributes to a custom element, if the name matches an **existing DOM property** on the element, the host writes it as a **property** (`el[name] = value`) instead of `setAttribute` — React 19 does this, and Vue's **bound** syntax (`:name`) hits the same rule for custom elements (source: `key in el`; except `spellcheck` / `draggable` / `translate` / `autocorrect`, which always go through attributes).

How OAS-UI handles it:

- **`prefix` / `suffix` / `blur`**: `prefix` collides with the read-only `Element.prefix`, and `blur` (oas-backdrop) with the `HTMLElement.blur()` method; `suffix` has no DOM counterpart and is handled symmetrically. The relevant components (input / input-number / statistic / countdown / tree-select / backdrop / mentions) **shadow** these names with property accessors mapped to the canonical attributes **`prefix-text` / `suffix-text` / `blur` / `trigger`** (mentions' `prefix` is a legacy alias of `trigger`) — so they work through **both channels**: React's `prefix="¥"` / `blur="blur(8px)"`, Vue's static/bound syntax, and plain HTML all behave. `prefix-text` / `suffix-text` are the canonical names and `prefix` / `suffix` are compatibility aliases; shadowing `blur` replaces that element's **native `blur()` method with the attribute accessor** (no practical impact on a backdrop). Under TypeScript, `blur` still shows the inherited method type (methods can't be declaration-merged), so to assign it as a property use the attribute or `setAttribute` instead.
- **Other writable colliding attributes** (`title`, `dir`, `role`, `hidden`, `draggable`, `spellcheck`, `autofocus`, `id`, …) are **writable** DOM properties, so React / Vue writing them as properties reflects back to attributes normally; note `title` also fires the browser's **native tooltip**.
- If you build your **own** custom element, avoid attribute names that match **read-only** `Element` / `HTMLElement` properties or methods (e.g. `prefix`, `blur`) — or shadow them with accessors the same way.

## Events

### Why do all events carry the `oas-` prefix?

`oas-` events are emitted through the core's shared `emit()`, which explicitly
sets `bubbles + composed` so they escape the Shadow DOM to window (note: the
platform's native `CustomEvent` defaults both to `false` — the escape is a
library convention, not a platform default). Unprefixed `change` / `select`
would collide with native events (e.g. the text-selection `select` event) and
host-framework synthetic events, making debugging painful. The prefix makes the
origin unambiguous — **listen for `oas-change`, not `change`**.

Each component's docs list its events and when they fire (see the "Events"
section of the component docs).

### Difference between `oas-input` and `oas-change`?

Same as native semantics: `oas-input` fires during continuous interaction
(e.g. every frame while dragging a slider); `oas-change` fires on commit
(release / Enter / blur). For low-frequency work such as auto-save, listen to
`oas-change` to avoid writing on every pixel of a drag.

### How do I read controlled state?

Controlled components (switch / radio-group / checkbox-group / slider /
input-number, etc.) write the latest value **back to the host attribute** after
interaction: `el.getAttribute('value')` / `el.hasAttribute('checked')` read the
current state directly, consistent with the event `detail` — no caching needed
on your side.

## Style customization

### No attribute selectors after `::part()`

Per the CSS specification, the `::part()` pseudo-element can only be followed
by pseudo-classes (`:hover` / `:focus`, etc.), never attribute selectors.
Writing `::part(item)[aria-expanded='true']` makes the **entire rule get
silently dropped** by the browser — including other comma-separated selectors
in the same rule — with no error whatsoever.

```css
/* correct */
#menu::part(item):hover {
  /* ... */
}

/* wrong: the whole rule dies (comma-separated siblings die with it) */
#menu::part(item):hover,
#menu::part(item)[aria-expanded='true'] {
  /* ... */
}
```

Alternatives for attribute-based states: components usually mirror state to a
CSS class or expose a dedicated `::part()`; prefer those. As a last resort,
operate on `shadowRoot` from JS.

### `::part()` cannot reach shadow-internal descendants

`::part()` only matches elements the component explicitly exposes; you
**cannot select their descendants** (`::part(item) .check` does not work). To
customize internals, look for deeper exposed parts, or pierce with CSS custom
properties (each component's "Style customization" section lists the available
variables).

### How do I make my own CSS follow light/dark themes?

Reference theme variables: `background: var(--oas-color-bg)`,
`color: var(--oas-color-text-primary)`. When `data-theme` switches, the library
variables swap automatically and anything referencing them follows. See
[Design Tokens](./tokens) for the full list.

## Internationalization (i18n)

### What is the placeholder syntax for translation interpolation?

The translation-interpolation placeholder in `@oas-ui/i18n` is **`{name}`** — a
single pair of braces with **no `#` prefix**. Write `{count}` in the language-pack
template and pass `t(key, { count })` to substitute, e.g.
`'pagination.total': '{total} items'` + `t('pagination.total', { total: 42 })` →
`42 items`.

> **`#{name}` is not a valid placeholder**: the `#` is never consumed by the
> interpolation logic and stays verbatim in the output (after substitution it is
> still `#{42}`). Do not carry over `#{var}`-style templating habits when writing
> language packs.

### How do I register a custom language pack / texts?

`@oas-ui/i18n` provides a global locale registry: `registerLocale(locale)`
registers a custom language pack and `setLocale(name)` switches globally; the pack
shape follows the built-in `zh-CN` (the key set is fully typed, missing keys are
compile-time errors). `oas-config-provider` supports local injection (the `locale`
attribute): components inside it prefer the injected locale for built-in texts
without any global setup.

For languages beyond the built-in 10: use the `zh-CN` pack's messages as a template → spread `...zhCN.messages` to inherit all keys and replace them one by one → tag `messages` as a `Locale` (type guardrail: missing keys fail at compile time) → mark `dir: 'rtl'` for RTL languages → `registerLocale` → `setLocale`. See the `@oas-ui/i18n` package README, "Custom locale packs" section, for the full step-by-step example. Ship custom packs with your own dynamic `import()` for code splitting (`loadLocale()` only maps the 10 built-in names).

### Which languages are built in? Can they be loaded on demand?

Ten locales ship built in: `zh-CN` (default), `en`, `ja`, `ko`, `de`, `fr`, `es`,
`pt`, `ru`, `ar` (RTL). The main entry only carries `zh-CN`; the rest are loaded
**on demand** and never enter the initial bundle:

```ts
import { loadLocale, setLocale } from '@oas-ui/i18n'
await loadLocale('ja') // dynamic import('@oas-ui/i18n/ja'), a separate chunk
setLocale('ja')
```

You can also import a pack statically (tree-shaking: only what you use ships):

```ts
import ja from '@oas-ui/i18n/ja'
setLocale(ja)
```

Date and number formatting go through native `Intl`
(`Intl.DateTimeFormat` / `Intl.NumberFormat`), so these locales display correctly
with no extra translations; the default `first-day-of-week` is derived from the
locale too (Monday for European/Chinese, Sunday for Japanese/Korean/English/Arabic).

### How are RTL locales handled?

The `ar` pack is tagged `dir: 'rtl'`, so switching to `ar` mirrors floating-layer
positioning automatically (select / combobox / auto-complete / tree-select /
date-picker / time-picker / mentions, …); you can also enable it explicitly or
locally with `oas-config-provider direction="rtl"`. Direction is resolved as
config-provider > nearest ancestor `dir` > `document.dir` > current locale.

## Theming

### The page body stays white after switching to dark?

The body is outside any component's Shadow DOM and is never styled
automatically. Reference the variables explicitly:

```css
body {
  background: var(--oas-color-bg);
  color: var(--oas-color-text-primary);
}
```
