Skip to content

Card System

A card is container for a few short, related pieces of information. It serves as an entry point or preview to more detailed information.

Applications can place various components within a card from a set of UI elements. Specific card types (e.g. list card, object card) do not exist anymore. Instead of having fixed card types we provide best practices and examples on how to create common card types with the new card concept.

The new card system has an adaptive layout with a flexible width and height due to the many different device sizes, resolutions, and layouts of Android devices.

The content displayed in a card is independent of the form factor (a specific card from a mobile app shows the same type and amount of content as in the tablet app, for example).

The new cards are optimized for list/staggered and carousel grid layouts, which support all device sizes and modes (portrait and landscape). All cards grid layouts can be calculated based on a “1 x 1 card” size. The formula to calculate card size can be found below in the grid layout section of this document.

Card Anatomy

Card contents are grouped into blocks. The card height is determined by the content in the card. The card width is determined by the type of grid layout that the card is placed in as well as the device type/orientation.

Card Block Alignments

An example of a card can be found in the demo application, which looks like this:

Card Demo

Card Header

The card header is the primary block of the card and contains the most important information. To ensure a consistent information architecture across the different card sizes, the UI elements are grouped into three blocks: Media, Main Header, and Extended Header.

Examples of card header:

Card Header Card Header Without Media

Card Body

The body is the middle area of a card, which can be utilized to provide additional context on the card, in addition to the header content. This area helps users find more specific details related to the card and can provide enough information at times to prompt the user to take action.

While the body section is flexible, you should aim to increase comprehensibility for users by only including the most important additional details. If the user needs to filter through list items or complete a complex task, we recommend including more functionality and information on the details page that follows after the user taps the container. The body section can have its own touch points or join the header container (outside of the overflow) to be a single touch point.

Card Body Block Alignments

The height of the UI element determines the height of the row inside the body. There can be as many rows as needed, until the maximum height of the card is reached. The UI elements themselves don’t have padding. The padding is derived from the outer body block.

The types of UI elements currently supported include:

  • Divider
  • Object Cell
  • Data Table
  • Key Value Cell
  • Description
  • Status and Info Labels
  • Header
  • Avatar Row
  • Media (image only)
  • Numeric KPI
  • Progress View KPI
  • Extra Spacing

An example of a card with only the body section is shown here. It contains a progress view KPI, a data table, a key value cell and a description. Card With Body Only

More examples of cards with different types of UI elements in the body section:

Card-Job Posting Card-Contacts Card-Company Profile

The footer is the bottom area of a card used for relevant or routine actions, such as "Approve" or "Submit." It supports up to two action buttons, which can be styled as text or symbol buttons. Additional actions can be included in an overflow menu.

To increase comprehensibility for users, we recommend the use of text buttons, as this makes it immediately clear what action will be performed after tap.

Examples of cards with footers:

Card-Marriott Hotel Card-Open Orders Card-Leave Request

Card Grids

Currently two types of card grid layouts are supported:

  • List/Staggered Grid
  • Carousel Grid

Depending on the type of card grid, the card width is calculated using the WindowSizeClass of the form factor.

List/Staggered Grid

This grid layout displays a group of cards in a vertically-stacked list layout or a vertically-staggered layout, depending on the WindowSizeClass. On a Compact screen, (such as a phone in portrait mode), it will be a list. The list will transform automatically when the phone is turned to landscape mode or when the grid is being displayed on a folder device or a tablet that has a WindowSizeClass of Medium or Expanded.

List or Staggered Grid on a Phone:

List Grid on a Phone Staggered Grid on a Phone in Landscape Mode

Staggered Grid on a Tablet:

Staggered Grid on a Tablet

This grid layout will display a group of cards in a horizontally scrollable carousel layout. The cards inside the carousel can either all have the same height as the tallest card inside the group or have varied heights. If all the cards have the same height, extra space is added between the body and the footer sections for shorter cards.

Multiple Carousel Grids on a Tablet:

Carousel Grid on a Tablet

How to Create a Card

To create a card, call the FioriMobileCard function as follows:

    FioriMobileCard(
        mobileCardData = mobileCardData,
        width = mobileCardDimensions.largeCardWidth,
        minHeight = mobileCardDimensions.minHeight,
    )

FioriMobileCard is a composable function with the following signature:

