SoroPass
Components

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

LayerWhat shipsFramework dependency
Logic / state / a11y / i18n@soropass/ui/headless — state machines + a11y prop-getters + i18n keysnone
Styled reference@soropass/ui/styled — vanilla-DOM mount* renderers + styled.css / tokens.cssnone
Theming surfacetokens.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

  1. 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.
  2. The styled layer is also framework-agnosticmount*Screen(root, { flow, … }) functions that render plain DOM and bind to a headless flow. Every visual value comes from CSS custom properties in tokens.css.
  3. 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.

On this page