Card
A container for one subject: a header, a body and actions on a surface of their own, with media edge to edge above them.
import com.pantrixui.components.container.card.PantrixCard
- Three variants.
variantsays how the card is set apart from what it stands on:Elevatedby a shadow,Filledby a fill,Outlinedby a hairline. - Slots.
headerstands above the body andfooterbelow it;mediais drawn above them all, edge to edge, and takes none of the card's padding. The lines of the body have the theme's small spacing (spacing.sm) between them. - Height. A card given a height is filled by its body: the
footerstands at the card's foot, and a child of the body given aweighttakes the room between. The cards of a row are of one height this way. - One button. Given
onClick, the whole card is one button: a screen reader hears one target. A button in thefooterkeeps its own press. - A card that cannot be pressed. A card that has
onClickand whoseenabledis false gives its content the theme's disabled colour; a child that sets a colour of its own keeps it. - Content colour. On the theme's
surface, that is, in anElevatedand anOutlinedcard, the card gives its content no colour: each child is drawn in its own default. AFilledcard, and a card given a role's fill, gives the colour that reads on that fill. - Colours of its own.
CardColorsgives the fill, the content's colour and the edge; one that is left out stays the variant's. - Platform. The corners and the shadow depend on the platform (
CardStyle): a medium corner and a 2dp shadow on Android, a large corner and no shadow on iOS, a small corner and a 1dp shadow on desktop and the web. - Skeleton.
PantrixCardSkeletonstands in for a card whose data has not arrived: the card's own surface, with bars in it where the header, the lines and the buttons will be. It is announced to a screen reader once, for the whole card. - Automation.
tagKeygives the card the tagCard_<tagKey>; a press is reported as aCardevent, along with the card's variant.
Examples
At its plainest
Its shortest form: a PantrixCard and its body. No variant was given, so it is Elevated. The lines of the body stand one under another, with the theme's small spacing between them.
The first card was given no width: it is as wide as its widest line. The second was given fillMaxWidth() and fills the space it is in.
PantrixCard {
PantrixText(text = "Weekly summary", style = PantrixTheme.typography.title)
PantrixText(text = "Three new orders, one return")
}
PantrixCard(modifier = Modifier.fillMaxWidth()) {
PantrixText(text = "Monthly summary", style = PantrixTheme.typography.title)
PantrixText(text = "A width was given: the card fills the space it is in")
}
Three variants
variant picks how the card is set apart from what it stands on. Elevated is in the theme's surface colour and is lifted by a shadow; on iOS it has no shadow, and the surface colour alone sets the card apart. Filled is not lifted and is set apart by the neutral role's fill: it is for a card on a surface that is already raised. Outlined is edged by a hairline in the theme's divider colour: it is for dense lists of cards, where many shadows become noise.
PantrixCard(modifier = Modifier.fillMaxWidth(), variant = CardVariant.Elevated) {
PantrixText(text = "Elevated card", style = PantrixTheme.typography.title)
PantrixText(text = "The default variant: set apart by a shadow")
}
PantrixCard(modifier = Modifier.fillMaxWidth(), variant = CardVariant.Filled) {
PantrixText(text = "Filled card", style = PantrixTheme.typography.title)
PantrixText(text = "Set apart by colour instead of a shadow")
}
PantrixCard(modifier = Modifier.fillMaxWidth(), variant = CardVariant.Outlined) {
PantrixText(text = "Outlined card", style = PantrixTheme.typography.title)
PantrixText(text = "Set apart by a hairline")
}
Media, header and actions
The card's three slots. media stands at the very top, above the header too, and takes none of the card's padding: it reaches the card's edges, and the card cuts its corners. header is above the body and is usually the card's title. footer gathers the actions in a row at the card's end edge; the main action stands last.
Since the card was given a width, the media spans that width and the actions sit against the end edge.
PantrixCard(
modifier = Modifier.widthIn(max = 360.dp).fillMaxWidth(),
header = {
PantrixText(
text = "Compose Multiplatform",
style = PantrixTheme.typography.title,
role = TextRole.Heading(level = 3)
)
},
media = {
PantrixImage(
painter = painterResource(Res.drawable.compose_multiplatform),
modifier = Modifier.height(140.dp).background(PantrixTheme.colors.info.container),
contentDescription = null
)
},
footer = {
PantrixButton(text = "Later", onClick = {}, variant = ButtonVariant.Text, tagKey = "article_later")
PantrixButton(text = "Read", onClick = {}, tagKey = "article_read")
},
tagKey = "article"
) {
PantrixText(text = "Android, iOS, desktop and web from a single codebase.")
}
Of one height in a row
The cards of a row are as tall as the tallest: the row is given height(IntrinsicSize.Min), and each card weight(1f) and fillMaxHeight(). A card given a height is filled by its body: the footer stands at the card's foot, the button of the plan with the short summary too. A child of the body given Modifier.weight(1f) takes the room between and pushes the price down to the button.
A card given no height is as tall as what it holds, and its footer stands right under its body.
Row(
modifier = Modifier.fillMaxWidth().height(IntrinsicSize.Min),
horizontalArrangement = Arrangement.spacedBy(PantrixTheme.spacing.md)
) {
PLANS.forEach { plan ->
PantrixCard(
modifier = Modifier.weight(1f).fillMaxHeight(),
variant = CardVariant.Outlined,
footer = { PantrixButton(text = "Choose", onClick = {}, tagKey = "plan_${plan.key}") },
tagKey = "plan_${plan.key}"
) {
PantrixText(text = plan.name, style = PantrixTheme.typography.title)
PantrixText(text = plan.summary, color = PantrixTheme.colors.textSecondary)
Spacer(Modifier.weight(1f))
PantrixText(text = plan.price, style = PantrixTheme.typography.callout)
}
}
}
private val PLANS = listOf(
Plan("solo", "Solo", "For one app.", "Free"),
Plan("team", "Team", "For a team of up to five, with a shared theme and shared components.", "400 ₺ a month")
)
private class Plan(val key: String, val name: String, val summary: String, val price: String)
The whole card as one button
Given onClick, the whole card is one button. That is the accessible way to build a card that opens something: a screen reader hears one target, with the card's text or, if one is given, with contentDescription. Press the first card: the app's snackbar lets you know.
The second card's enabled is false: it cannot be pressed and gives its content the theme's disabled colour; this colour of its text is what says that it cannot be pressed. A card without onClick ignores enabled.
val snackbars = LocalPantrixSnackbars.current
val scope = rememberCoroutineScope()
PantrixCard(
modifier = Modifier.fillMaxWidth(),
variant = CardVariant.Outlined,
onClick = { scope.launch { snackbars?.show(message = "Order #1043 opened", tagKey = "order_opened") } },
contentDescription = "Open order 1043",
tagKey = "order_1043"
) {
PantrixText(text = "Order #1043", style = PantrixTheme.typography.title)
PantrixText(text = "The whole card is one button", color = PantrixTheme.colors.textSecondary)
}
PantrixCard(modifier = Modifier.fillMaxWidth(), variant = CardVariant.Outlined, onClick = {}, enabled = false) {
PantrixText(text = "Order #1039", style = PantrixTheme.typography.title)
PantrixText(text = "Cancelled, cannot be opened")
}
In its own colours
With colors the card takes the given colours, not its variant's; a field that is left out stays the variant's.
The first card was given only a fill, and that fill is the colour of one of the theme's roles: the theme itself pairs it with the colour that reads on it. The second card's fill is in none of the theme's roles; it is the app's own purple, and the theme cannot know what reads on it, so content is given too. On the third card, which is Outlined, only the edge changed.
val own = ownColors()
PantrixCard(
modifier = Modifier.fillMaxWidth(),
variant = CardVariant.Filled,
colors = CardColors(container = PantrixTheme.colors.info.container)
) {
PantrixText(text = "New feature", style = PantrixTheme.typography.title)
PantrixText(text = "Only the fill was given; the theme paired the text's colour with it")
}
PantrixCard(
modifier = Modifier.fillMaxWidth(),
variant = CardVariant.Filled,
colors = CardColors(container = own.container, content = own.onContainer),
tagKey = "own_colors"
) {
PantrixText(text = "Membership", style = PantrixTheme.typography.title)
PantrixText(text = "Both the fill and the content colour were given")
}
PantrixCard(
modifier = Modifier.fillMaxWidth(),
variant = CardVariant.Outlined,
colors = CardColors(border = own.color)
) {
PantrixText(text = "Selected plan", style = PantrixTheme.typography.title)
PantrixText(text = "Only the edge's colour was given")
}
Shape, padding and style
shape changes the corners of one card and wins over the style's corners. contentPadding is the room around the header, the body and the actions; its default is spacing.lg, and the second card was given spacing.sm.
style changes the card's geometry: on the last card, a deeper shadow with CardStyle(elevation = 8.dp). Only the field that is given changes, and it changes on every platform: this card has a shadow on iOS too.
PantrixCard(modifier = Modifier.fillMaxWidth(), shape = PantrixTheme.shapes.none) {
PantrixText(text = "Square corners", style = PantrixTheme.typography.title)
PantrixText(text = "shape = PantrixTheme.shapes.none")
}
PantrixCard(
modifier = Modifier.fillMaxWidth(),
variant = CardVariant.Outlined,
contentPadding = PaddingValues(PantrixTheme.spacing.sm)
) {
PantrixText(text = "Tight padding", style = PantrixTheme.typography.title)
PantrixText(text = "contentPadding = PaddingValues(PantrixTheme.spacing.sm)")
}
PantrixCard(modifier = Modifier.fillMaxWidth(), style = CardStyle(elevation = 8.dp)) {
PantrixText(text = "Deeper shadow", style = PantrixTheme.typography.title)
PantrixText(text = "style = CardStyle(elevation = 8.dp)")
}
Until the data arrives: the skeleton
Press "Refresh": for two seconds the two cards are replaced by their skeletons. PantrixCardSkeleton is not drawn to look like a card: it is built from the card's own shape, fill, shadow, padding and spacing. Its bars take the room of what they stand in for: when the data arrives the card's height does not change, and nothing below it moves.
The skeleton cannot know what is in the card: showHeader, lines, footerButtons and mediaHeight are given to match the card it stands in for. The first card has a header, one line and two buttons: lines = 1, footerButtons = 2.
The fill and the content colour of the "Campaign" card were given by the caller. Its skeleton takes the card's colours with colors too; its bars are drawn in the colour given with skeletonColors.
val own = ownColors()
val campaign = CardColors(container = own.container, content = own.onContainer)
var loading by remember { mutableStateOf(false) }
LaunchedEffect(loading) {
if (loading) {
delay(2_000)
loading = false
}
}
if (loading) {
PantrixCardSkeleton(modifier = Modifier.fillMaxWidth(), lines = 1, footerButtons = 2)
} else {
PantrixCard(
modifier = Modifier.fillMaxWidth(),
header = {
PantrixText(
text = "Order #1042",
style = PantrixTheme.typography.title,
role = TextRole.Heading(level = 3)
)
},
footer = {
PantrixButton(
text = "Refresh",
onClick = { loading = true },
variant = ButtonVariant.Text,
tagKey = "reload"
)
PantrixButton(text = "Track", onClick = {}, tagKey = "track")
}
) {
PantrixText(text = "Two items, delivery on Thursday")
}
}
if (loading) {
PantrixCardSkeleton(
modifier = Modifier.fillMaxWidth(),
variant = CardVariant.Filled,
colors = campaign,
skeletonColors = SkeletonColors(base = own.onContainer.copy(alpha = 0.3f), highlight = own.container),
lines = 1
)
} else {
PantrixCard(
modifier = Modifier.fillMaxWidth(),
variant = CardVariant.Filled,
colors = campaign,
tagKey = "campaign"
) {
PantrixText(text = "Campaign", style = PantrixTheme.typography.title)
PantrixText(text = "Its fill and content colour were given by the caller")
}
}
API
modifier
A container for one subject: a header, a body and actions on a surface of their own, with media edge to edge above them.
| Parameter | Type | Default | What it is |
|---|---|---|---|
modifier | Modifier | Modifier | the card's modifier. |
variant | CardVariant | CardVariant.Elevated | how the card stands off what it sits on: its fill, its shadow and its edge. |
onClick | (() -> Unit)? | null | when given, the whole card becomes one button, and a click is reported as a Card event that carries the variant. |
enabled | Boolean | true | whether a card with onClick can be pressed. A card without one ignores it. |
style | CardStyle? | null | this call's own style: over every enclosing one and under the density. Only the fields it sets change, on every platform. |
shape | Shape | PantrixTheme.style(CardStyle, style).shape.toShape() | the card's corners: by default the resolved style's. Given here, it wins over style's. |
colors | CardColors? | null | this card's own colours: the ones it sets, with the variant's for the rest. |
contentPadding | PaddingValues | PaddingValues(PantrixTheme.spacing.lg) | the room around the header, the body and the footer. The media takes none. |
header | (@Composable () -> Unit)? | null | drawn above the body: usually the card's title. |
media | (@Composable () -> Unit)? | null | drawn edge to edge above the padded content, because a picture inset by the card's padding reads as a mistake. |
footer | (@Composable RowScope.() -> Unit)? | null | the card's actions in a row at the end edge, the main one last: closest to the thumb on a phone, and where the eye finishes on a wide card. In a card taller than what it holds, they stand at its foot. |
contentDescription | String? | null | what a screen reader says for the card. For a card with onClick, name what it opens. |
tagKey | String? | null | the card's automation key: the card is Card_<tagKey>. |
interactionSource | MutableInteractionSource? | null | the card's interactions, hoisted. The card keeps a source of its own when this is null. |
content | @Composable ColumnScope.() -> Unit | the card's body, in a column whose children stand spacing.sm apart. A child given a weight takes the room a card's height leaves. |
CardStyle
PantrixCard's geometry: its corners, and the shadow of its elevated variant.
| Parameter | Type | Default | What it is |
|---|---|---|---|
shape | ShapeSpec | ShapeSpec.Unspecified | The card's corners: medium on Android, large on iOS, small on desktop and the web. |
elevation | Dp | Dp.Unspecified | The shadow under CardVariant.Elevated: 2dp on Android, none on iOS, 1dp on desktop and the web. The other variants draw none. |
CardColors
What PantrixCard is painted with, for a card that does not take its variant's colours: a colour that is set is used, and one that is left out stays the variant's.
| Parameter | Type | Default | What it is |
|---|---|---|---|
container | Color | Color.Unspecified | The card's fill. The theme's surface, or the neutral role's container in a CardVariant.Filled card. |
content | Color | Color.Unspecified | What the card holds is drawn in, unless a child sets a colour of its own: the colour that reads on container. None on the theme's surface, where each child draws in its own default, a text in textPrimary; the neutral role's onContainer in a filled card. |
border | Color | Color.Unspecified | The hairline around the card. The theme's divider on a CardVariant.Outlined card, and where that card is given a fill that is no ground of the theme's, on which that line would not read, a share of what is written there: the edge a subtle button has. The other variants have none unless one is given here. |