@OptIn(ExperimentalComposeUiApi::class)
@Composable
fun FioriMobileCard(
    mobileCardData: MobileCardData,
    modifier: Modifier = Modifier,
    colors: MobileCardColors = MobileCardDefaults.colors(),
    textStyles: MobileCardTextStyles = MobileCardDefaults.textStyles(),
    styles: MobileCardStyles = MobileCardDefaults.styles(),
    borderColors: MobileCardBorderColors = MobileCardDefaults.borderColors(),
    width: Dp = 0.dp,
    minHeight: Dp = 0.dp,
    maxHeight: Dp = 0.dp,
    inSameHeightCarousel: Boolean = false,
    placeHolderSize: MobileCardPlaceHolderSize = MobileCardPlaceHolderSize.Medium,
) {
    ...
}

In the parameters, the MobileCardData is a comprehensive data class that defines the content and layout of a card. It provides almost all the necessary APIs to build a card.

@Parcelize
data class MobileCardData(
    val mainHeader: @RawValue MobileCardMainHeaderData? = null,
    val mediaHeader: @RawValue MobileCardMediaHeaderData? = null,
    val extendedHeader: @RawValue MobileCardExtendedHeaderData? = null,
    val body: @RawValue MobileCardBodyData? = null,
    val footer: @RawValue MobileCardFooterData? = null,
    val interactions: @RawValue MobileCardInteractionData = MobileCardInteractionData(),
    val state: @RawValue MobileCardStateData? = null,
    val style: MobileCardStyle = MobileCardStyle.ELEVATED
): Parcelable

All the parameters, except for interactions, are optional. A card can include any combination of the following five sections:

  • Header
    • Media header
    • Main header
    • Extended header
  • Body
  • Footer

An optional state parameter is used to define the empty state or error state of the card, if required.

The default interactions is not null, but sets the card to be non-clickable. It should be overridden if you need to make the card clickable and add click event handlers.

The default style is ELEVATED, which is the default card style. The other card styles are OUTLINED and FILLED.

Adding a Main Header and/or Media Header

If a card has a Main Header, in the mobileCardData object, set the mainHeader with an object of MobileCardMainHeaderData.

data class MobileCardMainHeaderData(
    val thumbnail: MobileCardHeaderThumbnail? = null,
    val title: String? = null,
    val titleOnMediaHeader: Boolean = false,
    val subtitle: String? = null,
    val action: MobileCardHeaderAction? = null,
    val counter: String? = null,
    val statusInfoLabels: FioriStatusInfoLabelData? = null,
    val titleMarkdownData: MarkdownData = MarkdownData(),
    val subtitleMarkdownData: MarkdownData = MarkdownData(),
    val counterColor: FioriSemanticColors? = null,
    val statusInfoLabelsBgColor: Color? = null,
    val customComposableInSlot1: (@Composable () -> Unit)? = null,
    val customComposableInSlot2: (@Composable () -> Unit)? = null,
    val rightAccessory: MobileCardMainHeaderRightAccessory? = null,
    val customStyle: MobileCardMainHeaderCustomStyle? = null,
    val flexItem: MobileCardMainHeaderFlexItem? = null,
)

data class MobileCardMainHeaderRightAccessory(
  val horizontalAlignment:Alignment.Horizontal = Alignment.End,
  val verticalAlignment:Alignment.Vertical = Alignment.Top,
  val slot1: MobileCardMainHeaderRightAccessoryType = MobileCardMainHeaderRightAccessoryType.NONE,
  val slot2: MobileCardMainHeaderRightAccessoryType = MobileCardMainHeaderRightAccessoryType.NONE,
)

enum class MobileCardMainHeaderRightAccessoryType {
  ACTION, COUNTER, CUSTOM, NONE
}

If a card has a Media Header, in the mobileCardData object, set the mediaHeader with an object of MobileCardMediaHeaderData.

In the main header, to the left of the title, users can add either a thumbnail or a vertical status info label. The thumbnail can be an icon, image, or avatar, set with a MobileCardHeaderThumbnail object. For the status info label, use a FioriStatusInfoLabelData object. The background color of the label can be customized with the statusInfoLabelsBgColor parameter.

