Skip to content

Render an accessible result card

Create a checked result model from the current result in a Model Context Protocol (MCP) application, then pass it to the renderer that fits your application. Em See Pea supplies the result structure and accessibility semantics. Your application keeps ownership of domain parsing, calculations, wording, effects, action authorization, and styles.

You need Node.js 22 or newer and an application with a page shell. Continuous integration (CI) exercises Node.js 22 and 24. The page shell owns the document language, title, skip link, main landmark, one H1, routing, route focus, and visible focus styles. Add an empty element with id="result" where the card belongs.

Renderer installation comes after the shared result mapping below. Styling is a separate choice from the renderer.

Create a new ResultView from the current checked domain result whenever the result or relevant UI state changes. Put the mapping function in result-view.ts beside the client or widget component that renders the card.

Runtime parsing happens before this adapter. The adapter formats already checked data for presentation; domain parsing, calculations, permissions, effect authorization, and styling stay outside it. This example declares a small domain type so it can run on its own. In an application, import the equivalent post-parse type from your domain layer.

result-view.ts
import { defineResultView, type ResultView } from "@emseepea/server/ui";
export interface PlantingPlanResult {
readonly title: string;
readonly matchingCount: number;
readonly notice: string;
}
export function toResultView(result: PlantingPlanResult): ResultView {
const count = result.matchingCount;
const headline = `${count} ${
count === 1 ? "variety matches" : "varieties match"
}`;
return defineResultView({
id: "pea-result",
heading: result.title,
headline,
metrics: [{ label: "Matching varieties", value: String(count) }],
reasons: {
label: "Why these varieties match",
items: ["They suit the selected growing conditions."],
},
disclaimer: result.notice,
state: {
kind: count === 0 ? "empty" : "ready",
status: count === 0
? "No pea varieties match the selected conditions."
: `Pea planting plan ready: ${headline}.`,
focusTarget: "none",
},
});
}

Each returned view is bounded and validated before rendering. It has a contextual heading, a definition list for the metric, a labelled reasons list, a disclaimer, and one persistent polite status message.

Select a renderer below. Your selection applies to the installation, rendering, and lifecycle instructions on this page.

Use native HTML when your application does not need a framework renderer. Use React when your application already uses React. Use Svelte when your application already uses Svelte or you choose it for a self-contained Model Context Protocol Apps (MCP Apps) resource.

Native HTML

Install the checked model and native HTML renderer.

Install Native HTML packages
npm install @emseepea/server

Native HTML

In native.ts, use renderResultView when the application does not need a framework renderer. Pass the heading level that fits the surrounding page and a unique idPrefix for every result in the document.

native.ts
import { renderResultView } from "@emseepea/server/ui";
import {
toResultView,
type PlantingPlanResult,
} from "./result-view.js";
const target = document.querySelector<HTMLElement>("#result");
if (!target) throw new Error("Missing #result host element");
export function renderInitialPlantingPlan(
result: PlantingPlanResult,
): void {
target.innerHTML = renderResultView(toResultView(result), {
headingLevel: 2,
idPrefix: "pea-result-card",
});
}

The host element now contains native headings, lists, and status semantics. The renderer escapes result text and does not create the page shell.

All three renderers produce the same names, semantics, heading level, and status. ResultCard does not parse domain data or perform effects. Em See Pea does not provide a Svelte initializer.

Connect MCP Apps lifecycle only when needed

Section titled “Connect MCP Apps lifecycle only when needed”

Native HTML

Call renderInitialPlantingPlan once when the first checked result arrives. For later status changes, keep the status element mounted and update its textContent. Use React or Svelte when the rest of the card also changes.

A native MCP Apps resource can use createMcpAppController from @emseepea/server/ui directly. Call connect() after the document is ready, subscribe to checked state changes, and run the cleanup function returned by connect() during teardown.

The lifecycle bindings handle initialization, host context, tool results, cancellation, teardown, and ui/message through the shared checked controller.

The maintained React result-card fixture and Svelte result-card fixture show the framework bindings. Treat every host message as untrusted input.

Compare React and Svelte bundle measurements

Section titled “Compare React and Svelte bundle measurements”

If React and Svelte both fit your application, the maintained equivalent production fixture can inform your choice. It measured Svelte 27.9% smaller with gzip and 27.2% smaller with Brotli. The fixture used esbuild 0.28.2, React and React DOM 19.2.8, Svelte 5.57.0, minified ES modules, an ES2022 target, and no source maps.

  • React: 534,121 raw bytes; 129,181 bytes with gzip level 9; 109,052 bytes with Brotli quality 11.
  • Svelte: 406,619 raw bytes; 93,075 bytes with gzip level 9; 79,414 bytes with Brotli quality 11.
  • Reduction: 23.9% raw; 27.9% with gzip; 27.2% with Brotli.

These measurements apply only to this fixture and dependency set. They do not establish a general performance or bundle-size advantage. Run npm run measure:result-cards in the repository to reproduce them.

Renderer choice does not add styles. All three renderers are unstyled unless your application imports styles.

Install the optional stylesheet package, then import its one CSS file from your application entry point:

Install the optional stylesheet
npm install @emseepea/tailwind
Import the optional stylesheet
import "@emseepea/tailwind/styles.css";

The same import styles native renderResultView, React ResultCard, and Svelte ResultCard. Set data-emseepea-theme="light" or data-emseepea-theme="dark" on an ancestor. In React, pass the checked host theme to useMcpTheme and apply its result. Native HTML and Svelte applications resolve and apply the checked host value themselves. The renderers do not choose or apply that theme.

Your application still owns domain mapping, wording, business policy, actions, effect authorization, the page shell, theme application, branding, and style overrides. Removing the import changes presentation only.

Result actions are native buttons. An action callback reports the selected presentation action; it does not authorize a server effect. Check identity, permissions, current state, and request data again at the effect boundary.

For an action that continues the conversation, set the state to sending before awaiting app.sendMessage(prompt). This sends a ui/message request. While it is pending, set action.disabled to true and update the existing status to ongoing text such as Asking ChatGPT: Show me growing tips.

After the host accepts the message, use completion text such as Asked ChatGPT: Show me growing tips. If the request fails, show a short error. Tell the user how to continue in the chat.

The maintained React result example implements this sequence without making the action a server-side effect.

Update the existing state.status text for loading, completion, cancellation, and errors. Keep one persistent polite status instead of adding another live region. Use focusTarget only when focus must move after a meaningful update. Do not make the whole card clickable or add custom card roles, positive tabindex values, redundant ARIA, or extra landmarks.

The documentation test suite builds these examples outside the monorepo against packed packages. The cross-renderer contract tests also check hostile text escaping, names, semantics, heading levels, status updates, focus targets, native buttons, mobile reflow, forced colours, reduced motion, and contrast-related states.

After adapting the model, run your application build and keyboard checks. Confirm that the card reflows at 320 CSS pixels, visible focus remains clear, and status changes are announced once without moving focus unnecessarily.

Source and package checks do not prove publication. PUBLISHED does not prove that an adopter uses the package successfully. Adopter PROD_VERIFIED requires independent evidence from the adopter’s exact production revision and Result Card journey.

For the complete checked behaviour, read: