+ );
+});
+
+export default AudioPlayer;
diff --git a/renderers/react/src/components/content/Divider.tsx b/renderers/react/src/components/content/Divider.tsx
new file mode 100644
index 000000000..010e7f0d4
--- /dev/null
+++ b/renderers/react/src/components/content/Divider.tsx
@@ -0,0 +1,51 @@
+/**
+ * Copyright 2026 Google LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import {memo} from 'react';
+import type * as Types from '@a2ui/web_core/types/types';
+import type {A2UIComponentProps} from '../../types';
+import {useA2UIComponent} from '../../hooks/useA2UIComponent';
+import {classMapToString, stylesToObject} from '../../lib/utils';
+
+/**
+ * Divider component - renders a visual separator line.
+ *
+ * Structure mirrors Lit's Divider component:
+ *
+ );
+});
+
+export default Divider;
diff --git a/renderers/react/src/components/content/Icon.tsx b/renderers/react/src/components/content/Icon.tsx
new file mode 100644
index 000000000..be8a607ee
--- /dev/null
+++ b/renderers/react/src/components/content/Icon.tsx
@@ -0,0 +1,72 @@
+/**
+ * Copyright 2026 Google LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import {memo} from 'react';
+import type * as Types from '@a2ui/web_core/types/types';
+import type {A2UIComponentProps} from '../../types';
+import {useA2UIComponent} from '../../hooks/useA2UIComponent';
+import {classMapToString, stylesToObject} from '../../lib/utils';
+
+/**
+ * Convert camelCase to snake_case for Material Symbols font.
+ * e.g., "shoppingCart" -> "shopping_cart"
+ * This matches the Lit renderer's approach.
+ */
+function toSnakeCase(str: string): string {
+ return str.replace(/([A-Z])/g, '_$1').toLowerCase();
+}
+
+/**
+ * Icon component - renders an icon using Material Symbols Outlined font.
+ *
+ * This matches the Lit renderer's approach using the g-icon class with
+ * Material Symbols Outlined font.
+ *
+ * @example Add Material Symbols font to your HTML:
+ * ```html
+ *
+ * ```
+ */
+export const Icon = memo(function Icon({node, surfaceId}: A2UIComponentProps) {
+ const {theme, resolveString} = useA2UIComponent(node, surfaceId);
+ const props = node.properties;
+
+ const iconName = resolveString(props.name);
+
+ if (!iconName) {
+ return null;
+ }
+
+ // Convert camelCase to snake_case for Material Symbols
+ const snakeCaseName = toSnakeCase(iconName);
+
+ // Apply --weight CSS variable on root div (:host equivalent) for flex layouts
+ const hostStyle: React.CSSProperties =
+ node.weight !== undefined ? ({'--weight': node.weight} as React.CSSProperties) : {};
+
+ return (
+
+
+ {snakeCaseName}
+
+
+ );
+});
+
+export default Icon;
diff --git a/renderers/react/src/components/content/Image.tsx b/renderers/react/src/components/content/Image.tsx
new file mode 100644
index 000000000..95e1b8de7
--- /dev/null
+++ b/renderers/react/src/components/content/Image.tsx
@@ -0,0 +1,70 @@
+/**
+ * Copyright 2026 Google LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import {memo} from 'react';
+import type * as Types from '@a2ui/web_core/types/types';
+import type {A2UIComponentProps} from '../../types';
+import {useA2UIComponent} from '../../hooks/useA2UIComponent';
+import {classMapToString, stylesToObject, mergeClassMaps} from '../../lib/utils';
+
+type UsageHint = 'icon' | 'avatar' | 'smallFeature' | 'mediumFeature' | 'largeFeature' | 'header';
+type FitMode = 'contain' | 'cover' | 'fill' | 'none' | 'scale-down';
+
+/**
+ * Image component - renders an image from a URL with optional sizing and fit modes.
+ *
+ * Supports usageHint values: icon, avatar, smallFeature, mediumFeature, largeFeature, header
+ * Supports fit values: contain, cover, fill, none, scale-down (maps to object-fit via CSS variable)
+ */
+export const Image = memo(function Image({node, surfaceId}: A2UIComponentProps) {
+ const {theme, resolveString} = useA2UIComponent(node, surfaceId);
+ const props = node.properties;
+
+ const url = resolveString(props.url);
+ const altText = resolveString((props as any).altText);
+ const usageHint = props.usageHint as UsageHint | undefined;
+ const fit = (props.fit as FitMode) ?? 'fill';
+
+ // Get merged classes for section (matches Lit's Styles.merge)
+ const classes = mergeClassMaps(
+ theme.components.Image.all,
+ usageHint ? theme.components.Image[usageHint] : {}
+ );
+
+ // Build style object with object-fit as CSS variable (matches Lit)
+ const style: React.CSSProperties = {
+ ...stylesToObject(theme.additionalStyles?.Image),
+ '--object-fit': fit,
+ } as React.CSSProperties;
+
+ if (!url) {
+ return null;
+ }
+
+ // Apply --weight CSS variable on root div (:host equivalent) for flex layouts
+ const hostStyle: React.CSSProperties =
+ node.weight !== undefined ? ({'--weight': node.weight} as React.CSSProperties) : {};
+
+ return (
+
+
+
+
+
+ );
+});
+
+export default Image;
diff --git a/renderers/react/src/components/content/Text.tsx b/renderers/react/src/components/content/Text.tsx
new file mode 100644
index 000000000..dc5da434c
--- /dev/null
+++ b/renderers/react/src/components/content/Text.tsx
@@ -0,0 +1,225 @@
+/**
+ * Copyright 2026 Google LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import {useMemo, memo} from 'react';
+import type * as Types from '@a2ui/web_core/types/types';
+import type {A2UIComponentProps} from '../../types';
+import {useA2UIComponent} from '../../hooks/useA2UIComponent';
+import {classMapToString, stylesToObject, mergeClassMaps} from '../../lib/utils';
+import MarkdownIt from 'markdown-it';
+
+type UsageHint = 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'caption' | 'body';
+
+interface HintedStyles {
+ h1?: Record;
+ h2?: Record;
+ h3?: Record;
+ h4?: Record;
+ h5?: Record;
+ body?: Record;
+ caption?: Record;
+}
+
+function isHintedStyles(styles: unknown): styles is HintedStyles {
+ if (typeof styles !== 'object' || !styles || Array.isArray(styles)) return false;
+ const expected = ['h1', 'h2', 'h3', 'h4', 'h5', 'caption', 'body'];
+ return expected.some((v) => v in styles);
+}
+
+/**
+ * Markdown-it instance for rendering markdown text.
+ * Uses synchronous import to ensure availability at first render (matches Lit renderer).
+ *
+ * Configuration matches Lit's markdown directive (uses MarkdownIt defaults):
+ * - html: false (default) - Security: disable raw HTML
+ * - linkify: false (default) - Don't auto-convert URLs/emails to links
+ * - breaks: false (default) - Don't convert \n to
+ * - typographer: false (default) - Don't use smart quotes/dashes
+ */
+const markdownRenderer = new MarkdownIt();
+
+/**
+ * Maps HTML tag names to their markdown-it token names.
+ * Mirrors the Lit renderer's markdown directive approach.
+ */
+const TAG_TO_TOKEN: Record = {
+ p: 'paragraph',
+ h1: 'heading',
+ h2: 'heading',
+ h3: 'heading',
+ h4: 'heading',
+ h5: 'heading',
+ h6: 'heading',
+ ul: 'bullet_list',
+ ol: 'ordered_list',
+ li: 'list_item',
+ a: 'link',
+ strong: 'strong',
+ em: 'em',
+};
+
+function toClassArray(classes: string[] | Record): string[] {
+ if (Array.isArray(classes)) return classes;
+ return Object.entries(classes)
+ .filter(([, v]) => v)
+ .map(([k]) => k);
+}
+
+/**
+ * Render markdown to HTML, applying theme classes via markdown-it renderer rules.
+ * Uses token.attrJoin() on _open tokens — same approach as the Lit renderer.
+ * Safe to mutate the module-level renderer because MarkdownIt.render() is synchronous.
+ */
+function renderWithTheme(text: string, markdownTheme: Types.Theme['markdown']): string {
+ const appliedKeys: string[] = [];
+ // Cast to a generic record for dynamic token.tag lookups inside renderer rules
+ const themeMap = markdownTheme as Record> | undefined;
+
+ if (themeMap) {
+ for (const [tag, classes] of Object.entries(themeMap)) {
+ if (!classes) continue;
+ const tokenName = TAG_TO_TOKEN[tag];
+ if (!tokenName) continue;
+
+ const key = `${tokenName}_open`;
+ if (!appliedKeys.includes(key)) appliedKeys.push(key);
+
+ markdownRenderer.renderer.rules[key] = (tokens, idx, options, _env, self) => {
+ const token = tokens[idx];
+ if (token) {
+ const tagClasses = themeMap[token.tag];
+ if (tagClasses) {
+ for (const cls of toClassArray(tagClasses)) {
+ token.attrJoin('class', cls);
+ }
+ }
+ }
+ return self.renderToken(tokens, idx, options);
+ };
+ }
+ }
+
+ const html = markdownRenderer.render(text);
+
+ for (const key of appliedKeys) {
+ delete markdownRenderer.renderer.rules[key];
+ }
+
+ return html;
+}
+
+/**
+ * Text component - renders text content with markdown support.
+ *
+ * Structure mirrors Lit's Text component:
+ *
← :host equivalent
+ * ← theme classes
+ *
...
← rendered markdown content
+ *
+ *
+ *
+ * Text is parsed as markdown and rendered as HTML (matches Lit renderer behavior).
+ * Supports usageHint values: h1, h2, h3, h4, h5, caption, body
+ *
+ * Markdown features supported:
+ * - **Bold** and *italic* text
+ * - Lists (ordered and unordered)
+ * - `inline code` and code blocks
+ * - [Links](url) (auto-linkified URLs too)
+ * - Blockquotes
+ * - Horizontal rules
+ *
+ * Note: Raw HTML is disabled for security.
+ */
+export const Text = memo(function Text({node, surfaceId}: A2UIComponentProps) {
+ const {theme, resolveString} = useA2UIComponent(node, surfaceId);
+ const props = node.properties;
+
+ const textValue = resolveString(props.text);
+ const usageHint = props.usageHint as UsageHint | undefined;
+
+ // Get merged classes (matches Lit's Styles.merge)
+ const classes = mergeClassMaps(
+ theme.components.Text.all,
+ usageHint ? theme.components.Text[usageHint] : {}
+ );
+
+ // Get additional styles based on usage hint
+ const additionalStyles = useMemo(() => {
+ const textStyles = theme.additionalStyles?.Text;
+ if (!textStyles) return undefined;
+
+ if (isHintedStyles(textStyles)) {
+ const hint = usageHint ?? 'body';
+ return stylesToObject(textStyles[hint]);
+ }
+ return stylesToObject(textStyles as Record);
+ }, [theme.additionalStyles?.Text, usageHint]);
+
+ // Render markdown content (matches Lit behavior - always uses markdown)
+ const renderedContent = useMemo(() => {
+ if (textValue === null || textValue === undefined) {
+ return null;
+ }
+
+ // Add markdown prefix based on usageHint (matches Lit behavior)
+ let markdownText = textValue;
+ switch (usageHint) {
+ case 'h1':
+ markdownText = `# ${markdownText}`;
+ break;
+ case 'h2':
+ markdownText = `## ${markdownText}`;
+ break;
+ case 'h3':
+ markdownText = `### ${markdownText}`;
+ break;
+ case 'h4':
+ markdownText = `#### ${markdownText}`;
+ break;
+ case 'h5':
+ markdownText = `##### ${markdownText}`;
+ break;
+ case 'caption':
+ markdownText = `*${markdownText}*`;
+ break;
+ default:
+ break; // Body - no prefix
+ }
+
+ return {__html: renderWithTheme(markdownText, theme.markdown)};
+ }, [textValue, theme.markdown, usageHint]);
+
+ if (!renderedContent) {
+ return null;
+ }
+
+ // Apply --weight CSS variable on root div (:host equivalent) for flex layouts
+ const hostStyle: React.CSSProperties =
+ node.weight !== undefined ? ({'--weight': node.weight} as React.CSSProperties) : {};
+
+ return (
+
+
+
+ );
+});
+
+export default Text;
diff --git a/renderers/react/src/components/content/Video.tsx b/renderers/react/src/components/content/Video.tsx
new file mode 100644
index 000000000..cf3b674ef
--- /dev/null
+++ b/renderers/react/src/components/content/Video.tsx
@@ -0,0 +1,82 @@
+/**
+ * Copyright 2026 Google LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import {memo} from 'react';
+import type * as Types from '@a2ui/web_core/types/types';
+import type {A2UIComponentProps} from '../../types';
+import {useA2UIComponent} from '../../hooks/useA2UIComponent';
+import {classMapToString, stylesToObject} from '../../lib/utils';
+
+/**
+ * Check if a URL is a YouTube URL and extract the video ID.
+ */
+function getYouTubeVideoId(url: string): string | null {
+ const patterns = [/(?:youtube\.com\/watch\?v=|youtu\.be\/|youtube\.com\/embed\/)([^&\s?]+)/];
+ for (const pattern of patterns) {
+ const match = url.match(pattern);
+ if (match && match.length > 1) {
+ // Non-null assertion is safe here since we checked match.length > 1
+
+ return match[1]!;
+ }
+ }
+ return null;
+}
+
+/**
+ * Video component - renders a video player.
+ *
+ * Supports regular video URLs and YouTube URLs (renders as embedded iframe).
+ */
+export const Video = memo(function Video({node, surfaceId}: A2UIComponentProps) {
+ const {theme, resolveString} = useA2UIComponent(node, surfaceId);
+ const props = node.properties;
+
+ const url = resolveString(props.url);
+
+ if (!url) {
+ return null;
+ }
+
+ const youtubeId = getYouTubeVideoId(url);
+
+ // Apply --weight CSS variable on root div (:host equivalent) for flex layouts
+ const hostStyle: React.CSSProperties =
+ node.weight !== undefined ? ({'--weight': node.weight} as React.CSSProperties) : {};
+
+ return (
+
+
+ {youtubeId ? (
+
+ ) : (
+
+ )}
+
+
+ );
+});
+
+export default Video;
diff --git a/renderers/react/src/components/content/index.ts b/renderers/react/src/components/content/index.ts
new file mode 100644
index 000000000..bf36c0952
--- /dev/null
+++ b/renderers/react/src/components/content/index.ts
@@ -0,0 +1,22 @@
+/**
+ * Copyright 2026 Google LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+export {Text} from './Text';
+export {Image} from './Image';
+export {Icon} from './Icon';
+export {Divider} from './Divider';
+export {Video} from './Video';
+export {AudioPlayer} from './AudioPlayer';
diff --git a/renderers/react/src/components/interactive/Button.tsx b/renderers/react/src/components/interactive/Button.tsx
new file mode 100644
index 000000000..50f47214f
--- /dev/null
+++ b/renderers/react/src/components/interactive/Button.tsx
@@ -0,0 +1,60 @@
+/**
+ * Copyright 2026 Google LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import {useCallback, memo} from 'react';
+import type * as Types from '@a2ui/web_core/types/types';
+import type {A2UIComponentProps} from '../../types';
+import {useA2UIComponent} from '../../hooks/useA2UIComponent';
+import {classMapToString, stylesToObject} from '../../lib/utils';
+import {ComponentNode} from '../../core/ComponentNode';
+
+/**
+ * Button component - a clickable element that triggers an action.
+ *
+ * Contains a child component (usually Text or Icon) and dispatches
+ * a user action when clicked.
+ */
+export const Button = memo(function Button({
+ node,
+ surfaceId,
+}: A2UIComponentProps) {
+ const {theme, sendAction} = useA2UIComponent(node, surfaceId);
+ const props = node.properties;
+
+ const handleClick = useCallback(() => {
+ if (props.action) {
+ sendAction(props.action);
+ }
+ }, [props.action, sendAction]);
+
+ // Apply --weight CSS variable on root div (:host equivalent) for flex layouts
+ const hostStyle: React.CSSProperties =
+ node.weight !== undefined ? ({'--weight': node.weight} as React.CSSProperties) : {};
+
+ return (
+
+
+
+ );
+});
+
+export default Button;
diff --git a/renderers/react/src/components/interactive/CheckBox.tsx b/renderers/react/src/components/interactive/CheckBox.tsx
new file mode 100644
index 000000000..26ec15f24
--- /dev/null
+++ b/renderers/react/src/components/interactive/CheckBox.tsx
@@ -0,0 +1,110 @@
+/**
+ * Copyright 2026 Google LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import {useState, useCallback, useEffect, useId, memo} from 'react';
+import type * as Types from '@a2ui/web_core/types/types';
+import type {A2UIComponentProps} from '../../types';
+import {useA2UIComponent} from '../../hooks/useA2UIComponent';
+import {classMapToString, stylesToObject} from '../../lib/utils';
+
+/**
+ * CheckBox component - a boolean toggle with a label.
+ *
+ * Supports two-way data binding for the checked state.
+ */
+export const CheckBox = memo(function CheckBox({
+ node,
+ surfaceId,
+}: A2UIComponentProps) {
+ const {theme, resolveString, resolveBoolean, setValue, getValue} = useA2UIComponent(
+ node,
+ surfaceId
+ );
+ const props = node.properties;
+ const id = useId();
+
+ const label = resolveString(props.label);
+ const valuePath = props.value?.path;
+ const initialChecked = resolveBoolean(props.value) ?? false;
+
+ const [checked, setChecked] = useState(initialChecked);
+
+ // Sync with external data model changes (path binding)
+ useEffect(() => {
+ if (valuePath) {
+ const externalValue = getValue(valuePath);
+ if (externalValue !== null && Boolean(externalValue) !== checked) {
+ setChecked(Boolean(externalValue));
+ }
+ }
+ }, [valuePath, getValue]); // eslint-disable-line react-hooks/exhaustive-deps
+
+ // Sync when literal value changes from props (server-driven updates via surfaceUpdate)
+ useEffect(() => {
+ if (props.value?.literalBoolean !== undefined) {
+ setChecked(props.value.literalBoolean);
+ }
+ }, [props.value?.literalBoolean]);
+
+ const handleChange = useCallback(
+ (e: React.ChangeEvent) => {
+ const newValue = e.target.checked;
+ setChecked(newValue);
+
+ // Two-way binding: update data model
+ if (valuePath) {
+ setValue(valuePath, newValue);
+ }
+ },
+ [valuePath, setValue]
+ );
+
+ // Structure mirrors Lit's CheckBox component:
+ //
+
+ // Apply --weight CSS variable on root div (:host equivalent) for flex layouts
+ const hostStyle: React.CSSProperties =
+ node.weight !== undefined ? ({'--weight': node.weight} as React.CSSProperties) : {};
+
+ return (
+
+
+
+
+
+
+ );
+});
+
+export default DateTimeInput;
diff --git a/renderers/react/src/components/interactive/MultipleChoice.tsx b/renderers/react/src/components/interactive/MultipleChoice.tsx
new file mode 100644
index 000000000..86a7006c9
--- /dev/null
+++ b/renderers/react/src/components/interactive/MultipleChoice.tsx
@@ -0,0 +1,93 @@
+/**
+ * Copyright 2026 Google LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import {useCallback, useId, memo} from 'react';
+import type * as Types from '@a2ui/web_core/types/types';
+import type {A2UIComponentProps} from '../../types';
+import {useA2UIComponent} from '../../hooks/useA2UIComponent';
+import {classMapToString, stylesToObject} from '../../lib/utils';
+
+/**
+ * MultipleChoice component - a selection component using a dropdown.
+ *
+ * Renders a