Adaptive Cards Integration
Use json-render to render Microsoft Adaptive Cards natively.
Concept: This page demonstrates how json-render can support Adaptive Cards. The examples are illustrative and may require adaptation for production use.
Adaptive Cards Overview
Adaptive Cards is a JSON-based format for platform-agnostic UI snippets. Cards have a body array of elements and an optional actions array for interactive buttons.
Example Adaptive Card
{
"$schema": "http://adaptivecards.io/schemas/adaptive-card.json",
"type": "AdaptiveCard",
"version": "1.5",
"body": [
{
"type": "TextBlock",
"text": "Hello, Adaptive Cards!",
"size": "large",
"weight": "bolder"
},
{
"type": "Image",
"url": "https://example.com/image.png",
"altText": "Example image"
},
{
"type": "Container",
"items": [
{
"type": "TextBlock",
"text": "This is inside a container",
"wrap": true
}
]
},
{
"type": "ColumnSet",
"columns": [
{
"type": "Column",
"width": "auto",
"items": [
{ "type": "TextBlock", "text": "Column 1" }
]
},
{
"type": "Column",
"width": "stretch",
"items": [
{ "type": "TextBlock", "text": "Column 2" }
]
}
]
},
{
"type": "Input.Text",
"id": "userInput",
"placeholder": "Enter your name",
"label": "Name"
}
],
"actions": [
{
"type": "Action.Submit",
"title": "Submit"
},
{
"type": "Action.OpenUrl",
"title": "Learn More",
"url": "https://adaptivecards.io"
}
]
}Creating an Adaptive Cards Catalog
Define a catalog matching the Adaptive Cards element types:
import { defineCatalog } from '@json-render/core';
import { schema } from '@json-render/react';
import { z } from 'zod';
// Common Adaptive Cards properties
const Spacing = z.enum(['none', 'small', 'default', 'medium', 'large', 'extraLarge', 'padding']);
const HorizontalAlignment = z.enum(['left', 'center', 'right']);
const VerticalAlignment = z.enum(['top', 'center', 'bottom']);
const FontSize = z.enum(['small', 'default', 'medium', 'large', 'extraLarge']);
const FontWeight = z.enum(['lighter', 'default', 'bolder']);
const ImageSize = z.enum(['auto', 'stretch', 'small', 'medium', 'large']);
const ImageStyle = z.enum(['default', 'person']);
// Base element properties shared by most elements
const BaseElement = {
id: z.string().optional(),
isVisible: z.boolean().optional(),
separator: z.boolean().optional(),
spacing: Spacing.optional(),
};
export const adaptiveCardsCatalog = defineCatalog(schema, {
components: {
// Root card
AdaptiveCard: {
description: 'Root Adaptive Card container',
props: z.object({
version: z.string(),
body: z.array(z.unknown()).optional(),
actions: z.array(z.unknown()).optional(),
fallbackText: z.string().optional(),
minHeight: z.string().optional(),
rtl: z.boolean().optional(),
verticalContentAlignment: VerticalAlignment.optional(),
}),
},
// Elements
TextBlock: {
description: 'Displays text with formatting options',
props: z.object({
...BaseElement,
text: z.string(),
color: z.enum(['default', 'dark', 'light', 'accent', 'good', 'warning', 'attention']).optional(),
fontType: z.enum(['default', 'monospace']).optional(),
horizontalAlignment: HorizontalAlignment.optional(),
isSubtle: z.boolean().optional(),
maxLines: z.number().optional(),
size: FontSize.optional(),
weight: FontWeight.optional(),
wrap: z.boolean().optional(),
}),
},
Image: {
description: 'Displays an image',
props: z.object({
...BaseElement,
url: z.string(),
altText: z.string().optional(),
backgroundColor: z.string().optional(),
height: z.string().optional(),
width: z.string().optional(),
horizontalAlignment: HorizontalAlignment.optional(),
size: ImageSize.optional(),
style: ImageStyle.optional(),
}),
},
Container: {
description: 'Groups elements together',
props: z.object({
...BaseElement,
items: z.array(z.unknown()),
style: z.enum(['default', 'emphasis', 'good', 'attention', 'warning', 'accent']).optional(),
verticalContentAlignment: VerticalAlignment.optional(),
bleed: z.boolean().optional(),
minHeight: z.string().optional(),
}),
},
ColumnSet: {
description: 'Arranges columns horizontally',
props: z.object({
...BaseElement,
columns: z.array(z.unknown()),
horizontalAlignment: HorizontalAlignment.optional(),
minHeight: z.string().optional(),
}),
},
Column: {
description: 'A column within a ColumnSet',
props: z.object({
...BaseElement,
items: z.array(z.unknown()).optional(),
width: z.union([z.string(), z.number()]).optional(),
style: z.enum(['default', 'emphasis', 'good', 'attention', 'warning', 'accent']).optional(),
verticalContentAlignment: VerticalAlignment.optional(),
}),
},
FactSet: {
description: 'Displays a series of facts as key/value pairs',
props: z.object({
...BaseElement,
facts: z.array(z.object({
title: z.string(),
value: z.string(),
})),
}),
},
// Inputs
'Input.Text': {
description: 'Text input field',
props: z.object({
...BaseElement,
id: z.string(),
isMultiline: z.boolean().optional(),
maxLength: z.number().optional(),
placeholder: z.string().optional(),
label: z.string().optional(),
value: z.string().optional(),
style: z.enum(['text', 'tel', 'url', 'email', 'password']).optional(),
isRequired: z.boolean().optional(),
errorMessage: z.string().optional(),
}),
},
'Input.Number': {
description: 'Number input field',
props: z.object({
...BaseElement,
id: z.string(),
max: z.number().optional(),
min: z.number().optional(),
placeholder: z.string().optional(),
label: z.string().optional(),
value: z.number().optional(),
isRequired: z.boolean().optional(),
errorMessage: z.string().optional(),
}),
},
'Input.Toggle': {
description: 'Toggle/checkbox input',
props: z.object({
...BaseElement,
id: z.string(),
title: z.string(),
label: z.string().optional(),
value: z.string().optional(),
valueOff: z.string().optional(),
valueOn: z.string().optional(),
isRequired: z.boolean().optional(),
}),
},
'Input.ChoiceSet': {
description: 'Dropdown or radio/checkbox group',
props: z.object({
...BaseElement,
id: z.string(),
choices: z.array(z.object({
title: z.string(),
value: z.string(),
})),
isMultiSelect: z.boolean().optional(),
style: z.enum(['compact', 'expanded']).optional(),
label: z.string().optional(),
value: z.string().optional(),
placeholder: z.string().optional(),
isRequired: z.boolean().optional(),
}),
},
// Actions
'Action.OpenUrl': {
description: 'Opens a URL',
props: z.object({
title: z.string().optional(),
url: z.string(),
iconUrl: z.string().optional(),
}),
},
'Action.Submit': {
description: 'Submits input data',
props: z.object({
title: z.string().optional(),
data: z.unknown().optional(),
iconUrl: z.string().optional(),
}),
},
'Action.ShowCard': {
description: 'Shows a card inline',
props: z.object({
title: z.string().optional(),
card: z.unknown(),
iconUrl: z.string().optional(),
}),
},
'Action.Execute': {
description: 'Universal action for bots',
props: z.object({
title: z.string().optional(),
verb: z.string().optional(),
data: z.unknown().optional(),
iconUrl: z.string().optional(),
}),
},
},
});Building an Adaptive Cards Renderer
Create a renderer that processes Adaptive Cards JSON. See the A2UI integration page for a similar pattern. The key is mapping each Adaptive Card element type to a React component, resolving nested items and columns arrays recursively.
Usage Example
Render an Adaptive Card and handle actions:
'use client';
import { AdaptiveCardRenderer } from './adaptive-card-renderer';
const card = {
type: 'AdaptiveCard' as const,
version: '1.5',
body: [
{
type: 'TextBlock',
text: 'Contact Form',
size: 'large',
weight: 'bolder',
},
{
type: 'Input.Text',
id: 'name',
label: 'Your Name',
placeholder: 'Enter your name',
},
{
type: 'Input.Text',
id: 'message',
label: 'Message',
placeholder: 'Enter your message',
isMultiline: true,
},
],
actions: [
{
type: 'Action.Submit',
title: 'Send',
data: { action: 'submitForm' },
},
],
};
export function ContactCard() {
const handleAction = (action: any, inputData: Record<string, unknown>) => {
console.log('Action:', action);
console.log('Input data:', inputData);
// Send to your backend
fetch('/api/submit', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ action, data: inputData }),
});
};
return <AdaptiveCardRenderer card={card} onAction={handleAction} />;
}Handling Action.Execute for Bots
For bot scenarios, handle Action.Execute with the verb and data:
interface ActionExecutePayload {
action: {
type: 'Action.Execute';
verb: string;
data?: unknown;
};
inputs: Record<string, unknown>;
}
async function handleBotAction(payload: ActionExecutePayload) {
const response = await fetch('/api/bot/action', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
verb: payload.action.verb,
data: payload.action.data,
inputs: payload.inputs,
}),
});
// Bot may return a new card to render
const result = await response.json();
if (result.card) {
return result.card; // New AdaptiveCard to render
}
}Next
Learn about A2UI integration for another agent-driven UI protocol.