Blog

Mach deine React-App mit UI SDK und WebMCP für KI-Agenten bedienbar

Lass KI-Agenten deine React-UI mit Vercels UI SDK lesen und bedienen: createAgent, snapshot, act, allow, Source agent, definePrimitive und WebMCP registerTools.

Preet Suthar · · 10 Min. Lesezeit

  • AI
  • React
  • UI SDK
  • WebMCP

Die meisten KI-Agenten nutzen eine Web-App so, wie es ein Mensch mit einem langsamen Screenreader täte. Sie machen einen Screenshot oder lesen das DOM, raten, welches Element das Dropdown „Region“ ist, klicken darauf, lesen die Seite erneut und klicken eine Option an. Jeder Durchlauf kostet Tokens und Zeit, und jede Vermutung kann falsch sein.

UI SDK, das Vercel Labs am 12. November 2026 veröffentlicht hat, geht einen anderen Weg. Seine Komponenten registrieren sich bei einer Bridge, die einem Agenten sagt, was auf der Seite ist, in welchem Zustand jede Komponente ist und was sie kann, und führt diese Aktionen dann über die eigene API der Komponente aus. Dieselbe Bridge kann ihre Tools über WebMCP an einen Agenten veröffentlichen, der in den Browser eingebaut ist.

Dieser Post baut eine kleine Einstellungsseite in React, verbindet einen Agenten damit und geht die Teile der API durch, auf die es ankommt: createAgent, snapshot(), act(), den allow-Callback, source: "agent" in Change-Callbacks, eine eigene Komponente mit definePrimitive und registerTools. Jede Ausgabe unten ist aus einem Lauf in einer Test-App mit React 19.3, Vite 8 sowie @ui-sdk/react und @ui-sdk/core in 0.1.0-beta.1 kopiert. UI SDK ist in der Beta, Namen und Verhalten können sich also ändern.

Wie die Bridge funktioniert

Deine React-App

  • Input
  • Select
  • Switch
  • Deine definePrimitive-Parts

Die Bridge: createAgent()

  • snapshot() listet Komponenten, Zustand und Aktionen
  • query() findet Komponenten und ihre Optionen
  • act() führt Schritte über die API jeder Komponente aus
  • allow genehmigt oder lehnt jeden Schritt ab

Dein eigener Agent

Tool-Definitionen aus agentTools, angebunden an dein Model-SDK

Der Agent eines Browsers

registerTools() veröffentlicht die Tools auf document.modelContext (WebMCP), mit einem zweiten allow

Jede Änderung, die ein Agent macht, erreicht deine Change-Callbacks mit details.source === "agent", und details.cancel() kann sie weiterhin ablehnen.

Wie ein Agent über UI SDK deine Komponenten erreicht

Die Bridge ist ein Objekt, das createAgent() zurückgibt. UI-SDK-Komponenten auf der Seite registrieren sich beim Mounten bei ihr. Sie hat drei Methoden, die ein Agent nutzt:

  • snapshot() gibt jede registrierte Komponente mit einer kurzen ref wie c3 zurück, dazu ihre Art, ihr barrierefreies Label, ihren Zustand und die Aktionen, die sie gerade akzeptiert.
  • query() findet Komponenten per Text oder listet die Optionen in einer Komponente auf, etwa die Optionen eines Select.
  • act() führt eine Liste von Schritten der Reihe nach aus. Jeder Schritt nennt eine Komponente und eine Aktion, etwa setValue, und die Bridge ruft diese Aktion an der Komponente auf.

agentTools beschreibt diese drei Methoden als Tool-Definitionen mit JSON Schemas, für deinen eigenen Model-Loop. registerTools() registriert sie stattdessen beim Browser.

Die wichtige Designentscheidung ist, dass act() keine Klicks simuliert. Es ruft dieselbe API auf, die ein Controller aufrufen würde, also geht die Änderung durch den Change-Callback der Komponente, mit einer source von "agent". Deine App sieht Agent-Änderungen an derselben Stelle wie die eines Menschen und kann sie dort ablehnen.

Vercels Benchmark

Vercels Doku enthält einen Benchmark. Agenten haben ein Projekt umbenannt, Mitglieder und Benachrichtigungseinstellungen aktualisiert und dann gespeichert. Ein Agent nutzte Browser-Tools auf einer Seite, die mit einer anderen Komponentenbibliothek gebaut war. Der andere nutzte die Agent-Bridge auf demselben Screen, gebaut mit UI SDK.

Browser-ToolsUI-SDK-Agent-Bridge
Tool-Aufrufe−69%
165
Gelesener Text (Zeichen)−83%
39.8k6.8k
Zeit−61%
41 s16 s
Kosten−56%
$0.073$0.032

Quelle: Vercels UI-SDK-Doku. Mediane aus drei Läufen pro Ansatz, Sonnet über Claude Code. Nicht von HextaUI nachgemessen.

Vercels Benchmark mit Projekteinstellungen: Browser-Tools vs Agent-Bridge von UI SDK

Das sind Vercels Zahlen, nicht meine. Es sind Mediane aus drei Läufen pro Ansatz, beide Agenten nutzten Sonnet über Claude Code mit derselben Aufgabe und demselben System-Prompt, und jeder Lauf hat die Aufgabe erledigt. „Gelesener Text“ zählt die Zeichen, die die Tools zurückgeben. Ich habe den Benchmark nicht nachgemessen. Zur Einordnung: Ein vollständiger Snapshot der Demo unten ist 729 Zeichen JSON. Das HTML der Seite im Dev-Server von Vite hat etwa 28.000 Zeichen. Der ARIA-Snapshot von Playwright für dieselbe Seite war noch kürzer, etwa 370 Zeichen, aber er listet nur Rollen und Namen. Er sagt nicht, welche Aktionen jedes Control akzeptiert, welche Optionen ein geschlossenes Select enthält oder welcher Button einen Menschen braucht. Diese zusätzliche Struktur spart einem Agenten seine Folgeaufrufe, nicht die reine Größe.

Die Demo einrichten

Starte mit dem React- und TypeScript-Template von Vite und füge die zwei Pakete hinzu:

npm create vite@latest agent-demo -- --template react-ts
cd agent-demo
npm install
npm install @ui-sdk/react@0.1.0-beta.1 @ui-sdk/core@0.1.0-beta.1

Erzeuge die Bridge in einem eigenen Modul. Importiere es in main.tsx, bevor die App rendert, damit sie Komponenten sieht, während sie erzeugt werden. Laut Doku findet sie auch Komponenten, die schon auf der Seite sind, du kannst sie also auch später erzeugen, wenn sich ein Assistent öffnet.

import { createAgent, registerTools } from "@ui-sdk/react/agent"

const agent = createAgent({
  allow: (step) => {
    if (step.confirm) return window.confirm(step.confirm)
    return step.action !== "clear"
  },
})

const unregister = registerTools(agent, {
  allow: (step) => step.action !== "press",
})

Object.assign(window, { agent })

if (import.meta.hot) import.meta.hot.dispose(unregister)

export { agent }

Die zwei allow-Callbacks kommen weiter unten noch einmal vor. Object.assign(window, { agent }) legt die Bridge auf window, damit du sie in der Konsole ausprobieren kannst.

Die Seite ist ein Einstellungsformular mit einem Textfeld, einem Select, einem Switch, einem eigenen Seat-Zähler und einem Save-Button. Dieser Ausschnitt lässt Klassen, Layout und die Log-Komponente weg und behält die Parts, die für den Agenten zählen:

<Field>
  <FieldLabel>Project name</FieldLabel>
  <Input
    defaultValue="Apollo"
    onValueChange={(value, details) =>
      record(`Project name → ${value}`, details.source)
    }
  />
</Field>

<Select
  defaultValue="europe"
  items={regions}
  onValueChange={(value, details) => {
    if (details.source === "agent" && value === "asia-pacific") {
      details.cancel()
      return
    }
    record(`Region → ${value}`, details.source)
  }}
>
  <SelectLabel>Region</SelectLabel>
  <SelectTrigger>
    <SelectValue />
  </SelectTrigger>
  <SelectContent>
    {regions.map((region) => (
      <SelectItem key={region.value} value={region.value}>
        {region.label}
      </SelectItem>
    ))}
  </SelectContent>
</Select>

<label>
  Email notifications
  <Switch
    defaultChecked
    onCheckedChange={(checked, details) =>
      record(`Email notifications → ${checked}`, details.source)
    }
  >
    <SwitchThumb />
  </Switch>
</label>

<Button
  data-agent-confirm="Saves the settings for every member of Atlas"
  onClick={(_event, { source }) => record("Save pressed", source)}
>
  Save
</Button>

