Framework decision
SoroPass UI ships framework-agnostic at every layer — headless logic and a vanilla-DOM styled layer, with no React/Vue/Svelte runtime dependency. Here is why, and how to adopt it in any framework.
SoroPass UI is framework-agnostic at every layer. There is no React, Vue, or Svelte runtime dependency anywhere in @soropass/ui. Because the headless layer already owns all logic, state, accessibility, and i18n, a framework wrapper is ~20 lines, not a re-implementation.
The layers
| Layer | What ships | Framework dependency |
|---|---|---|
| Logic / state / a11y / i18n | @soropass/ui/headless — state machines + a11y prop-getters + i18n keys | none |
| Styled reference | @soropass/ui/styled — vanilla-DOM mount* renderers + styled.css / tokens.css | none |
| Theming surface | tokens.css (CSS custom properties) | none (CSS) |
The same shape covers all five flows: create · sign · recover · connect · add-device. Each is a headless state machine plus an optional styled screen bound to it.
The decision
- The headless core is the product. The create / sign / recover / connect / add-device state machines, the a11y prop-getters (
getTriggerProps,getStatusProps,getListProps,getOptionProps), and the i18n message keys. Zero DOM, zero framework. - The styled layer is also framework-agnostic —
mount*Screen(root, { flow, … })functions that render plain DOM and bind to a headless flow. Every visual value comes from CSS custom properties intokens.css. - No framework wrapper ships as the default. Adopters drop in a thin wrapper (below). A first-class React package, if asked for, is additive and low-risk — a wrapper, not a rewrite.
Why framework-agnostic (not "React first")
- Minimal API surface. A React/React-DOM peer dependency is the single heaviest thing we could add to a minimal, headless SDK. Staying lean is the point.
- No design-system lock-in. "Looks great out of the box, re-theme by tokens" lives in
tokens.css, not in a component framework. Framework-free keeps the promise literal. - Prop-getters compose everywhere. A
{ role, 'aria-live', onClick, … }object spreads onto a React element, a Vue template, or a DOM node identically. - Widest adoption surface. A wallet team on Svelte, Lit, or vanilla adopts the styled layer as-is; a React team adds a 20-line wrapper. A React-only package would exclude everyone else.
Considered and declined
- A React component package — rejected as the default (heaviest dependency, narrows adoption); kept as a documented, trivial wrapper.
- Web Components / custom elements — a reasonable framework-agnostic alternative, but it adds a registration / shadow-DOM model and complicates token inheritance and testing for no fidelity gain over plain
mount.
How adopters consume it
Vanilla / any framework:
import { createCreatePasskeyFlow } from '@soropass/ui/headless';
import { mountCreateScreen } from '@soropass/ui/styled';
import '@soropass/ui/styled.css';
const flow = createCreatePasskeyFlow({ create, userActivation: navigator.userActivation });
const { unmount } = mountCreateScreen(document.getElementById('slot')!, { flow });React (thin wrapper — no package needed):
function CreatePasskey(props: CreateScreenOptions) {
const ref = useRef<HTMLDivElement>(null);
useEffect(() => mountCreateScreen(ref.current!, props).unmount, []);
return <div ref={ref} className="pk" />;
}Vue: the same shape in onMounted / onUnmounted. Lit / Web Component: call mount* in connectedCallback, return the handle's unmount from disconnectedCallback.
Re-theme contract
Override tokens on :root or any wrapping scope — never edit components:
.my-wallet {
--pk-color-brand: oklch(0.7 0.13 190); /* teal */
--pk-radius-lg: 4px;
}
.my-wallet[data-theme='dark'] {
/* dark skin ships in tokens.css */
}See Theming for the full token reference and light / dark screenshots proving a single token swap restyles every one of the 22 component states with no code change.