Skip to content

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 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);
}

Released under the MIT OR Apache-2.0 License.