adaptive-cards-hostconfig-theme

star 3

How AdaptiveCards HostConfig maps to Flutter rendering, how ReferenceResolver bridges the two, how light/dark themes are structured, and how elements read theme-aware colors, fonts, and spacing. Use this before modifying styling, colors, spacing, or HostConfig parsing in any element.

freemansoft By freemansoft schedule Updated 6/10/2026

name: adaptive-cards-hostconfig-theme description: > How AdaptiveCards HostConfig maps to Flutter rendering, how ReferenceResolver bridges the two, how light/dark themes are structured, and how elements read theme-aware colors, fonts, and spacing. Use this before modifying styling, colors, spacing, or HostConfig parsing in any element.

HostConfig / Theme Integration Skill

Overview

Adaptive Cards theming is driven by a JSON HostConfig object — a platform developer's contract that defines colors, font sizes, spacing, and more. This project maps that HostConfig object to Flutter rendering through two central abstractions:

HostConfigs  ──(contains)──►  HostConfig (light)
             ──(contains)──►  HostConfig (dark)
                                   │
                                   ▼
                          ReferenceResolver
                    (the bridge used by all elements)

The ReferenceResolver is created in RawAdaptiveCardState and exposed to every element widget via a Riverpod Provider override.


Key Files

File Purpose
lib/src/hostconfig/host_config.dart HostConfig + HostConfigs classes
lib/src/reference_resolver.dart ReferenceResolver — HostConfig/style resolution only
lib/src/hostconfig/fallback_configs.dart Hardcoded defaults used when HostConfig is absent
lib/src/hostconfig/*.dart Typed config classes for each subsection
lib/src/riverpod/providers.dart Card-scoped Riverpod providers (registries, resolver, document)
lib/src/flutter_raw_adaptive_card.dart Where ReferenceResolver is created and scoped via ProviderScope

The Two-Level Config Structure

HostConfigs — Light + Dark Wrapper

class HostConfigs {
  final HostConfig light;   // used for light mode rendering
  final HostConfig dark;    // used for dark mode rendering
  late HostConfig current;  // set to light by default in constructor
}

Brightness: RawAdaptiveCardState._updateResolver() sets HostConfigs.current from Theme.of(context).brightness when brightnessMode is auto (default). Hosts can force light or dark with AdaptiveCardBrightnessMode.light / .dark.

HostConfig — The AC Spec Object

HostConfig is a Dart representation of the Adaptive Cards HostConfig JSON schema. All properties are nullable — if absent, ReferenceResolver falls back to FallbackConfigs.

class HostConfig {
  final String? fontFamily;
  final ForegroundColorsConfig? foregroundColors;  // text colors by semantic name
  final ContainerStylesConfig? containerStyles;     // container bg + fg colors
  final FontSizesConfig? fontSizes;                 // small/medium/large/extraLarge
  final FontWeightsConfig? fontWeights;             // lighter/default/bolder
  final SpacingsConfig? spacing;                    // none/small/medium/large/...
  final SeparatorConfig? separator;                 // divider thickness + color
  final ImageSizesConfig? imageSizes;               // small/medium/large px values
  final ActionsConfig? actions;                     // button layout, spacing
  final InputsConfig? inputs;                       // label/error message styling
  final BadgeStylesConfig? badgeStyles;             // badge filled/tint colors
  final ProgressSizesConfig? progressSizes;         // progress bar dimensions
  final ProgressColorsConfig? progressColors;       // progress bar colors
  final ChartColorsConfig? chartColors;            // chart palettes and defaults
  // ... other subsections
}

How ReferenceResolver is Created and Distributed

In RawAdaptiveCardState._updateResolver() (called from initState / didUpdateWidget):

_resolver = ReferenceResolver(
  hostConfigs: widget.hostConfigs,
  colorFallbacks: ThemeColorFallbacks(Theme.of(context)),
);

Registries are not passed to ReferenceResolver. They are supplied separately via Riverpod overrides in build():

return ProviderScope(
  overrides: [
    cardTypeRegistryProvider.overrideWithValue(widget.cardTypeRegistry),
    actionTypeRegistryProvider.overrideWithValue(widget.actionTypeRegistry),
    styleReferenceResolverProvider.overrideWithValue(_resolver),
    // ... document and card-state providers
  ],
  child: Card(color: backgroundColor, child: child),
);

Containers use ChildStyler to override styleReferenceResolverProvider for descendants with inherited foreground context and alignment:

// ChildStyler (in additional.dart) — simplified
final childResolver = parent.copyWith(
  inheritedContainerStyle: ReferenceResolver.inheritedContainerStyleForChildren(
    parentInherited: parent.inheritedContainerStyle,
    ownContainerStyle: adaptiveMap['style'] as String?,
  ),
  inheritedHorizontalAlignment:
      ReferenceResolver.inheritedHorizontalAlignmentForChildren(
    parentInherited: parent.inheritedHorizontalAlignment,
    ownAlignment: adaptiveMap['horizontalAlignment'] as String?,
  ),
);

Container background uses only the element's own style JSON. Foreground palette uses inheritedContainerStyle on the scoped resolver.

See Style inheritance data flow.


Reading the Resolver in an Element

Inside any element State with ProviderScopeMixin, use styleResolver:

@override
Widget build(BuildContext context) {
  final resolver = styleResolver;

  // Now use resolver.resolve*() methods...
}

Stateless helpers can read the resolver from the card-scoped container:

final resolver = ProviderScope.containerOf(context)
    .read(styleReferenceResolverProvider);

Elements rebuild via setState or ref.watch, not by listening to an inherited widget.


ReferenceResolver Method Reference

Colors

Foreground (Text) Colors

AC defines semantic color names for text, resolved through the container style's ForegroundColorsConfig:

// Semantic color names: 'default', 'dark', 'light', 'accent',
//                        'good', 'warning', 'attention'
// isSubtle: true gives the 70% opacity variant

Color? color = resolver.resolveContainerForegroundColor(
  style: adaptiveMap['color'] as String?,  // JSON "color" property
  isSubtle: adaptiveMap['isSubtle'] as bool? ?? false,
);

Resolution priority:

  1. Explicitly passed style (if not 'default')
  2. currentContainerStyle (inherited from parent container's resolver)
  3. The 'default' container style foreground colors
  4. FallbackConfigs.containerStylesConfig if HostConfig is absent

Container Background Colors

Container "style" property maps to background colors:

// Container style names: 'default', 'emphasis', 'good',
//                         'attention', 'warning', 'accent'
Color? bg = resolver.resolveContainerBackgroundColor(style: 'emphasis');

// Smart version: returns null if backgroundImage is set or style is null
// (lets the container be transparent to its parent)
Color? bg2 = resolver.resolveContainerBackgroundColorIfNoBackgroundImage(
  context: context,
  style: adaptiveMap['style'] as String?,
  backgroundImageUrl: adaptiveMap['backgroundImage'] as String?,
);

Button Colors (Material Theme-Derived)

Action buttons do not use HostConfig colors — they use Theme.of(context):

// Action "style" values: 'default', 'positive', 'destructive'
Color? bg = resolver.resolveButtonBackgroundColor(
  context: context,
  style: adaptiveMap['style'] as String?,
);
// 'default'     → colorScheme.primary
// 'positive'    → colorScheme.secondary
// 'destructive' → colorScheme.error

Color? fg = resolver.resolveButtonForegroundColor(
  context: context,
  style: adaptiveMap['style'] as String?,
);
// 'default'     → colorScheme.onPrimary
// 'positive'    → colorScheme.onSecondary
// 'destructive' → colorScheme.onError

This is where Flutter's Material 3 ThemeData directly affects card rendering. Setting colorScheme.primary in the host app's theme changes button appearances.

Typography

// Font size — 'default', 'small', 'medium', 'large', 'extraLarge'
double size = resolver.resolveFontSize(
  context: context,
  sizeString: adaptiveMap['size'] as String?,
);

// Font weight — 'default', 'lighter', 'bolder'
FontWeight weight = resolver.resolveFontWeight(
  adaptiveMap['weight'] as String?,
);

// Font type — 'default', 'monospace' (currently both return theme font)
String? family = resolver.resolveFontType(
  context,
  adaptiveMap['fontType'] as String?,
);

Fallback font size values (from FallbackConfigs.fontSizesConfig):

AC Name Default px
small 10
default 12
medium 14
large 18
extraLarge 22

Spacing

AC's "spacing" JSON property controls vertical/horizontal gaps between elements:

// Spacing values: 'none', 'small', 'default', 'medium', 'large',
//                 'extraLarge', 'padding'
double gap = resolver.resolveSpacing(adaptiveMap['spacing'] as String?);

// Or use SpacingsConfig.resolveSpacing() directly:
double gap = SpacingsConfig.resolveSpacing(
  resolver.getSpacingsConfig(),
  spacing,
);

Fallback spacing values (from FallbackConfigs.spacingsConfig):

AC Name Default px
none 0
small 4
default 4
medium 8
large 16
extraLarge 32
padding 20

Separators


Charts

// Resolves the color palette for charts
List<Color> palette = resolver.resolveChartPalette();

// Resolves a single chart color (hex or semantic 'good'/'warning'/etc.)
Color color = resolver.resolveChartColor(
  adaptiveMap['color'] as String?,
  fallback: Colors.blue,
);

Chart layout (chartsLayout)

final layout = resolver.resolveLineChartLayout();
// Also: resolveBarChartLayout(), resolvePieChartLayout(), resolveDonutChartLayout()

JSON example:

{
  "chartsLayout": {
    "line": { "height": 250, "barWidth": 3, "borderColor": "#37434d" },
    "bar": { "height": 250, "barWidth": 16, "barsSpace": 4 },
    "pie": { "height": 200, "sectionRadius": 100 },
    "donut": { "centerSpaceRadius": 40, "sectionRadius": 50 }
  }
}

Alignment

// HorizontalAlignment: 'left', 'center', 'right'
Alignment align = resolver.resolveAlignment(
  adaptiveMap['horizontalAlignment'] as String?,
);

// CrossAxisAlignment variant
CrossAxisAlignment cross = resolver.resolveHorzontalCrossAxisAlignment(
  adaptiveMap['horizontalAlignment'] as String?,
);

// MainAxisAlignment (horizontal)
MainAxisAlignment main = resolver.resolveHorizontalMainAxisAlignment(
  adaptiveMap['horizontalAlignment'] as String?,
);

// VerticalContentAlignment: 'top', 'center', 'bottom'
MainAxisAlignment vertical = resolver.resolveVerticalMainAxisContentAlginment(
  adaptiveMap['verticalContentAlignment'] as String?,
);

// TextAlign: 'left', 'center', 'right'
TextAlign ta = resolver.resolveTextAlign(adaptiveMap['horizontalAlignment'] as String?);

Text Wrapping

// Respects both "wrap" (bool) and "maxLines" (int) JSON properties
int maxLines = resolver.resolveMaxLines(
  wrap: adaptiveMap['wrap'] as bool?,
  maxLines: adaptiveMap['maxLines'] as int?,
);
// wrap=false → always 1; wrap=true → maxLines ?? 1

The Color Model: ForegroundColorsConfigFontColorConfig

The color resolution chain for foreground text:

ContainerStylesConfig
  └── ContainerStyleConfig (for 'default' or 'emphasis')
        └── ForegroundColorsConfig
              └── FontColorConfig (for 'default', 'accent', 'good', etc.)
                    ├── defaultColor: Color   ← full opacity
                    └── subtleColor:  Color   ← ~70% opacity (isSubtle: true)

Colors in JSON are hex strings in #RRGGBB or #AARRGGBB format:

{ "default": "#FF000000", "subtle": "#B2000000" }

FontColorConfig._parseColor() handles both formats.


Providing a Custom HostConfig

From a Dart Object

final myConfig = HostConfig(
  fontSizes: FontSizesConfig(
    small: 11,
    defaultSize: 14,
    medium: 16,
    large: 20,
    extraLarge: 24,
  ),
  containerStyles: ContainerStylesConfig(
    defaultStyle: ContainerStyleConfig(
      backgroundColor: Colors.white,
      foregroundColors: ForegroundColorsConfig(
        defaultColor: FontColorConfig(
          defaultColor: Colors.black87,
          subtleColor: Colors.black54,
        ),
        accent: FontColorConfig(
          defaultColor: Colors.blue,
          subtleColor: Colors.blue.withAlpha(128),
        ),
        // ... other colors
      ),
    ),
    emphasis: ContainerStyleConfig(
      backgroundColor: const Color(0xFFF5F5F5),
      foregroundColors: /* same pattern */,
    ),
  ),
);