record hängt eine Zeile an ein Log auf der Seite an, damit du siehst, wer was geändert hat. Labels zählen hier mehr als sonst. Die Bridge benennt Komponenten nach ihrem barrierefreien Namen, also sind es FieldLabel, SelectLabel und das umschließende <label>, die einen Agenten „Region“ sagen lassen, statt zu raten.

Die Seite mit snapshot() lesen

Mit der gerenderten Seite gab agent.snapshot() das hier zurück:

[
  { "ref": "c1", "component": "input", "actions": ["setValue", "clear"], "label": "Project name", "value": "Apollo" },
  {
    "ref": "c3",
    "component": "select",
    "actions": ["setOpen", "setValue", "clear", "setQuery"],
    "label": "Region",
    "open": false,
    "value": "europe",
    "query": "",
    "items": [
      { "value": "europe", "label": "Europe" },
      { "value": "north-america", "label": "North America" },
      { "value": "asia-pacific", "label": "Asia Pacific" }
    ]
  },
  { "ref": "c4", "component": "switch", "actions": ["setChecked"], "label": "Email notifications", "checked": true, "value": "on" },
  { "ref": "c5", "component": "seat-stepper", "actions": ["add", "remove"], "label": "Seats", "value": 3 },
  { "ref": "c6", "component": "button", "actions": ["press"], "label": "Save", "confirm": "Saves the settings for every member of Atlas" }
]

Ein paar Dinge, die auffallen:

  • Es gibt kein c2. Das Field hat sich auch registriert, aber ein Field erscheint im Snapshot nur, solange es Fehler hat, weil sein Input das Label schon trägt. Die Lücke in den Refs ist harmlos.
  • Das Select listet seine Items auch geschlossen auf, weil die Demo items übergibt. Ohne sie liest query({ within: "Region" }) die Optionen, und die Bridge öffnet dafür eventuell kurz die Liste.
  • Der Save-Button trägt den confirm-Text aus data-agent-confirm, damit der Agent schon vor dem Versuch weiß, dass ein Mensch zustimmen muss.
  • seat-stepper ist meine eigene Komponente. Sie listet add und remove, weil ich es in ihrer Definition so festgelegt habe, dazu weiter unten mehr.

Dinge mit act() ändern

Ein act()-Aufruf kann jeden Schritt enthalten, den der Agent schon kennt. Schritte benennen Komponenten per ref oder per Label, und die Bridge vergleicht ganze Wörter, unabhängig von Groß- und Kleinschreibung und Akzenten. Auch ein Item lässt sich über sein Label benennen:

const result = await agent.act({
  steps: [
    { ref: "Project name", action: "setValue", args: ["Atlas"] },
    { ref: "Region", action: "setValue", args: ["North America"] },
    { ref: "Email notifications", action: "setChecked", args: [false] },
    { ref: "Seats", action: "add", args: [2] },
  ],
})

Das Ergebnis meldet die Änderungen jedes Schritts und den aktuellen Wert jedes Felds:

{
  "ok": true,
  "steps": [
    { "step": 0, "ref": "c1", "changes": [{ "component": "input", "label": "Project name", "key": "value", "value": "Atlas" }] },
    { "step": 1, "ref": "c3", "changes": [{ "component": "select", "label": "Region", "key": "value", "value": "north-america" }] },
    { "step": 2, "ref": "c4", "changes": [{ "component": "switch", "label": "Email notifications", "key": "checked", "value": false }] },
    { "step": 3, "ref": "c5", "changes": [{ "component": "seat-stepper", "label": "Seats", "key": "value", "value": 5 }] }
  ],
  "appeared": [],
  "values": {
    "c1 Project name": "Atlas",
    "c3 Region": "north-america",
    "c4 Email notifications": false,
    "c5 Seats": 5
  }
}

„North America“ wurde in den Wert des Items aufgelöst, north-america. Weil values bei jedem Aufruf zurückkommt, braucht ein Agent keinen weiteren Snapshot, um seine Arbeit zu prüfen.

Fehlermeldungen sind so geschrieben, dass ein Modell sie lesen kann. Zwei, die ich absichtlich ausgelöst habe:

setChecked on Email notifications (c4) takes true or false, not "yes".
seat-stepper c5 takes no keydown now. It takes: add, remove.

Ein fehlgeschlagener Schritt stoppt den Aufruf, und frühere Schritte bleiben angewendet. Es gibt kein Rollback.