To the right of the title, there are two vertically-stacked slots known as right accessories. These slots can display a counter, an action button, and/or a custom composable. You can set the action button using a MobileCardHeaderAction object. If both slots are empty, the title stretches to the right edge of the card. The rightAccessory parameter controls which UI elements appear in the right accessory and how they're displayed.

In the main header, you can set the title and subtitle using a MarkdownData object. This lets you apply markdown syntax to the text. You can set the counter with a string and use an optional counterColor parameter to apply different semantic colors.

data class MobileCardMediaHeaderData(
    val mediaType: MobileCardMediaType = MobileCardMediaType.IMAGE,
    val image: CardImage? = null,
    val titleColorOption: MobileCardMediaTitleColor = MobileCardMediaTitleColor.T1,
    val titleColor: Color? = null,
)

Note that if the title is displayed on the Media Header, you have to set the titleOnMediaHeader flag to true inside the MobileCardMainHeaderData object. And, when there is only a Media Header with a title on it and there is no Main Header in the card layout, you still have to create the MobileCardMainHeaderData object for the mainHeader parameter because the title is technically considered a property of a Main Header.

Main Header Flex Item

You can add any composable to the main header using the MobileCardMainHeaderFlexItem. This feature is useful for placing custom content alongside the main header's title and subtitle. The flex item accepts a composable content and a position. The position controls the vertical placement within the header.

data class MobileCardMainHeaderFlexItem(
    val content: @Composable () -> Unit,
    val position: FlexItemPosition = FlexItemPosition.TOP,
)

enum class FlexItemPosition {
    TOP, MIDDLE, BOTTOM, OUTSIDE
}

Positions:

  • TOP — places the flex item above the title area, inside the header block.
  • MIDDLE — places the flex item centered alongside the title/subtitle area.
  • BOTTOM — places the flex item below the title area, but still inside the header block.
  • OUTSIDE — places the flex item outside of the title/subtitle area which sits on top and stretches the entire width of the main header except for the right accessory.

Example usage:

val mainHeader = MobileCardMainHeaderData(
    thumbnail = MobileCardHeaderThumbnail(...),
    title = "Hotel ABC",
    subtitle = "2 nights, 1 room",
    flexItem = MobileCardMainHeaderFlexItem(
        content = {
                Row {
                    FioriStatusInfoLabel(
                        data = FioriStatusInfoLabelData(
                            items = listOf(
                                FioriLabelItemData(
                                    label = "Time Offs",
                                    iconType = FioriLabelIconType.ICON,
                                    icon = FioriIcon(
                                        resId = com.sap.cloud.mobile.fiori.compose.R.drawable.ic_sap_icon_calendar,
                                        contentDescription = "Time Offs Icon",
                                    ),
                                    color = FioriSemanticColors.NEUTRAL,
                                ),
                            ),
                            hasSeparator = false
                        )
                    )
                }
            },
        position = FlexItemPosition.OUTSIDE
    )
)

val mobileCardData = MobileCardData(
    mainHeader = mainHeader,
    body = ...
)

Examples:

Flex Outside Flex Bottom Flex Outside

Notes:

  • The content composable doesn't receive any parameters: compose whatever UI you need. Keep it lightweight to enhance list performance.
  • Use OUTSIDE cautiously. Depending on the layout, you may need to adjust z-indexing or padding in custom styles to avoid overlap issues.

Adding an Extended Header

If a card has an extended header, in the mobileCardData object, set the extendedHeader with an object of MobileCardExtendedHeaderData.

data class MobileCardExtendedHeaderData(
    val rows: List<MobileCardExtendedHeaderRowData> = listOf(),
    val numericKpi: FioriNumericKpiData? = null,
    val customComposable: (@Composable () -> Unit)? = null,
    val rightAccessory: MobileCardExtendedHeaderRightAccessory? = null
)

data class MobileCardExtendedHeaderRowData(
    val items: List<ExtendedHeaderItem> = listOf(),
    val nowrap: Boolean = true,
    val maxLines: Int = 2
)

data class ExtendedHeaderItem(
    val type: ExtendedHeaderItemType,
    val rating: ExtendedHeaderRating? = null,
    val tags: List<FioriTagData>? = null,
    val labels: FioriStatusInfoLabelData? = null,
    val description: MobileCardDescription? = null
)

