# Adaptive Card URL: /docs/web/components/adaptive-card Renderer for Microsoft's Adaptive Cards 1.5 schema. Surfaces LLM-generated and Microsoft Teams payloads as native Docyrus UI with stateful inputs, validation, nested ShowCards, and action dispatch. **Demo:** ```tsx 'use client'; import { useMemo, useState } from 'react'; import { AdaptiveCard, type AdaptiveCardActionEvent, type AdaptiveCardPayload } from '@docyrus/ui/components/adaptive-card'; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@docyrus/ui/primitives/ui/select'; import { cn } from '@docyrus/ui/primitives/lib/utils'; import { DEMO_AGENT_MESSAGE, DEMO_FLIGHT_CARD, DEMO_FORM_CARD } from '@/data/adaptive-card-data'; const DEMOS = { flight: { label: 'Flight status', payload: DEMO_FLIGHT_CARD }, form: { label: 'Form card', payload: DEMO_FORM_CARD }, agent: { label: 'Agent message', payload: DEMO_AGENT_MESSAGE } } as const satisfies Record; export function AdaptiveCardDemo() { const [selected, setSelected] = useState('flight'); const [open, setOpen] = useState(false); const [lastEvent, setLastEvent] = useState ))}
{lastEvent ? (
          {JSON.stringify(lastEvent, (_k, v) => v instanceof Object && 'card' in v ? { ...v, card: '<…>' } : v, 2)}
        
) : null} ); } ``` ## Installation ```bash pnpm dlx @docyrus/cli add @docyrus/ui-adaptive-card ``` **Dependencies:** - [lucide-react](https://www.npmjs.com/package/lucide-react) - [react-markdown](https://www.npmjs.com/package/react-markdown) - [remark-gfm](https://www.npmjs.com/package/remark-gfm) ## Overview `AdaptiveCard` consumes Microsoft's [Adaptive Cards 1.5 JSON schema](https://adaptivecards.io/schemas/1.5.0/adaptive-card.json) and renders it using Docyrus primitives (shadcn). The two primary use cases are: - **Agent-native UI** — Adaptive Cards are a JSON structure LLM agents already understand and emit reliably. Use the renderer to surface agent output as actionable UI. - **Microsoft Teams interop** — the same payload a Teams Bot returns can render directly inside Docyrus messages, comments, and notifications. The component is a thin wrapper around [`useAdaptiveCard`](/docs/web/hooks/use-adaptive-card). Drop in a payload, get a fully interactive card back. For advanced flows (server-driven validation, custom toolbars), use the hook directly. ## Usage ### Basic render ```tsx import { AdaptiveCard, type AdaptiveCardPayload } from '@docyrus/ui/components/adaptive-card'; const payload: AdaptiveCardPayload = { type: 'AdaptiveCard', version: '1.5', body: [ { type: 'TextBlock', text: 'Hello, **Adaptive Cards**!', size: 'large', weight: 'bolder' } ], actions: [ { type: 'Action.OpenUrl', title: 'Open docs', url: 'https://adaptivecards.io/' } ] }; console.log(event)} /> ``` ### Handling submit / execute ```tsx { if (event.type === 'submit') { await fetch('/api/forms', { method: 'POST', body: JSON.stringify(event.data) }); } else if (event.type === 'execute') { // event.verb identifies the bot action await invokeBot(event.verb, event.data); } }} /> ``` `onAction` fires for **every** action the user triggers — `submit`, `execute`, `openUrl`, `toggleVisibility`, `showCard` — so consumers can log analytics, persist drafts, or route Teams `Action.Execute` calls. ### Host config override The renderer maps Adaptive Cards' abstract enums (`Good`, `Attention`, `Bleed`, `Medium`) to concrete colors/spacing/sizes via a host config. `defaultHostConfig` reads from the repo's CSS-variable tokens, so dark mode is automatic. Pass `hostConfig` to deep-merge overrides: ```tsx ``` ### Custom element types Register a renderer for an unknown `type` string via `customElements`: ```tsx }} /> ``` Custom renderers take precedence over the built-in registry. The unknown-type fallback continues to honor the schema's `fallback` chain otherwise. ### Hook-driven flow For advanced cases (custom toolbar, programmatic submit, server-side validation), consume the hook directly: ```tsx import { AdaptiveCardView, useAdaptiveCard } from '@docyrus/ui/components/adaptive-card'; function CustomFlow({ payload }) { const adaptive = useAdaptiveCard(payload, { onAction }); return ( <> ); } ``` See [`useAdaptiveCard`](/docs/web/hooks/use-adaptive-card) for the full hook surface. ## Schema coverage The renderer ships **complete coverage** of the Adaptive Cards 1.5 schema: | Category | Elements | |----------|----------| | Text & media | `TextBlock`, `RichTextBlock` (with `TextRun` inlines), `Image`, `ImageSet`, `Media` | | Containers | `Container`, `ColumnSet` (`Column` children), `FactSet`, `Table` (`TableRow`, `TableCell`), `ActionSet` | | Inputs | `Input.Text` (single + multiline + `inlineAction`), `Input.Number`, `Input.Date`, `Input.Time`, `Input.Toggle`, `Input.ChoiceSet` (compact / expanded / filtered × single / multi) | | Actions | `Action.Submit`, `Action.Execute`, `Action.OpenUrl`, `Action.ShowCard` (nested cards), `Action.ToggleVisibility` | | Cross-cutting | `selectAction` on containers/columns/images/text-runs/table cells/root card, `requires` capability gating, `fallback` chains, `isVisible` defaults + toggle overrides, `spacing` + `separator`, markdown in `TextBlock` + `FactSet` values, `backgroundImage` (with safe-URL guard), validation (`isRequired`, `regex`, `min`/`max`, error messages) | Out of scope in v1: live `refresh` polling, server-side `Action.Execute` transport (we hand `verb` + `data` to `onAction`), `authentication` block rendering. ## Action dispatch Every action emits a typed event through `onAction`: | Event `type` | Fields | Triggered by | |--------------|--------|--------------| | `'submit'` | `data`, `action`, `card` | `Action.Submit` (after validation passes) | | `'execute'` | `verb`, `data`, `action`, `card` | `Action.Execute` (after validation passes) | | `'openUrl'` | `url`, `action`, `card` | `Action.OpenUrl` (also navigates the browser via the embedded ``) | | `'toggleVisibility'` | `action`, `card` | `Action.ToggleVisibility` | | `'showCard'` | `isOpen`, `action`, `card` | `Action.ShowCard` (fires for both open and close) | `data` follows the spec's `associatedInputs` contract: `'auto'` (default) collects every visible input in the current scope; `'none'` ships only `action.data`. For `Action.ShowCard` submits, the nested card's inputs merge into the parent's. ## API Reference ### `AdaptiveCardProps` | Prop | Type | Default | Description | |------|------|---------|-------------| | `payload` | `AdaptiveCardPayload` | — | The card JSON. Must have `type: 'AdaptiveCard'` and a `version`. | | `onAction` | `(event: AdaptiveCardActionEvent) => void \| Promise` | — | Fires for every dispatched action. | | `hostConfig` | `AdaptiveCardHostConfigOverride` (deep partial of `AdaptiveCardHostConfig`) | `defaultHostConfig` | Override colors, spacing, sizes, container styles, or action layout. Deep-merged over the default. | | `customElements` | `Record` | `{}` | Renderers for element types unknown to the spec. Take precedence over the built-in registry. | | `className` | `string` | — | Forwarded to the root ``. | ### `AdaptiveCardViewProps` `AdaptiveCardView` is the presentational layer that the hook drives. Use it when you call `useAdaptiveCard` yourself. | Prop | Type | Default | Description | |------|------|---------|-------------| | `cardProps` | `AdaptiveCardContextValue` | — | The `cardProps` projection returned by `useAdaptiveCard`. | | `className` | `string` | — | Forwarded to the root ``. | ## Components | Component | Description | |-----------|-------------| | `AdaptiveCard` | High-level component. Composes `useAdaptiveCard` + `AdaptiveCardView`. | | `AdaptiveCardView` | Presentational layer. Consumes `cardProps` from `useAdaptiveCard`. Use this when wiring the hook manually. | | `ElementNode` / `ElementList` | Recursive renderer. The `customElements` extension point routes through these. | | `ActionBar` | Renders an action list with overflow menu + ShowCard panels. | ## Helpers | Export | Purpose | |--------|---------| | `defaultHostConfig` | The token-driven host config used when no override is passed. | | `mergeHostConfig(base, override)` | Deep-merge a partial override onto a base config. | | `parseAdaptiveCard(payload)` | Validate + normalize an unknown payload. Returns `null` for invalid input. | | `isAdaptiveCard(value)` | Type guard for `AdaptiveCardPayload`. | | `isSafeBackgroundUrl(url)` | Refuses `javascript:` and `data:text/html` URIs; allows `https:`, `http:`, `data:image/*`. | | `validateInput(input, value)` / `validateInputs(inputs, values)` | The same validators the hook runs on submit. | | `collectInputs(elements, visibilityOverrides)` | Walks an element tree for `associatedInputs: 'auto'` collection. | | `buildSubmitData(action, card, values, overrides)` | Assemble the `data` payload for a `Submit` / `Execute` action. | | `registerElement(type, renderer)` | Register a globally-available element renderer. Prefer the per-instance `customElements` prop when possible. | ## Type Exports | Type | Description | |------|-------------| | `AdaptiveCardPayload` | Card root. | | `AdaptiveCardElement` | Discriminated union of every known element type. | | `AdaptiveCardCustomElement` | Open `type: string` shape for renderer extensions. Not in the main union to keep narrowing tight. | | `AdaptiveCardAction` | Discriminated union of every action type. | | `AdaptiveCardSelectAction` | Subset of actions legal as `selectAction` (no `ShowCard`). | | `AdaptiveCardInput` | Discriminated union of every input type. | | `AdaptiveCardActionEvent` | The union of events emitted to `onAction`. | | `AdaptiveCardSubmitEvent`, `AdaptiveCardExecuteEvent`, `AdaptiveCardOpenUrlEvent`, `AdaptiveCardToggleVisibilityEvent`, `AdaptiveCardShowCardEvent` | Individual event shapes. | | `AdaptiveCardHostConfig` / `AdaptiveCardHostConfigOverride` | Host config + deep-partial override. | | `AdaptiveCardInputValue` | `string \| Array \| boolean \| number \| null` — the runtime value an `Input.*` can hold. | | `ElementRenderer` | `(props: { element: T }) => ReactNode`. | | `AdaptiveCardProps` / `AdaptiveCardViewProps` | Component props. | ## Security notes - **No raw HTML** — markdown in `TextBlock` / `FactSet` is rendered with `react-markdown` + `remark-gfm` and the default raw-HTML-disabled config. Card payloads that arrive from LLMs or external bots cannot inject `