Eine Matching-Regel hat mich überrascht. Ein Schritt mit ref: "Name" hat „Project name“ geändert, weil Labels nach ganzen Wörtern verglichen werden. Das ist dokumentiert, und die Bridge lehnt einen Namen ab, der auf mehr als eine Komponente passt, aber gib Komponenten eindeutige Labels, wenn ein Agent sie erraten muss.

Berechtigungen: allow, Confirm-Markierungen und cancel

Es gibt drei Stellen, an denen du Nein sagen kannst, und sie haben unterschiedliche Aufgaben.

allow auf createAgent sieht jeden Schritt, bevor er läuft, mit ref, component, label, action, args und confirm. Gib false zurück, um abzulehnen, oder ein Promise für deine eigene Bestätigungs-UI. Meine Policy lehnt jedes clear ab, und das Ergebnis (hier ohne values) ist:

{ "ok": false, "error": { "step": 0, "message": "The app refused clear on c1." }, "steps": [] }

data-agent-confirm markiert ein Control, das immer einen Menschen braucht, egal was die Regeln sagen. Ohne allow-Callback schlägt ein Schritt darauf mit der Meldung „Needs a person“ fehl. Mit einem trägt step.confirm den Satz, damit du ihn anzeigen kannst. Mein allow gibt ihn an window.confirm weiter, und im Lauf zeigte der Dialog „Saves the settings for every member of Atlas“, bevor Save gedrückt wurde. Die Markierung gilt für alles in ihrem Element, du kannst sie also auf ein ganzes Formular oder einen Dialog setzen.

Dein Change-Callback hat bei einem Wert das letzte Wort. Das Region-Select lehnt die Anfrage eines Agenten nach Asia Pacific ab:

{
  "ok": true,
  "steps": [{ "step": 0, "ref": "c3", "result": false }],
  "appeared": [],
  "values": {
    "c1 Project name": "Atlas",
    "c3 Region": "north-america",
    "c4 Email notifications": false,
    "c5 Seats": 5
  }
}

Beachte, dass ok gleich true ist. Die Bridge hat den Schritt ausgeführt, und der Callback hat die Änderung abgebrochen, also meldet der Schritt result: false und der Wert bleibt unverändert. Die Doku sagt das klar: ok: true heißt nicht, dass jeder Wert angenommen wurde. Prüfe changes und values.

Es gibt außerdem zwei Attribute dafür, was ein Agent sehen kann. data-agent-hidden entfernt einen Bereich aus snapshot und query. data-agent-secret zeigt ein Feld mit dem Wert "[redacted]". Passwort-, Karten- und Einmalcode-Felder gelten auch ohne Markierung als geheim.

Agent-Änderungen über die Source unterscheiden

Jeder Change-Callback in UI SDK bekommt details.source: keyboard, mouse, touch, pen, virtual, programmatic oder agent. Nach dem Lauf oben sah das Log der Demo so aus:

Ein Formular „Project settings“ mit Project name Atlas, Region North America, Benachrichtigungen aus und 5 Seats, über einem Log, in dem jede Zeile auf source: agent endet
Die Demo nach dem Lauf des Agenten. Jede Log-Zeile kommt aus einem Change-Callback, und jede meldet source: agent.

Ein Klick von Hand auf den Plus-Button danach loggte Seats → 2 (source: mouse). Dieses eine Feld reicht, um ein Badge „von deinem Assistenten bearbeitet“ zu bauen, Agent-Änderungen in eine Review-Queue zu schicken oder Analytics-Events zu überspringen, die kein Mensch ausgelöst hat.

Ein Vorbehalt aus der Doku: Ein Button-Handler, der auf etwas wartet und danach den State ändert, meldet "programmatic", weil der Druck dann schon vorbei ist. Pack die spätere Änderung in withSource(source, …) aus @ui-sdk/react/observe, damit der Agent die Source bleibt.

Deine eigene Komponente: definePrimitive und der agent-Key

Eingebaute Komponenten schließen sich der Bridge von selbst an. Komponenten, die du mit definePrimitive schreibst, ebenfalls. Hier ist der Seat-Zähler aus der Demo, mit einem agent-Key, der die zwei Methoden nennt, die ein Agent aufrufen darf:

import { createChange, definePrimitive, type ChangeDetails } from "@ui-sdk/core"

type SeatStepperOptions = {
  value?: number
  defaultValue?: number
  max?: number
  onValueChange?: (
    value: number,
    details: ChangeDetails<"add" | "remove">,
  ) => void
}