data class MobileCardExtendedHeaderRightAccessory(
  val horizontalAlignment:Alignment.Horizontal = Alignment.End,
  val verticalAlignment:Alignment.Vertical = Alignment.Bottom,
  val slot: MobileCardExtendedHeaderRightAccessoryType = MobileCardExtendedHeaderRightAccessoryType.NONE,
)

enum class MobileCardExtendedHeaderRightAccessoryType {
  NUMERIC_KPI, CUSTOM, NONE
}

enum class ExtendedHeaderItemType {
    RATING, TAG, LABEL, DESCRIPTION, CUSTOM_COMPOSABLE
}

An Extended Header can have up to three rows, with or without the right accessory. Each row can contain more than one ExtendedHeaderItem.

In the right accessory, users can add numeric KPIs or custom composables. To change the alignment of the right accessory, adjust the horizontalAlignment and verticalAlignment parameters. The default horizontal alignment is Alignment.End, while the default vertical alignment is Alignment.Bottom.

Adding a Body Section

If a card has a body section, in the mobileCardData object, set the body with an object of MobileCardBodyData.

data class MobileCardBodyData (
    val rows: List<MobileCardBodyRowData> = listOf(),
)

data class MobileCardBodyRowData (
    val bodyElementType: MobileCardBodyElementType = MobileCardBodyElementType.Spacer,
    val numOfSpacings: Int = 1,
    val cardCellData: List<FioriCardCellData>? = null,
    val dataTable: FioriDataTableData? = null,
    val keyValueCell: MobileCardKeyValueCellData? = null,
    val description: MobileCardDescription? = null,
    val labels: FioriStatusInfoLabelData? = null,
    val header: String? = null,
    val avatarRow: FioriAvatarConstruct? = null,
    val mediaImage: CardImage? = null,
    val progressKpi: MobileCardProgressViewKpiData? = null,
    val numericKpi: FioriNumericKpiData? = null,
    val calendarData: MobileCardCalendarData? = null,
    val customComposable: @Composable (RowScope.() -> Unit)? = null,
    val alignment: Alignment.Horizontal = Alignment.Start
)

The body section can have multiple rows. Currently, each row can have one type of UI element. The default horizontal alignment is Alignment.Start, which can be overridden.

If a card has a footer, in the mobileCardData object, set the footer with an object of MobileCardFooterData.

data class MobileCardFooterData(
    val buttonType: MobileCardFooterButtonType = MobileCardFooterButtonType.TEXT,
    val firstButton: MobileCardFooterButton,
    val secondButton: MobileCardFooterButton? = null,
    val oppositeLayout: Boolean = true,
    val overflowIconButton: CardIcon? = null,
    val overflowMenu: CardOverflowMenu? = null
)

The footer can contain one or two buttons, which must be either Text buttons or Icon buttons. Additionally, the footer can include an overflow menu.

Setting Interaction Mode and Actions

If a card is clickable, in the mobileCardData object, set the interactions with an object of MobileCardInteractionData.

data class MobileCardInteractionData(
    val interactionMode: MobileCardInteractionMode = MobileCardInteractionMode.CARD_CONTAINER,
    val cardClickable: Boolean = false,
    val onCardClick: (() -> Unit)? = { },
    val headerBodyClickable: Boolean = false,
    val onHeaderBodyClick: (() -> Unit)? = null,
    val headerClickable: Boolean = false,
    val onHeaderClick: (() -> Unit)? = null,
    val bodyClickable: Boolean = false,
    val onBodyClick: (() -> Unit)? = null
)

enum class MobileCardInteractionMode{
    CARD_CONTAINER, COMBINED_HB_FOOTER_SPLIT, HEADER_BODY_FOOTER_SPLIT, HEADER_BODY_SPLIT
}

There are four interaction modes supported in a card:

  • CARD_CONTAINER: the touch target is the entire card.
  • COMBINED_HB_FOOTER_SPLIT: the touch targets are split into header/body and footer.
  • HEADER_BODY_FOOTER_SPLIT: the touch targets are split into header, body, and footer separately.
  • HEADER_BODY_SPLIT: the touch targets are split into header and body (when there is no footer in a card).

For each interaction mode, the touch target can be either clickable or non-clickable. A separate click event handler can be set to each touch target. By default, the interaction mode is CARD_CONTAINER and the card is non-clickable.

