Storyteller Cards are flexible, themeable components designed to promote content or direct users to key sections within your app. They can display a background image or video, along with an optional title and subtitle. Tapping on a Card can trigger various actions, such as opening a specific Story, a Story Category, a Clip, a Clip Collection or any other action defined in the CMS. The server or personalization engine can choose which Cards to return for a given user.
Cards currently support 1:1, 2:3, 3:4, 4:5, 9:16, 16:9, and 4:1 aspect ratios from CMS payloads.
For Jetpack Compose, use the StorytellerCard composable.
Data Model: Create a StorytellerCardDataModel object, specifying the collectionId for the Card collection you want to display. You can also provide optional context data for analytics. When configured, context will be included in all analytics events when users interact with the Card. See Analytics for more details.
State: Initialize a StorytellerCardState using rememberStorytellerCardState() to manage the card's state and reload functionality.
View: Create the StorytellerCard composable, passing in the StorytellerCardDataModel instance and state.
Delegate (Optional): Provide an optional StorytellerCardViewDelegate to handle events like onDataLoadComplete. This allows you to react to data loading success or failure (e.g., by hiding the component).
importandroidx.compose.runtime.Composableimportandroidx.compose.runtime.rememberimportandroidx.compose.ui.Modifierimportcom.storyteller.ui.compose.components.cards.StorytellerCardimportcom.storyteller.ui.compose.components.cards.StorytellerCardDataModelimportcom.storyteller.ui.compose.components.cards.StorytellerCardViewDelegateimportcom.storyteller.ui.compose.components.cards.rememberStorytellerCardStateimportcom.storyteller.domain.entities.StorytellerError@ComposablefunMyCardSection(){valdataModel=StorytellerCardDataModel(collectionId="card-collection-id",context=mapOf("placementId"to"home_card","location"to"Home"))valcardState=rememberStorytellerCardState(dataModel.collectionId)valcardViewDelegate=remember{object:StorytellerCardViewDelegate{overridefunonDataLoadComplete(success:Boolean,error:StorytellerError?,dataCount:Int){if(!success){// Example: update your UI state here (e.g., hide this section on failure).}}}}StorytellerCard(dataModel=dataModel,state=cardState,delegate=cardViewDelegate,modifier=Modifier)// Optional: Add a reload buttonButton(onClick={cardState.reloadData()}){Text("Reload Card")}}
Video Cards are muted by default. When Cards audio is enabled by the CMS/API settings theme, the active video Card displays an audio button that lets the user switch the Cards session between muted and unmuted.
Cards audio behavior is session-scoped:
only the active video Card can output audio
inactive, off-screen, image-only, and audio-unavailable Cards remain muted
the selected mute state follows the active video Card while the Cards surface remains open
if theme.behavior.cards.persistMuteState is false or missing, a new Cards session starts from theme.behavior.cards.defaultMuteState
if theme.behavior.cards.persistMuteState is true, the user's Cards mute choice is reused for that user
audio focus loss or interruption forces Cards back to muted
Cards audio is controlled by the CMS/API settings theme under theme.behavior.cards:
showMuteToggle: set true to show the toggle for active audio-capable video Cards. False, null, or missing keeps Cards muted with no icon.
persistMuteState: set true to persist the user's Cards mute choice. False, null, or missing keeps Cards state session-scoped.
defaultMuteState: uses soundOff, soundOn, or respectDeviceSilentToggle. Missing, null, or unrecognized values default to soundOff for Cards.
On Android, respectDeviceSilentToggle initializes Cards from the current ringer/silent mode when the Cards session starts. After initialization, only the Cards audio button changes Cards mute state; hardware volume buttons and ringer/silent changes do not mute or unmute Cards.
NBA/default muted-toggle behavior is showMuteToggle = true, persistMuteState = false, and defaultMuteState = soundOff. Video Cards that the API marks with hasAudio=false remain muted and do not show the audio toggle.
The audio button icons can be replaced through the local SDK theme, separately from the CMS/API audio behavior:
These icons affect only Cards audio controls. Stories and Clips player mute icons still use theme.player.icons.mute.
Custom Cards audio icons replace the whole 48 x 48 button visual. The SDK-drawn circular background is only used for the packaged default icons, so client apps that need a circle around a custom icon should include it in the custom asset.
In the CMS you can make Card collections be ordered based on viewed or tapped status, so that once a Card is viewed/tapped, the next Card from the collection will be shown to the user. This will enable users to always see fresh content.
Card appearance and behavior are primarily configured directly within the Storyteller CMS for each Card Collection.
The following properties can be configured in the CMS and influence the Card's presentation and behaviour:
style.textLengthMode (default: truncate): How text that exceeds the available space is handled.
truncate: Display text at the specified size; truncate with an ellipsis (...) if it doesn't fit.
resize: Start at the specified text size and reduce the font size until the text fits (up to two lines for heading and subheading).
style.textAlignment (default: start): Horizontal alignment of the heading and subheading. Can be start, center, or end.
style.padding (default: 12): Inner padding around the text content.
For full-bleed cards (marginHorizontal = 0) with text below the image and all cards with text on the image, padding is applied to all sides of the text.
For cards with text below the image where marginHorizontal > 0, padding is applied only to the top and bottom of the text.
style.marginHorizontal (default: 0): Horizontal margin around the card. 0 means full-bleed.
style.cornerRadius (default: {theme.primitives.cornerRadius}): Corner radius of the card. The application depends on marginHorizontal and text position.
Not applied for full-bleed cards (marginHorizontal=0) with text below the image.
Applied to the image for cards with text below the image and marginHorizontal > 0.
Applied to the whole card for cards with text on the image and marginHorizontal > 0.
style.headingsSpacing (default: 3): Vertical spacing between the heading and subheading.
style.buttonSpacing (optional): Vertical spacing between the headings block and the button. When omitted, Android preserves the previous effective gap of style.padding * 2.
style.dynamicTypeEnabled (default: true): Whether cards typography participates in Android Dynamic Type scaling. When false, cards keep fixed text sizes, line heights, and letter spacing regardless of system font scale.
style.backgroundColorLight (optional): Light-mode background color for the text container when textOverContent = false and the card is full-bleed (marginHorizontal = 0).
style.backgroundColorDark (optional): Dark-mode background color for the text container when textOverContent = false and the card is full-bleed (marginHorizontal = 0).
style.heading.font (default: {theme.customFont}): Font family for the heading.
style.heading.textSize (default: 22): Font size for the heading.
style.heading.lineHeight (default: 28): Line height for the heading.
style.heading.textCase (default: default): Text case transformation (upper, lower, default).
style.heading.letterSpacing (default: 0): Letter spacing for the heading.
style.heading.textColor (default: {theme.colors.white.primary}): Text color for the heading when text is displayed on the background asset.
style.heading.textBelowContentColorLight (optional): Light-mode heading text color override when textOverContent = false.
style.heading.textBelowContentColorDark (optional): Dark-mode heading text color override when textOverContent = false.
style.subHeading.font (default: {theme.customFont}): Font family for the subheading.
style.subHeading.textSize (default: 16): Font size for the subheading.
style.subHeading.lineHeight (default: 21): Line height for the subheading.
style.subHeading.textCase (default: default): Text case transformation (upper, lower, default).
style.subHeading.letterSpacing (default: 0): Letter spacing for the subheading.
style.subHeading.textColor (default: {theme.colors.white.secondary}): Text color for the subheading when text is displayed on the background asset.
style.subHeading.textBelowContentColorLight (optional): Light-mode subheading text color override when textOverContent = false.
style.subHeading.textBelowContentColorDark (optional): Dark-mode subheading text color override when textOverContent = false.
behavior.reloading.reloadOnExit (default: true): Whether the Card Collection reloads after returning from tapping a Card (e.g., after dismissing the Story/Clip Player).
behavior.reloading.reloadOnForeground (default: true): Whether the Card Collection reloads when the app comes to the foreground.
Button positioning: Buttons are optional visual elements that follow the textOverContent property:
When textOverContent = true: Button appears on the card (overlaying the content), positioned below the title/subtitle
When textOverContent = false: Button appears below the card (below the title/subtitle section)
Button functionality: Buttons do not change the tappability of Cards - the entire card remains tappable and executes the same action as the button when tapped
Button text: The button text is defined in the Card data, not the theme
style.button.title.font (default: uses heading font): Font family for the button text. If not specified or null, uses the heading font with the button's text size and line height.
style.button.title.textSize (default: 16): Font size for the button text.
style.button.title.lineHeight (default: 21): Line height for the button text.
style.button.title.textCase (default: default): Text case transformation for the button text (upper, lower, default).
style.button.title.letterSpacing (default: 0): Letter spacing for the button text.
style.button.title.textColor (default: {theme.colors.white.primary}): Text color for the button when text is displayed on the background asset.
style.button.title.textBelowContentColorLight (optional): Light-mode button text color override when textOverContent = false.
style.button.title.textBelowContentColorDark (optional): Dark-mode button text color override when textOverContent = false.
style.button.backgroundColor (optional): Background color of the button. If not set, the button will have a transparent background with an outline.
style.button.outlineColor (default: {theme.colors.white.primary} for text on image, otherwise the resolved button title below-content color when present, falling back to {theme.colors.black.primary} in light mode and {theme.colors.white.primary} in dark mode): Color of the button outline/border.
style.button.outlineWidth (default: 1): Width of the button outline/border in points.
style.button.cornerRadius (optional, default: {theme.primitives.cornerRadius}): Corner radius of the button. If null or not set, falls back to the theme's default corner radius.
style.button.textAlignment (default: uses card textAlignment): Text alignment for the button text. If not specified or null, uses the card's text alignment setting.
{"slug": "cards", "page_title": "Cards", "page_url": "Cards/", "canonical_url": "/android/Cards/", "markdown": "# Storyteller Cards\n\nStoryteller Cards are flexible, themeable components designed to promote content or direct users to key sections within your app. They can display a background image or video, along with an optional title and subtitle. Tapping on a Card can trigger various actions, such as opening a specific Story, a Story Category, a Clip, a Clip Collection or any other action defined in the CMS. The server or personalization engine can choose which Cards to return for a given user.\n\nCards currently support `1:1`, `2:3`, `3:4`, `4:5`, `9:16`, `16:9`, and `4:1` aspect ratios from CMS payloads.\n\n## Showcase examples\n\n- [Compose \u2014 rendering Cards (`HomeScreen`)](https://github.com/getstoryteller/storyteller-showcase-android/blob/main/compose/app/src/main/java/com/getstoryteller/storytellershowcaseapp/ui/features/home/HomeScreen.kt#L206)\n\n## Usage\n\nYou can integrate Storyteller Cards into your app using Jetpack Compose.\n\n### Jetpack Compose\n\nFor Jetpack Compose, use the `StorytellerCard` composable.\n\n1. **Data Model**: Create a `StorytellerCardDataModel` object, specifying the `collectionId` for the Card collection you want to display. You can also provide optional `context` data for analytics. When configured, `context` will be included in all analytics events when users interact with the Card. See [Analytics](Analytics.md#context) for more details.\n2. **State**: Initialize a `StorytellerCardState` using `rememberStorytellerCardState()` to manage the card's state and reload functionality.\n3. **View**: Create the `StorytellerCard` composable, passing in the `StorytellerCardDataModel` instance and state.\n4. **Delegate (Optional)**: Provide an optional `StorytellerCardViewDelegate` to handle events like `onDataLoadComplete`. This allows you to react to data loading success or failure (e.g., by hiding the component).\n\n```kotlin\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.remember\nimport androidx.compose.ui.Modifier\nimport com.storyteller.ui.compose.components.cards.StorytellerCard\nimport com.storyteller.ui.compose.components.cards.StorytellerCardDataModel\nimport com.storyteller.ui.compose.components.cards.StorytellerCardViewDelegate\nimport com.storyteller.ui.compose.components.cards.rememberStorytellerCardState\nimport com.storyteller.domain.entities.StorytellerError\n\n@Composable\nfun MyCardSection() {\n val dataModel = StorytellerCardDataModel(\n collectionId = \"card-collection-id\",\n context = mapOf(\"placementId\" to \"home_card\", \"location\" to \"Home\")\n )\n val cardState = rememberStorytellerCardState(dataModel.collectionId)\n\n val cardViewDelegate = remember {\n object : StorytellerCardViewDelegate {\n override fun onDataLoadComplete(success: Boolean, error: StorytellerError?, dataCount: Int) {\n if (!success) {\n // Example: update your UI state here (e.g., hide this section on failure).\n }\n }\n }\n }\n\n StorytellerCard(\n dataModel = dataModel,\n state = cardState,\n delegate = cardViewDelegate,\n modifier = Modifier\n )\n\n // Optional: Add a reload button\n Button(onClick = { cardState.reloadData() }) {\n Text(\"Reload Card\")\n }\n}\n```\n\n## Reloading\n\nThe `StorytellerCardState` provides a `reloadData()` method. Call this method to manually trigger a refresh of the Card data from the server.\n\n## Video Card Audio\n\nVideo Cards are muted by default. When Cards audio is enabled by the CMS/API settings theme, the active video Card displays an audio button that lets the user switch the Cards session between muted and unmuted.\n\nCards audio behavior is session-scoped:\n\n- only the active video Card can output audio\n- inactive, off-screen, image-only, and audio-unavailable Cards remain muted\n- the selected mute state follows the active video Card while the Cards surface remains open\n- if `theme.behavior.cards.persistMuteState` is false or missing, a new Cards session starts from `theme.behavior.cards.defaultMuteState`\n- if `theme.behavior.cards.persistMuteState` is true, the user's Cards mute choice is reused for that user\n- audio focus loss or interruption forces Cards back to muted\n\nCards audio is controlled by the CMS/API settings theme under `theme.behavior.cards`:\n\n- `showMuteToggle`: set `true` to show the toggle for active audio-capable video Cards. False, null, or missing keeps Cards muted with no icon.\n- `persistMuteState`: set `true` to persist the user's Cards mute choice. False, null, or missing keeps Cards state session-scoped.\n- `defaultMuteState`: uses `soundOff`, `soundOn`, or `respectDeviceSilentToggle`. Missing, null, or unrecognized values default to `soundOff` for Cards.\n\nOn Android, `respectDeviceSilentToggle` initializes Cards from the current ringer/silent mode when the Cards session starts. After initialization, only the Cards audio button changes Cards mute state; hardware volume buttons and ringer/silent changes do not mute or unmute Cards.\n\nNBA/default muted-toggle behavior is `showMuteToggle = true`, `persistMuteState = false`, and `defaultMuteState = soundOff`. Video Cards that the API marks with `hasAudio=false` remain muted and do not show the audio toggle.\n\nThe audio button icons can be replaced through the local SDK theme, separately from the CMS/API audio behavior:\n\n```kotlin\nStoryteller.theme = buildTheme {\n light {\n cards {\n audio {\n mutedIcon = drawableRes(R.drawable.custom_cards_audio_muted)\n unmutedIcon = drawableRes(R.drawable.custom_cards_audio_unmuted)\n }\n }\n }\n}\n```\n\nThese icons affect only Cards audio controls. Stories and Clips player mute icons still use `theme.player.icons.mute`.\nCustom Cards audio icons replace the whole 48 x 48 button visual. The SDK-drawn circular background is only used for the packaged default icons, so client apps that need a circle around a custom icon should include it in the custom asset.\n\n## Viewed/Tapped Ordering\n\nIn the CMS you can make Card collections be ordered based on viewed or tapped status, so that once a Card is viewed/tapped, the next Card from the collection will be shown to the user. This will enable users to always see fresh content.\n\n## Theming\n\nCard appearance and behavior are primarily configured directly within the Storyteller CMS for each Card Collection.\nThe following properties can be configured in the CMS and influence the Card's presentation and behaviour:\n\n- `style.textLengthMode` (default: `truncate`): How text that exceeds the available space is handled.\n + `truncate`: Display text at the specified size; truncate with an ellipsis (...) if it doesn't fit.\n + `resize`: Start at the specified text size and reduce the font size until the text fits (up to two lines for heading and subheading).\n- `style.textAlignment` (default: `start`): Horizontal alignment of the heading and subheading. Can be `start`, `center`, or `end`.\n- `style.padding` (default: `12`): Inner padding around the text content.\n + For full-bleed cards (`marginHorizontal = 0`) with text _below_ the image and _all_ cards with text _on_ the image, padding is applied to all sides of the text.\n + For cards with text _below_ the image where `marginHorizontal > 0`, padding is applied only to the top and bottom of the text.\n- `style.marginHorizontal` (default: `0`): Horizontal margin around the card. `0` means full-bleed.\n- `style.cornerRadius` (default: `{theme.primitives.cornerRadius}`): Corner radius of the card. The application depends on `marginHorizontal` and text position.\n + Not applied for full-bleed cards (`marginHorizontal=0`) with text _below_ the image.\n + Applied to the _image_ for cards with text _below_ the image and `marginHorizontal > 0`.\n + Applied to the _whole card_ for cards with text _on_ the image and `marginHorizontal > 0`.\n- `style.headingsSpacing` (default: `3`): Vertical spacing between the heading and subheading.\n- `style.buttonSpacing` (optional): Vertical spacing between the headings block and the button. When omitted, Android preserves the previous effective gap of `style.padding * 2`.\n- `style.dynamicTypeEnabled` (default: `true`): Whether cards typography participates in Android Dynamic Type scaling. When `false`, cards keep fixed text sizes, line heights, and letter spacing regardless of system font scale.\n- `style.backgroundColorLight` (optional): Light-mode background color for the text container when `textOverContent = false` and the card is full-bleed (`marginHorizontal = 0`).\n- `style.backgroundColorDark` (optional): Dark-mode background color for the text container when `textOverContent = false` and the card is full-bleed (`marginHorizontal = 0`).\n- `style.heading.font` (default: `{theme.customFont}`): Font family for the heading.\n- `style.heading.textSize` (default: `22`): Font size for the heading.\n- `style.heading.lineHeight` (default: `28`): Line height for the heading.\n- `style.heading.textCase` (default: `default`): Text case transformation (`upper`, `lower`, `default`).\n- `style.heading.letterSpacing` (default: `0`): Letter spacing for the heading.\n- `style.heading.textColor` (default: `{theme.colors.white.primary}`): Text color for the heading when text is displayed _on_ the background asset.\n- `style.heading.textBelowContentColorLight` (optional): Light-mode heading text color override when `textOverContent = false`.\n- `style.heading.textBelowContentColorDark` (optional): Dark-mode heading text color override when `textOverContent = false`.\n- `style.subHeading.font` (default: `{theme.customFont}`): Font family for the subheading.\n- `style.subHeading.textSize` (default: `16`): Font size for the subheading.\n- `style.subHeading.lineHeight` (default: `21`): Line height for the subheading.\n- `style.subHeading.textCase` (default: `default`): Text case transformation (`upper`, `lower`, `default`).\n- `style.subHeading.letterSpacing` (default: `0`): Letter spacing for the subheading.\n- `style.subHeading.textColor` (default: `{theme.colors.white.secondary}`): Text color for the subheading when text is displayed _on_ the background asset.\n- `style.subHeading.textBelowContentColorLight` (optional): Light-mode subheading text color override when `textOverContent = false`.\n- `style.subHeading.textBelowContentColorDark` (optional): Dark-mode subheading text color override when `textOverContent = false`.\n- `behavior.reloading.reloadOnExit` (default: `true`): Whether the Card Collection reloads after returning from tapping a Card (e.g., after dismissing the Story/Clip Player).\n- `behavior.reloading.reloadOnForeground` (default: `true`): Whether the Card Collection reloads when the app comes to the foreground.\n\n### Button Behavior\n\n- **Button positioning**: Buttons are optional visual elements that follow the `textOverContent` property:\n + When `textOverContent = true`: Button appears on the card (overlaying the content), positioned below the title/subtitle\n + When `textOverContent = false`: Button appears below the card (below the title/subtitle section)\n- **Button functionality**: Buttons do not change the tappability of Cards - the entire card remains tappable and executes the same action as the button when tapped\n- **Button text**: The button text is defined in the Card data, not the theme\n\n### Button Theme Properties\n\n- `style.button.title.font` (default: uses heading font): Font family for the button text. If not specified or null, uses the heading font with the button's text size and line height.\n- `style.button.title.textSize` (default: `16`): Font size for the button text.\n- `style.button.title.lineHeight` (default: `21`): Line height for the button text.\n- `style.button.title.textCase` (default: `default`): Text case transformation for the button text (`upper`, `lower`, `default`).\n- `style.button.title.letterSpacing` (default: `0`): Letter spacing for the button text.\n- `style.button.title.textColor` (default: `{theme.colors.white.primary}`): Text color for the button when text is displayed _on_ the background asset.\n- `style.button.title.textBelowContentColorLight` (optional): Light-mode button text color override when `textOverContent = false`.\n- `style.button.title.textBelowContentColorDark` (optional): Dark-mode button text color override when `textOverContent = false`.\n- `style.button.backgroundColor` (optional): Background color of the button. If not set, the button will have a transparent background with an outline.\n- `style.button.outlineColor` (default: `{theme.colors.white.primary}` for text on image, otherwise the resolved button title below-content color when present, falling back to `{theme.colors.black.primary}` in light mode and `{theme.colors.white.primary}` in dark mode): Color of the button outline/border.\n- `style.button.outlineWidth` (default: `1`): Width of the button outline/border in points.\n- `style.button.cornerRadius` (optional, default: `{theme.primitives.cornerRadius}`): Corner radius of the button. If null or not set, falls back to the theme's default corner radius.\n- `style.button.textAlignment` (default: uses card `textAlignment`): Text alignment for the button text. If not specified or null, uses the card's text alignment setting.\n", "copy_markdown_include_header": false, "base_path": "android", "ai_dir": "ai", "missing_payload_behavior": "empty"}