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.