Setting a State for a Card

When a card needs to display an empty state or error state, a MobileCardStateData object can be set for the state parameter.

data class MobileCardStateData(
    val title: String,
    val illustratedMessageData: FioriIllustratedMessageData? = null,
    val description: String? = null,
    val image: CardImage? = null,
    val buttonContent: FioriButtonContent? = null,
    var buttonOnClick: (() -> Unit)? = null,
    val displayMode: MobileCardStateDisplayMode = MobileCardStateDisplayMode.BODY
)

enum class MobileCardStateDisplayMode {
    BODY, HEADER_BODY, BODY_FOOTER, ENTIRE_CARD
}

Creating a List/Staggered Card Grid

Usually, cards are created inside a grid layout. The most common grid layout is a list or staggered grid. To create a list or staggered card grid, call the MobileCardListStaggeredGrid composable function, like so:

Column(
  modifier = Modifier.fillMaxWidth()
) {
  MobileCardListStaggeredGrid(
    windowSizeClass = windowSizeClass,
    cardList = cardList
  )
}

@Composable
fun MobileCardListStaggeredGrid(
  windowSizeClass: WindowSizeClass,
  cardList: List<MobileCardData>,
  modifier: Modifier = Modifier,
  placeHolderSize: MobileCardPlaceHolderSize = MobileCardPlaceHolderSize.Medium,
) {
    ...
}

The function will take care of the calculation of the card dimensions based on the windowSizeClass parameter the application passed in. It will automatically display the cards in a vertically scrollable list when the WindowSizeClass is Compact. And for Medium and Large WindowSizeClass, they will be displayed in a vertical-staggered grid layout.

Cards can also be displayed in a horizontally scrollable carousel grid. Call the MobileCardCarouselGrid composable function as follows:

Column(
  modifier = Modifier
    .fillMaxWidth()
    .verticalScroll(rememberScrollState())
) {
  MobileCardCarouselGrid(
    windowSizeClass = windowSizeClass,
    cardList = cardList1,
    useSameHeight = true,
  )

  MobileCardCarouselGrid(
    windowSizeClass = windowSizeClass,
    cardList = cardList2,
    useSameHeight = false,
  )

  MobileCardCarouselGrid(
    windowSizeClass = windowSizeClass,
    cardList = cardList3,
    useSameHeight = true,
    placeHolderSize = MobileCardPlaceHolderSize.Small,
  )
}

@Composable
fun MobileCardCarouselGrid(
  windowSizeClass: WindowSizeClass,
  cardList: List<MobileCardData>,
  modifier: Modifier = Modifier,
  cardWidth: MobileCardWidth = MobileCardWidth.LARGE,
  useSameHeight: Boolean = true,
  verticalAlignment: Alignment.Vertical = Alignment.Top,
  placeHolderSize: MobileCardPlaceHolderSize = MobileCardPlaceHolderSize.Medium,
) {
    ...
}

enum class MobileCardWidth{
  BASIC, LARGE
}

By default, the card width inside a carousel grid is set to be LARGE. And the useSameHight flag is set to true.

Card Width Calculation

In the preceding section, you see an enum class defined to represent the type of a card's width. There are two types of card's width:

  • Basic: refers to the width of a Basic Card.
  • Large: refers to the width of a Large Card.

So what is a Basic Card? And what is a Large Card?

Dynamic grid layout depends on the window class size. The screen width is split into a different number of columns depending on the different window class sizes.

Grid Specs

In List/Staggered Grid

List Grid Card Width

Carousel Grid Card Width

Apply Custom Styling to a Card

You can apply customized styling to Mobile Card and its sub-components by using customization data classes.

data class MobileCardMainHeaderCustomStyle(
    val labelsCustomStyles: MobileCardLabelsCustomStyles? = null,
)

data class MobileCardExtendedHeaderCustomStyle(
    val numericKpiCustomStyles: MobileCardNumericKpiCustomStyles? = null,
    val tagsCustomStyles: MobileCardTagsCustomStyles? = null,
    val labelsCustomStyles: MobileCardLabelsCustomStyles? = null,
)

