Adaptive Card
Render Microsoft Adaptive Cards JSON natively — elements, inputs, actions, charts, and templating.
AdaptiveCard renders an Adaptive Cards JSON document with native React Native primitives — no WebView. It is a 1:1 native port of the web @docyrus/ui Adaptive Card renderer and supports the same payloads produced by the Docyrus Adaptive Card Designer: every element, input, action, the chart family (including Teams' grouped and stacked bars), templating (${...}, $data, $when), requires/fallback, and host-config overrides.
Installation
pnpm dlx @docyrus/cli add @docyrus/rn-adaptive-cardpnpm add react-native-svg @react-native-community/datetimepicker (optional)react-native-svg powers the chart family, the progress ring, and CDN icons. @react-native-community/datetimepicker is optional — when absent, Input.Date / Input.Time fall back to a plain text field.
Usage
import { AdaptiveCard } from '@/components/docyrus-native/adaptive-card';
const payload = {
type: 'AdaptiveCard',
version: '1.5',
body: [
{ type: 'TextBlock', text: 'Hello **world**', size: 'large', weight: 'bolder', wrap: true },
{ type: 'Input.Text', id: 'name', label: 'Name', isRequired: true },
],
actions: [{ type: 'Action.Submit', title: 'Send', style: 'positive' }],
};
<AdaptiveCard
payload={payload}
onAction={(event) => {
if (event.type === 'submit') console.log(event.data); // { name: '...' }
}}
/>With templating
Pass a data context to expand ${...} expressions, $data scoping/repeats, and $when conditions before rendering.
<AdaptiveCard
payload={{
type: 'AdaptiveCard',
version: '1.5',
body: [
{ type: 'TextBlock', text: '${greeting}, ${name}!', weight: 'bolder' },
{ type: 'TextBlock', $data: '${items}', text: '${title} — ${price}' },
],
}}
data={{ greeting: 'Hello', name: 'Erhan', items: [{ title: 'Coffee', price: '$3.50' }] }}
/>Handling submit / execute
onAction fires for every action the user triggers, so you can persist forms, route Teams Action.Execute verbs, or log analytics. Submit/execute events arrive only after input validation passes.
<AdaptiveCard
payload={formPayload}
onAction={async (event) => {
if (event.type === 'submit') {
await client.post('/forms', event.data); // { name: '...', topic: '...' }
} else if (event.type === 'execute') {
await invokeAgent(event.verb, event.data); // event.verb identifies the action
}
}}
/>Host config override
Adaptive Cards' abstract enums (good, attention, medium, vertical actions…) resolve through a host config. defaultHostConfig reads the app's theme tokens, so light/dark is automatic. Pass hostConfig to deep-merge overrides:
<AdaptiveCard
payload={payload}
hostConfig={{ actions: { maxActions: 3, actionsOrientation: 'vertical' } }}
/>Custom / overridden element types
customElements registers a renderer for any type string and takes precedence over the built-ins — use it for host-specific elements or to swap a placeholder (e.g. a real Media player) for a native one:
import { Text } from 'react-native';
<AdaptiveCard
payload={payload}
customElements={{
'Docyrus.UserChip': ({ element }) => <UserChip id={element.userId} />,
Media: ({ element }) => <VideoPlayer source={element.sources?.[0]?.url} />,
}}
/>Teams cards (PascalCase enums)
Microsoft's schema spells enum values in PascalCase ("Large", "Bolder", "Good", "PartToWhole"), and Teams matches them case-insensitively. The native renderer matches the same way, through matchesEnum and lookupEnum from lib/enum-match. So a card copied from Microsoft's documentation renders the same as its camelCase equivalent. This covers text size, weight and font type, colours, alignment, spacing, container, table and badge styles, image size and style, action style and mode, chart colour sets, display mode, thickness and value format. An unrecognized value falls back to the default instead of dropping the styling.
<AdaptiveCard
payload={{
type: 'AdaptiveCard',
version: '1.5',
body: [
{ type: 'TextBlock', text: 'Deployment finished', size: 'Large', weight: 'Bolder', color: 'Good' },
{ type: 'Container', style: 'Emphasis', items: [{ type: 'TextBlock', text: 'All checks passed', horizontalAlignment: 'Center' }] }
],
actions: [{ type: 'Action.Submit', title: 'Acknowledge', style: 'Positive' }]
}}
/>Charts (Teams contract)
Microsoft's chart shapes differ per chart type, and the renderer accepts every spelling:
- Bar charts take
{ x, y }points. - Pie and donut take
{ legend, value }. - Gauge segments size themselves with
size(orvalue). Chart.LineandChart.VerticalBar.Groupedtake one entry per series,{ legend, values: [{ x, y }] }.Chart.HorizontalBar.Stackednests the segments under a titled bar:{ title, data: [{ legend, value, color? }] }.
const charts = {
type: 'AdaptiveCard',
version: '1.5',
body: [
{
type: 'Chart.VerticalBar.Grouped',
title: 'Revenue by region',
stacked: false, // true stacks the series on one bar
showBarValues: true,
xAxisTitle: 'Quarter',
yAxisTitle: 'Revenue',
valueFormat: 'short',
data: [
{ legend: 'EMEA', values: [{ x: 'Q1', y: 1200 }, { x: 'Q2', y: 1800 }] },
{ legend: 'APAC', values: [{ x: 'Q1', y: 900 }, { x: 'Q2', y: 1400 }] }
]
},
{
type: 'Chart.HorizontalBar.Stacked',
title: 'Tickets by team',
colorSet: 'diverging',
data: [
{ title: 'Support', data: [{ legend: 'Open', value: 12 }, { legend: 'Closed', value: 30 }] },
{ title: 'Sales', data: [{ legend: 'Open', value: 5 }, { legend: 'Closed', value: 18 }] }
]
},
{
type: 'Chart.HorizontalBar',
displayMode: 'PartToWhole', // scale each bar against the total; hides the value axis
data: [{ x: 'Won', y: 42 }, { x: 'Lost', y: 18 }]
}
]
};| Chart option | Applies to | Values |
|---|---|---|
valueFormat | bar, line, pie / donut labels, gauge | short, long, percentage / Percentage (a value from 0 to 1 is shown as a %), fraction / Fraction (gauge only: value/max). |
displayMode | Chart.HorizontalBar only | AbsoluteWithAxis (default), AbsoluteNoAxis (no value axis), PartToWhole (each bar is a % of the total). camelCase spellings are also accepted. |
stacked / showBarValues | Chart.VerticalBar.Grouped | Stack the series on one bar instead of side by side, and print each value. |
thickness | Chart.Donut | thin / Thin, medium (default), thick / Thick. |
colorSet | all charts | categorical (default), sequential, divergent / diverging. |
xAxisTitle / yAxisTitle | bar, grouped, stacked, line | The x title is centered under the plot. The y title is a caption above the plot, because rotated text does not lay out reliably on RN. |
Headless hook flow
For a custom toolbar, programmatic validation, or a non-default layout, drive the renderer with useAdaptiveCard + AdaptiveCardView. A failed submit focuses the first invalid Input.Text / Input.Number:
import { AdaptiveCardView, useAdaptiveCard } from '@/components/docyrus-native/adaptive-card';
function CustomFlow({ payload }) {
const adaptive = useAdaptiveCard(payload, { onAction });
return (
<>
<Button onPress={() => adaptive.controls.validateAll()}>Pre-validate</Button>
<AdaptiveCardView cardProps={adaptive.cardProps} />
</>
);
}Filtered Input.ChoiceSet (Data.Query)
A style: 'filtered' ChoiceSet that declares choices.data calls onChoiceQuery with the dataset + debounced search text; return the matching choices:
<AdaptiveCard
payload={payload}
onChoiceQuery={async ({ dataset, search }) => {
const rows = await client.get(`/lookup/${dataset}?q=${search}`);
return rows.map(r => ({ title: r.label, value: r.id }));
}}
/>API Reference
| Prop | Type | Default | Description |
|---|---|---|---|
payload | AdaptiveCardPayload | — | The Adaptive Card JSON document to render. Required. |
data | unknown | — | Data context for template expansion (${path}, $data, $when, built-in functions). When provided, the payload is expanded against it before rendering. |
onAction | (event: AdaptiveCardActionEvent) => void | Promise<void> | — | Fires for every action: submit, execute, openUrl, toggleVisibility, showCard, resetInputs. |
hostConfig | AdaptiveCardHostConfigOverride | — | Deep-partial overrides for spacing, actions orientation/limits, etc. Merged over defaultHostConfig. |
customElements | Record<string, ElementRenderer> | — | Custom renderers keyed by element type. Takes precedence over the built-ins — use it to override Media, Component, or any element. |
onChoiceQuery | (request: { dataset, search, inputId }) => Promise<AdaptiveCardChoice[]> | — | Async resolver for a filtered Input.ChoiceSet declaring choices.data (Data.Query). |
className | string | — | Extra classes for the card surface. |
Supported elements
| Group | Types |
|---|---|
| Text & media | TextBlock, RichTextBlock, Image, ImageSet, Media |
| Containers | Container, ColumnSet / Column, FactSet, Table, ActionSet |
| Inputs | Input.Text, Input.Number, Input.Date, Input.Time, Input.Toggle, Input.ChoiceSet, Input.Rating |
| Display (1.6) | Badge, CodeBlock, CompoundButton, Icon, ProgressBar, ProgressRing, Rating |
| Layout (1.6) | Carousel, Accordion, TabSet |
| Charts (1.6) | Chart.VerticalBar, Chart.VerticalBar.Grouped, Chart.HorizontalBar, Chart.HorizontalBar.Stacked, Chart.Pie, Chart.Donut, Chart.Line (single or multi-series), Chart.Gauge |
| Extensibility | Component, LoopComponent (placeholders) |
Supported actions
| Type | Behavior |
|---|---|
Action.Submit | Validates inputs, then fires onAction with the collected input map merged with data. |
Action.Execute | Same as submit plus verb. |
Action.OpenUrl | Opens the URL via Linking and fires onAction. |
Action.ShowCard | Toggles an inline nested card below the action row. |
Action.ToggleVisibility | Shows/hides target elements by id. |
Action.ResetInputs | Clears the named inputs (or all in scope). |
selectAction is supported on Container, Column, ColumnSet, Image, Icon, TableCell, CompoundButton, and the card root.
Action dispatch
Every action emits a typed event through onAction (AdaptiveCardActionEvent, a discriminated union on type):
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 opens the URL via Linking) |
'toggleVisibility' | action, card | Action.ToggleVisibility |
'showCard' | isOpen, action, card | Action.ShowCard (fires on open and close) |
'resetInputs' | action, card | Action.ResetInputs |
data follows the spec's associatedInputs contract: 'auto' (default) collects every visible input in scope; 'none' ships only action.data. ${input.<id>} placeholders inside action.data are substituted with the current input values.
Helpers
All exported from @/components/docyrus-native/adaptive-card:
| Export | Use |
|---|---|
useAdaptiveCard(payload, options) | Headless hook — state, validation, action dispatch. Returns { cardProps, controls, state }. Also installable on its own as @docyrus/rn-hooks-use-adaptive-card. |
AdaptiveCardView | Renders from a cardProps produced by the hook. |
registerElement(type, renderer) | Register a global custom element renderer (vs. the per-instance customElements prop). |
expandTemplate(payload, data) | Stand-alone templating expander (${}, $data, $when). |
evaluateExpression(expr, scope) / registerExpressionFunction(name, fn) | Evaluate / extend the expression language. |
validateInput / validateInputs / collectInputs / buildSubmitData | Input validation + submit-payload helpers. |
defaultHostConfig / mergeHostConfig | Host config base + deep-merge. |
parseAdaptiveCard / isAdaptiveCard / isSafeBackgroundUrl | Payload guards. |
getElementRenderer / listElementTypes / registerDefaultElements | Inspect the element registry or re-register the built-ins. |
useAdaptiveCardContext / useAdaptiveCardInput / isElementVisible | Read card state inside a custom element renderer. |
ElementNode / ElementList / ActionBar / NativeIcon | Building blocks for custom element renderers. |
matchesEnum(value, ...accepted) / lookupEnum(map, key) | Case-insensitive enum matching, the same rules Teams uses. |
formatChartValue(value, format) / formatGaugeValue(value, min, max, format) | Chart value formatting (short / long / percentage / fraction). |
buildColoredData / buildGroupedData / buildLineData / buildStackedBarData / pointValue / segmentValue | Normalize the Teams chart data shapes. |
Native notes
- Charts render with
react-native-svg, without the tooltips of the web charting library. Plain vertical and horizontal bars and the stacked horizontal bar are View-based; the grouped vertical bar, pie, donut, line and gauge are SVG. Horizontal bars show a 0 / ½ / max value axis unlessdisplayModeisAbsoluteNoAxisorPartToWhole, and the grouped chart draws a y-axis with gridlines. Tablegives a column with a numericwidtha fixed size (width× 50 px, same as web) and scrolls horizontally when the columns are wider than the card.- Text sizes never go below
text-xs(12 px): badgeextraSmall/small, citations, chart labels and compound-button badges were normalized, matching web. Mediahas no bundled player: it shows the poster (or an audio chip) and opens the source viaLinkingon tap.Component/LoopComponentrender lossless placeholders. Register acustomElementsoverride for inline playback.TextBlock/FactSetmarkdown supports the inline subset: bold, italic, strikethrough, inline code, links. Block lists are not formatted.- Icons (
Icon,Badge.icon, accordion/tab headers) map Fluent names to Font Awesome glyphs loaded from the Docyrus icon CDN.
Type Exports
| Type | Description |
|---|---|
AdaptiveCardPayload | The card document (type, version, body, actions, …). |
AdaptiveCardProps | Props for the AdaptiveCard component. |
AdaptiveCardElement | Union of all supported element shapes. |
AdaptiveCardAction / AdaptiveCardSelectAction | Action unions. |
AdaptiveCardActionEvent | The onAction event union. |
AdaptiveCardHostConfig / AdaptiveCardHostConfigOverride | Host config + deep-partial override. |
AdaptiveCardInput / AdaptiveCardInputValue | Input element union + value type. |
ElementRenderer | Signature for customElements entries. |
AdaptiveCardContextValue | The context value AdaptiveCardView renders from (cardProps). |
AdaptiveCardViewProps | Props for AdaptiveCardView (cardProps, className). |
UseAdaptiveCardOptions / UseAdaptiveCardReturn / AdaptiveCardChoiceQueryRequest | Hook option, return and choice-query request types. |
ValidationResult | { isValid, errorMessage? } per input. |
AdaptiveCardChart* | AdaptiveCardChart union, …VerticalBar, …VerticalBarGrouped, …HorizontalBar, …HorizontalBarStacked, …StackedBar, …StackedBarSegment, …Pie, …Donut, …Line, …Gauge, …Point, …DataPoint, …Segment, …ColorSet, …Color, …ValueFormat, …DisplayMode, …Thickness. |
ChartSeries / ColoredDatum / LineData | Return shapes of the chart-data helpers. |
Every other schema type from adaptive-card-types.ts is re-exported from the barrel as well. That includes the enums (AdaptiveCardSpacing, …Color, …FontSize, …FontWeight, …FontType, alignments, …ContainerStyle, …ImageSize / …ImageStyle, …ActionStyle / …ActionMode, …), the elements (AdaptiveCardTextBlock, …RichTextBlock, …TextRun, …Image, …Media, …Container, …ColumnSet, …Table*, …Badge, …Carousel*, …Accordion*, …TabSet, …), the inputs (AdaptiveCardInputText, …InputNumber, …InputDate, …InputTime, …InputToggle, …InputChoiceSet, …InputRating, AdaptiveCardChoice), the actions (AdaptiveCardActionSubmit, …Execute, …OpenUrl, …ShowCard, …ToggleVisibility, …ResetInputs), the events (AdaptiveCardSubmitEvent, …) and the host-config parts, so the native barrel matches the web one.
Related
useAdaptiveCard: the headless hook that powers the component.useDocyrusAdaptiveCardItemDetail/useDocyrusAdaptiveCardItemList: generate cards from a Docyrus field list plus records.registerElement(type, renderer)— register a global custom element renderer.expandTemplate(payload, data)/evaluateExpression(expr, scope)— standalone templating helpers.