AdaptiveCardsCanvas.asset(
  assetPath: 'assets/my_card.json',
  hostConfigs: HostConfigs(light: myConfig, dark: myDarkConfig),
);

From a JSON String (HostConfig Spec)

import 'dart:convert';

final Map<String, dynamic> json = jsonDecode(hostConfigJsonString);
final config = HostConfig.fromJson(json);
final hostConfigs = HostConfigs(light: config);

The JSON schema is documented in lib/src/hostconfig/host_config_schema.json. The official spec is at adaptivecards.io/explorer/HostConfig.html.


Fallback Strategy

Every ReferenceResolver method follows this fallback chain:

  1. HostConfig value — from the HostConfig object if the property is set
  2. ThemeColorFallbacks — color defaults derived from ThemeData.colorScheme (see docs/hostconfig.md)
  3. FallbackConfigs static value — non-color defaults only (spacing, font sizes, …) in fallback_configs.dart
  4. Flutter Theme.of(context) — action buttons only (no HostConfig equivalent)

ReferenceResolver requires colorFallbacks: ThemeColorFallbacks(Theme.of(context)), created in RawAdaptiveCardState._updateResolver().


Testing HostConfig Behavior

Unit tests for config parsing are in test/hostconfig/:

// Testing config parsing (no widget needed)
test('custom font sizes are parsed', () {
  final config = HostConfig.fromJson({
    'fontSizes': {'default': 16, 'large': 22},
  });
  expect(config.fontSizes?.defaultSize, 16);
  expect(config.fontSizes?.large, 22);
  expect(config.fontSizes?.small, FallbackConfigs.fontSizesConfig.small); // fallback
});

Widget-level HostConfig tests use getTestWidgetFromMap with a custom AdaptiveCardsCanvas that has an injected HostConfigs:

// See test/host_config_test.dart for the established pattern
Install via CLI
npx skills add https://github.com/freemansoft/Flutter-AdaptiveCards --skill adaptive-cards-hostconfig-theme
Repository Details
star Stars 3
call_split Forks 1
navigation Branch main
article Path SKILL.md
More from Creator