data class MobileCardBodyCustomStyle(
    val cardCellCustomStyles: MobileCardCardCellCustomStyles? = null,
    val dataTableCustomStyles: MobileCardDataTableCustomStyles? = null,
    val labelsCustomStyles: MobileCardLabelsCustomStyles? = null,
    val numericKpiCustomStyles: MobileCardNumericKpiCustomStyles? = null,
    val calendarCustomStyles: MobileCardCalendarCustomStyles? = null,
    val avatarRowCustomStyles: MobileCardAvatarRowCustomStyles? = null,
    val keyValueCellCustomStyles: MobileCardKeyValueCellCustomStyles? = null,
)

The following card has been customized in styling using the above data classes. For example, the mobileCardExtendedHeaderData added the parameter: customStyle.

Card with Customized Styles

val customStyles =
        MobileCardExtendedHeaderCustomStyle(
            numericKpiCustomStyles = MobileCardNumericKpiCustomStyles(
                numericKpiColors = FioriNumericKpiDefaults.colors(
                    valueColor = Color.Red
                )
            ),
            tagsCustomStyles = MobileCardTagsCustomStyles(
                tagsStyles = FioriTagsRowDefaults.styles(
                    horizontalSpacing = 16.dp
                ),
                tagsTextStyles = FioriTagsRowDefaults.textStyles(
                    tagTextStyle = MaterialTheme.fioriHorizonAttributes.textAppearanceHeadline6
                )
            ),
            labelsCustomStyles = MobileCardLabelsCustomStyles(
                statusInfoLabelsColor = FioriLabelDefaults.colors(
                    defaultColor = Color.Cyan,
                    positiveColor = Color.Black
                ),
                statusInfoLabelsTextStyles = FioriLabelDefaults.textStyles(
                    labelTextStyle = MaterialTheme.fioriHorizonAttributes.textAppearanceHeadline6
                )

            )
        )

val mobileCardExtendedHeaderData = MobileCardExtendedHeaderData(
        ...
        customStyle = customStyles
)

A card now offers three different variants: Elevated, Outlined, and Filled. To switch between these variants, set the style parameter in MobileCardData.

Elevated Card Filled Card Outlined Card
@Parcelize
data class MobileCardData(
    val mainHeader: @RawValue MobileCardMainHeaderData? = null,
    val mediaHeader: @RawValue MobileCardMediaHeaderData? = null,
    val extendedHeader: @RawValue MobileCardExtendedHeaderData? = null,
    val body: @RawValue MobileCardBodyData? = null,
    val footer: @RawValue MobileCardFooterData? = null,
    val interactions: @RawValue MobileCardInteractionData = MobileCardInteractionData(),
    val state: @RawValue MobileCardStateData? = null,
    val style: MobileCardStyle = MobileCardStyle.ELEVATED
): Parcelable

enum class MobileCardStyle {
    ELEVATED, FILLED, OUTLINED,
}

Custom Card Layout (Flex Layout)

The card system now supports a flexible, declarative layout using MobileCardLayoutItem. Instead of relying on the built-in vertical flow (Media → Header (main+extended) → Body → Footer), you can provide a layoutItem on MobileCardData. This allows you to describe a custom nested layout for how the card's sections and media should appear.

Please note: the Main Header and Extended Header always combine into one logical HEADER section. You can't separate them into two distinct sections in a custom layout.

Example: MobileCardData with layoutItem:

@Parcelize
data class MobileCardData(
    val mainHeader: @RawValue MobileCardMainHeaderData? = null,
    val mediaHeader: @RawValue MobileCardMediaHeaderData? = null,
    val extendedHeader: @RawValue MobileCardExtendedHeaderData? = null,
    val body: @RawValue MobileCardBodyData? = null,
    val footer: @RawValue MobileCardFooterData? = null,
    val interactions: @RawValue MobileCardInteractionData = MobileCardInteractionData(),
    val state: @RawValue MobileCardStateData? = null,
    val style: MobileCardStyle = MobileCardStyle.ELEVATED,
    val layoutItem: @RawValue MobileCardLayoutItem? = null,
): Parcelable

MobileCardLayoutItem (sealed class) and supporting enums

sealed class MobileCardLayoutItem {
    data class Media(
        val weight: Float = 1f,
        val edgeAlignment: Set<Edge> = setOf()
    ) : MobileCardLayoutItem()