const seatStepper = definePrimitive({
  name: "seat-stepper",
  options: {
    value: "number",
    defaultValue: "number",
    max: "number",
    onValueChange: "callback",
  },
  controlled: ["value"],
  state: (options: SeatStepperOptions) => ({
    value: options.value ?? options.defaultValue ?? 1,
  }),
  sync: (options) =>
    options.value === undefined ? {} : { value: options.value },
  api: (context) => {
    const change = createChange(context)
    const max = () => context.options().max ?? 50
    return {
      add: (count = 1, event?: Event) =>
        change("value", Math.min(max(), context.store.get().value + count), {
          reason: "add",
          event,
        }),
      remove: (count = 1, event?: Event) =>
        change("value", Math.max(1, context.store.get().value - count), {
          reason: "remove",
          event,
        }),
    }
  },
  slots: (slot) => ({
    root: slot<{}, { value: number }>({
      role: "group",
      state: (state) => ({ value: state.value }),
    }),
    remove: slot({
      attrs: (state) => ({
        type: "button",
        "aria-label": "Remove a seat",
        disabled: state.value <= 1 ? "" : undefined,
      }),
      press: ({ api }, event) => api.remove(1, event),
    }),
    add: slot({
      attrs: () => ({ type: "button", "aria-label": "Add a seat" }),
      press: ({ api }, event) => api.add(1, event),
    }),
  }),
  agent: { actions: ["add", "remove"] },
})

export { seatStepper }

createParts(seatStepper, { root: "div", remove: "button", add: "button" }) aus @ui-sdk/react/primitive macht daraus React-Komponenten. Der Root nimmt aria-labelledby wie jedes Element, und daher kam das Label „Seats“ im Snapshot.

Warum der agent-Key wichtig ist: Ohne ihn rät die Bridge Aktionen anhand von Methodennamen. Sie erkennt set gefolgt von einem Großbuchstaben, etwa setValue, und Verben wie open, toggle und clear. add oder remove würde sie nicht erraten, der Stepper würde also keine Aktionen zeigen. Mit dem Key werden nur die gelisteten Methoden angeboten und alles andere abgelehnt, wie der keydown-Fehler oben gezeigt hat. Die Doku bittet dich, agent zuletzt zu setzen, nach slots, damit TypeScript die API bis dahin gelesen hat, und markiert den Key als experimentell.

Weil die Änderung durch createChange geht, sieht onValueChange der App weiterhin source: "agent" und kann abbrechen. Als der Agent bei drei Seats fünf entfernen wollte, wurde der Wert auf 1 begrenzt und der Entfernen-Button deaktiviert, genau so, wie es die Klicks eines Menschen hinterlassen hätten.

Zwei Anmerkungen vom Schreiben. Attributwerte in attrs sind Strings, also ist disabled gleich "" oder undefined, kein Boolean; TypeScript hat das bemerkt. Und die Core-Doku sagt, man solle role nie in attrs setzen, sondern den role-Key des Slots nutzen, während ein Core-Beispiel role doch in attrs setzt. Ich habe den role-Key genutzt.

Die Tools für WebMCP registrieren

registerTools(agent, { allow }) veröffentlicht snapshot, query und act auf document.modelContext, der Browser-API aus dem WebMCP-Vorschlag. Ein Browser, der sie bereitstellt, kann die Tools an seinen eingebauten Agenten übergeben. In einem Browser ohne sie tut registerTools nichts.

Das Headless Chromium, mit dem ich getestet habe, stellt document.modelContext nicht bereit. Um zu sehen, was registriert wird, habe ich also vor dem Laden der Seite ein Ersatzobjekt installiert. Die drei Tools kamen mit diesen Annotations an:

[
  { "name": "snapshot", "annotations": { "readOnlyHint": true, "untrustedContentHint": true } },
  { "name": "act", "annotations": { "readOnlyHint": false, "untrustedContentHint": true } },
  { "name": "query", "annotations": { "readOnlyHint": true, "untrustedContentHint": true } }
]

untrustedContentHint gibt es, weil Tool-Ergebnisse Seitentext enthalten, den Nutzer geschrieben haben, etwa einen Namen in einer Tabellenzeile. Die Tool-Beschreibungen sagen dem Modell außerdem, diesen Text als Daten zu behandeln, nie als Anweisungen. Das ist ein Hinweis, keine Garantie, also halte alles Riskante hinter allow und data-agent-confirm.

Das allow auf registerTools ist Pflicht, und es läuft nach dem eigenen allow der Bridge, nur für Schritte, die vom Agenten des Browsers kommen. Das gibt dir zwei Policies: eine für den Agenten, den du geschrieben hast, und eine strengere für einen Agenten, den du nicht geschrieben hast.

  1. Das Ziel findenrefEine Ref wie c3, dann ein Label, ein Item, dann eine Art.
  2. Bridge-allowcreateAgentDeine Policy für jeden Aufrufer. False stoppt den Aufruf.
  3. WebMCP-allowregisterToolsEine zweite Policy, nur für den Agenten des Browsers.
  4. Komponenten-APIsetValueDieselbe Methode, die ein Controller aufrufen würde.
  5. Dein Callbackdetails.sourceSieht "agent" und kann cancel() aufrufen.
Was mit einem act-Schritt passiert

Der Guide von UI SDK zeigt, wie man die Tools aus dem eingebauten Browser der ChatGPT-Desktop-App nutzt, wo Site-Tools aktiviert werden müssen und nur manche Modelle und Workspaces sie unterstützen. Diesen Weg habe ich nicht getestet. Prüfe typeof document.modelContext?.registerTool in dem Browser, auf den du zielst. Wo die Registrierung unterstützt wird, gibt es "function" zurück.

Ein Beta-Befund: eine Ablehnung, die dem Menschen angelastet wird

In meinem Setup bat das allow der Bridge den Menschen, Save zu bestätigen, und das WebMCP-allow lehnt jedes press ab. Als ich das registrierte act-Tool aufrief, um Save zu drücken, erschien der Bestätigungsdialog, ich habe ihn akzeptiert, und dann lehnte die WebMCP-Policy den Schritt ab. Der Fehler lautete:

The person said no to "Saves the settings for every member of Atlas".

Der Mensch hat Ja gesagt. Der Code hat Nein gesagt. Der Schritt wurde abgelehnt, was korrekt ist, aber ein Modell, das diese Meldung liest, könnte dem Nutzer erzählen, er habe etwas abgelehnt, dem er zugestimmt hat. Außerdem wurde der Mensch gefragt, bevor eine Policy geprüft wurde, die ohnehin ablehnen würde. Wenn du Confirm-Markierungen mit einem WebMCP-allow kombinierst, das Presses ablehnt, lehne früher ab: Prüfe step.action auch im allow der Bridge, damit der Mensch gar nicht erst gefragt wird. Ich melde das für 0.1.0-beta.1 und gehe davon aus, dass es sich ändert.

Wann sich das lohnt

Die Bridge lohnt sich, wenn ein Agent Teil des Produkts ist: ein Assistenten-Panel, das Formulare ausfüllt, ein Support-Tool, das Einstellungen für den Nutzer ändert, oder ein Browser-Agent, der auf deiner Seite gut funktionieren soll. Sie ist auch in Tests nützlich, weil act() dir einen typisierten Weg gibt, Komponenten ohne Selektoren zu steuern.

Allein ist das kein Grund, eine stabile App zu migrieren. Die Bridge sieht nur UI-SDK-Komponenten, eine Seite, die Bibliotheken mischt, gibt dem Agenten also nur eine Teilansicht. Und es ist eine Beta, bei der der agent-Key und die Verlaufs-Helfer als experimentell markiert sind.

Wenn du darauf aufbaust, halten ein paar Gewohnheiten Agenten nützlich und sicher:

  • gib jedem Control einen barrierefreien Namen, denn den tippt der Agent;
  • übergib items an Selects und Comboboxes, damit die Optionen auch geschlossen lesbar sind;
  • lehne in allow ab, was ein Agent nie tun sollte, und markiere mit data-agent-confirm, was einen Menschen braucht;
  • validiere Werte in Change-Callbacks, denn setValue ruft die API direkt auf und überspringt deine Tastatur- und Pointer-Handler;
  • lies values aus jedem act()-Ergebnis, statt ok zu vertrauen.

HextaUI arbeitet an einer gestylten UI-SDK-Variante seiner Komponenten, also sind das die Patterns, um die herum ich baue. Wie UI SDK sich jenseits von Agenten mit Radix und Base UI vergleicht, steht in UI SDK vs Base UI vs Radix. Zum Stylen der Komponenten mit Tailwind siehe Vercels UI SDK mit Tailwind stylen.