    data class Section(
        val cardSection: CardSection,
        val weight: Float = 1f,
        val edgeAlignment: Set<Edge> = setOf(),
    ) : MobileCardLayoutItem()

    data class NestedLayout(
        val orientation: Orientation,
        val weight: Float = 1f,
        val edgeAlignment: Set<Edge> = setOf(),
        val children: List<MobileCardLayoutItem>
    ) : MobileCardLayoutItem()
}

enum class CardSection {
    HEADER,
    BODY,
    FOOTER
}

enum class Orientation {
    VERTICAL,
    HORIZONTAL
}

enum class Edge {
    TOP,
    BOTTOM,
    START,
    END
}

Example: horizontal layout with media on the left and stacked header/body/footer on the right

layoutItem = MobileCardLayoutItem.NestedLayout(
    orientation = Orientation.HORIZONTAL,
    children = listOf(
        MobileCardLayoutItem.Media(
            weight = 1f,
            edgeAlignment = setOf(Edge.START, Edge.TOP, Edge.BOTTOM)
        ),
        MobileCardLayoutItem.NestedLayout(
            orientation = Orientation.VERTICAL,
            weight = 2f,
            edgeAlignment = setOf(Edge.TOP, Edge.BOTTOM, Edge.END),
            children = listOf(
                MobileCardLayoutItem.Section(
                    cardSection = CardSection.HEADER
                ),
                MobileCardLayoutItem.Section(
                    cardSection = CardSection.BODY,
                ),
                MobileCardLayoutItem.Section(
                    cardSection = CardSection.FOOTER
                )
            )
        )
    )
),

Usage and notes

  • The Media item maps to the card's media image, such as mediaHeader.image. It renders an image in the position described by the layout tree.
  • Section(CardSection.HEADER) renders the combined main and extended header content in the header area. You can't place mainHeader and extendedHeader separately. They're treated as a single header section.
  • Section(CardSection.BODY) corresponds to the card's body content. Section(CardSection.FOOTER) corresponds to the footer area.
  • NestedLayout lets you create nested arrangements: vertical stacks can contain horizontal rows and vice versa. We recommend keeping the nesting reasonably shallow to enhance performance and readability.
  • The weight property controls how space is distributed when siblings are arranged horizontally. When the parent is arranged vertically, weights are typically ignored. In this case, the content height is determined by the child content and card constraints.
  • You can use edgeAlignment in rendering code to adjust padding for items that touch the card edges. For example, you might use it to ensure an image aligns with the card's start edge.

Examples using the demo app screenshot

Flex Layout Cards Flex Layout Cards More

Implementation notes for engineers

  • The rendering code should traverse the MobileCardLayoutItem tree and position children based on orientation, weight, and edge alignment. Ensure that accessibility and focus order remain consistent with the visual order.
  • If layoutItem == null, the card should fall back to the original default vertical layout.
  • Validate user-supplied layouts: the rendering engine ensures that every Section used maps to an available content block. Missing sections render as empty placeholders or collapse gracefully.

Media aspect ratio and width calculation

When a media image in a MobileCardLayoutItem.Media has a fixed aspect ratio set by the developer, the rendering engine calculates the image width based on the aspect ratio and the available height. In a horizontal layout, the layout engine first determines the media image width while respecting the fixed aspect ratio. It then distributes the remaining horizontal space among sibling items according to their weight values.

If you don't set a fixed aspect ratio on the media image, widths are allocated based on weight among horizontal siblings. For example: a Media with weight=1.0f next to a NestedLayout with weight=2.0f allocates one-third of the available width to the media and two-thirds to the nested layout.

Example images: The top image uses a media-forced 1:1 aspect ratio. The bottom image has no aspect ratio. Weights of 1.0f versus 2.0f are used to calculate widths:

Media Aspect Ratio 1x1
Media Weights 1v2

Notes:

  • When you use fixed aspect ratios, test the card across different WindowSizeClass values. This ensures the calculated image width and resulting content wrapping or ellipsis are acceptable.
  • The rendering engine should clamp image sizes to reasonable minimum and maximum values. This prevents extremely large or tiny images when dealing with unusual aspect ratios or container sizes.
  • When you use both edgeAlignment and aspect ratio together, ensure you apply the visual padding rules after determining the image size. This ensures the image aligns correctly with the specified edges.

Last update: January 23, 2026