Pantrix UI

Menu

A list of actions for one element, in a panel beside it: what a "more" button opens, or the actions of a row.

import com.pantrixui.components.overlay.menu.PantrixMenu

Examples

Row actions

At its plainest: a file's "more" button opens the menu. Whether the menu is open is the screen's state (visible). The menu does not close itself: when it wants to close it calls onDismissRequest, and it is this code that sets visible to false. The row that deletes is under a PantrixDivider and, with destructive, in the theme's danger colour.

Pick a row: the menu is first asked to close, then the row's onClick is called; the card's "Last event" line says what happened last. A press outside, back and Escape only close it. When the menu is opened by the keyboard, the focus is on its first row; once it has closed, the focus is back on the button.

The rows of a menu given a tagKey are named inside it for automation: the test tag of the "Delete" row here is Menu_row_actions.MenuItem_delete, and it cannot be mixed up with another menu's "Delete" row.

var open by rememberSaveable { mutableStateOf(false) }
val report = LocalExampleEvents.current
Row(verticalAlignment = Alignment.CenterVertically) {
    PantrixText(text = "Report.pdf")
    PantrixMenu(
        visible = open,
        onDismissRequest = {
            open = false
            report("File actions closed")
        },
        anchor = {
            PantrixIconButton(
                imageVector = PantrixIcons.MoreHorizontal,
                contentDescription = "Actions for Report.pdf",
                onClick = { open = true },
                tagKey = "open_row_actions"
            )
        },
        tagKey = "row_actions"
    ) {
        PantrixMenuItem(
            text = "Rename",
            onClick = { report("Rename chosen") },
            tagKey = "rename"
        )
        PantrixMenuItem(text = "Duplicate", onClick = { report("Duplicate chosen") }, tagKey = "duplicate")
        PantrixDivider()
        PantrixMenuItem(text = "Delete", onClick = { report("Delete chosen") }, destructive = true, tagKey = "delete")
    }
}

Icon and shortcut

icon draws an icon before the label: in the label's colour and at the style's icon size. The icon is a picture of what the label says; a screen reader reads the label alone. shortcut shows the keys that do the same thing at the end of the row. A shortcut is only shown and read out: binding the keys is the app's job.

enabled = false draws the row faded: "Move to folder" takes no press, and the keyboard skips it. destructive draws both the label and the icon in the destructive colour.

var open by rememberSaveable { mutableStateOf(false) }
val report = LocalExampleEvents.current
PantrixMenu(
    visible = open,
    onDismissRequest = {
        open = false
        report("File menu closed")
    },
    anchor = {
        PantrixButton(
            text = "File",
            onClick = { open = true },
            variant = ButtonVariant.Outline,
            tagKey = "open_file"
        )
    },
    tagKey = "file"
) {
    PantrixMenuItem(
        text = "Search",
        onClick = { report("Search chosen") },
        icon = PantrixIcons.Search,
        shortcut = "Ctrl+F",
        tagKey = "search"
    )
    PantrixMenuItem(
        text = "Refresh",
        onClick = { report("Refresh chosen") },
        icon = PantrixIcons.Refresh,
        shortcut = "F5",
        tagKey = "refresh"
    )
    PantrixMenuItem(
        text = "Move to folder",
        onClick = { report("Move to folder chosen") },
        icon = PantrixIcons.Folder,
        enabled = false,
        tagKey = "move"
    )
    PantrixDivider()
    PantrixMenuItem(
        text = "Delete",
        onClick = { report("Delete chosen") },
        icon = PantrixIcons.Close,
        shortcut = "Del",
        destructive = true,
        tagKey = "delete"
    )
}

Chosen row

A menu whose rows each choose one thing: the list's order. The one in effect is selected: a check stands at its end, in the label's colour, and a screen reader is told it is selected. Choose another order: the check moves to its row, and the button writes the new order.

var open by rememberSaveable { mutableStateOf(false) }
var order by rememberSaveable { mutableStateOf("newest") }
val orders = listOf("newest" to "Newest first", "oldest" to "Oldest first", "name" to "By name")
PantrixMenu(
    visible = open,
    onDismissRequest = { open = false },
    anchor = {
        PantrixButton(
            text = "Sort: ${orders.first { it.first == order }.second}",
            onClick = { open = true },
            variant = ButtonVariant.Outline,
            tagKey = "open_order"
        )
    },
    tagKey = "order"
) {
    orders.forEach { (key, label) ->
        PantrixMenuItem(text = label, onClick = { order = key }, tagKey = key, selected = order == key)
    }
}

Second line

supportingText puts a second, smaller line under the label: what the choice leads to. Here, under each language, the site's address in it, set in the code type (supportingTextStyle); its colour is the row's. The row grows to hold it; a screen reader hears the second line after the label. The second line runs to two lines at the most, and the rest is cut.

var open by rememberSaveable { mutableStateOf(false) }
var chosen by rememberSaveable { mutableIntStateOf(0) }
PantrixMenu(
    visible = open,
    onDismissRequest = { open = false },
    anchor = {
        PantrixButton(
            text = "Language: ${LANGUAGES[chosen].first}",
            onClick = { open = true },
            variant = ButtonVariant.Outline,
            tagKey = "open_language"
        )
    },
    tagKey = "language"
) {
    LANGUAGES.forEachIndexed { index, (name, address) ->
        PantrixMenuItem(
            text = name,
            onClick = { chosen = index },
            selected = index == chosen,
            tagKey = "language_$index",
            supportingText = address,
            supportingTextStyle = PantrixTheme.typography.code
        )
    }
}

private val LANGUAGES = listOf(
    "Türkçe" to "pantrixui.com/tr",
    "English" to "pantrixui.com/en",
    "العربية" to "pantrixui.com/ar"
)

Side and alignment

side says which side of the anchor the panel opens on, and alignment how it lines up along that side. Unless told otherwise the menu is below its anchor and hangs from the anchor's start edge: PopoverSide.Bottom and PopoverAlignment.Start. The two enums are the popover's; the menu shares them.

Start and End follow the reading direction: in a left-to-right layout Start is the left. While the panel is beside the anchor (Start or End) the alignment is vertical: Start makes the top edges meet, and End the bottom edges.

Where the side asked for has no room the menu takes the opposite side: pick Top and open the menu while the button is near the top edge of the window. With room on neither side the menu keeps the side asked for and slides inside the window, over its anchor.

presentation = OverlayPresentation.Popover: the menu is a panel in a narrow window too, not a sheet; its side and alignment show on a phone as well.

var open by rememberSaveable { mutableStateOf(false) }
var side by rememberSaveable { mutableStateOf(PopoverSide.Bottom) }
var alignment by rememberSaveable { mutableStateOf(PopoverAlignment.Start) }
val report = LocalExampleEvents.current
PantrixSegmentButton(options = SIDES, selected = side, onSelect = { side = it }, tagKey = "menu_side")
PantrixSegmentButton(
    options = ALIGNMENTS,
    selected = alignment,
    onSelect = { alignment = it },
    tagKey = "menu_alignment"
)
Box(modifier = Modifier.fillMaxWidth(), contentAlignment = Alignment.Center) {
    PantrixMenu(
        visible = open,
        onDismissRequest = {
            open = false
            report("Menu closed")
        },
        anchor = {
            PantrixButton(
                text = "Open",
                onClick = { open = true },
                variant = ButtonVariant.Outline,
                tagKey = "open_placed_menu"
            )
        },
        side = side,
        alignment = alignment,
        tagKey = "placed_menu",
        presentation = OverlayPresentation.Popover
    ) {
        PantrixMenuItem(text = "side = ${side.name}", onClick = { report("Side row chosen") }, tagKey = "side")
        PantrixMenuItem(
            text = "alignment = ${alignment.name}",
            onClick = { report("Alignment row chosen") },
            tagKey = "alignment"
        )
    }
}

private val SIDES = listOf(
    SegmentOption(value = PopoverSide.Top, label = "Top", tagKey = "top"),
    SegmentOption(value = PopoverSide.Bottom, label = "Bottom", tagKey = "bottom"),
    SegmentOption(value = PopoverSide.Start, label = "Start", tagKey = "start"),
    SegmentOption(value = PopoverSide.End, label = "End", tagKey = "end")
)

private val ALIGNMENTS = listOf(
    SegmentOption(value = PopoverAlignment.Start, label = "Start", tagKey = "start"),
    SegmentOption(value = PopoverAlignment.Center, label = "Centre", tagKey = "center"),
    SegmentOption(value = PopoverAlignment.End, label = "End", tagKey = "end")
)

At the edge

A "more" button near the end edge of the window. A menu hangs from the start edge of its anchor; with the button near the edge, the panel would run out of the window. It does not: it shifts to stay the style's margin in from the edge. In a narrow window the menu would be a sheet; presentation = OverlayPresentation.Popover keeps it a panel in every window.

Try it in a narrow window: the one to narrow is the app's own window. A menu picks its place by the real window, not by the window a card's window selector gives its example; that is why this card has no such selector. In a window that is wide enough there is room for the panel beside the card; there the panel does not shift, and hangs from the start edge of the button.

var open by rememberSaveable { mutableStateOf(false) }
val report = LocalExampleEvents.current
Box(modifier = Modifier.fillMaxWidth(), contentAlignment = Alignment.CenterEnd) {
    PantrixMenu(
        visible = open,
        onDismissRequest = {
            open = false
            report("Menu at the edge closed")
        },
        anchor = {
            PantrixIconButton(
                imageVector = PantrixIcons.MoreHorizontal,
                contentDescription = "Folder actions",
                onClick = { open = true },
                tagKey = "open_edge"
            )
        },
        tagKey = "edge",
        presentation = OverlayPresentation.Popover
    ) {
        PantrixMenuItem(
            text = "Share folder",
            onClick = { report("Share folder chosen") },
            icon = PantrixIcons.Person,
            tagKey = "share"
        )
        PantrixMenuItem(
            text = "Add to favourites",
            onClick = { report("Add to favourites chosen") },
            icon = PantrixIcons.Star,
            tagKey = "favourite"
        )
    }
}

A long list

Thirty rows do not fit the style's tallest height (MenuStyle.maxHeight): the panel stays at that height and the rows scroll inside it. On the keyboard End goes to the last row and Home to the first; a row that takes the focus comes into view.

presentation = OverlayPresentation.Popover: a panel in a narrow window too. In a sheet the rows scroll inside the sheet, which is as tall as the window at the most.

var open by rememberSaveable { mutableStateOf(false) }
val report = LocalExampleEvents.current
PantrixMenu(
    visible = open,
    onDismissRequest = {
        open = false
        report("Version menu closed")
    },
    anchor = {
        PantrixButton(
            text = "Choose a version",
            onClick = { open = true },
            variant = ButtonVariant.Outline,
            tagKey = "open_versions"
        )
    },
    tagKey = "versions",
    presentation = OverlayPresentation.Popover
) {
    (30 downTo 1).forEach { number ->
        PantrixMenuItem(
            text = "Version $number.0",
            onClick = { report("Version $number.0 chosen") },
            tagKey = "version_$number"
        )
    }
}

Width

The panel is as wide as its longest row, between the style's narrowest and widest widths (MenuStyle.minWidth and maxWidth). The labels of the "Short" menu are shorter than the narrowest width: the panel stays at that width. The first label of the "Long" menu does not fit the widest panel: it is cut at one line.

presentation = OverlayPresentation.Popover: a sheet is as wide as the window; both menus are panels so that the panel's width shows in a narrow window too.

var narrow by rememberSaveable { mutableStateOf(false) }
var wide by rememberSaveable { mutableStateOf(false) }
val report = LocalExampleEvents.current
Row(horizontalArrangement = Arrangement.spacedBy(PantrixTheme.spacing.sm)) {
    PantrixMenu(
        visible = narrow,
        onDismissRequest = {
            narrow = false
            report("Short menu closed")
        },
        anchor = {
            PantrixButton(
                text = "Short",
                onClick = { narrow = true },
                variant = ButtonVariant.Outline,
                tagKey = "open_narrow"
            )
        },
        tagKey = "narrow",
        presentation = OverlayPresentation.Popover
    ) {
        PantrixMenuItem(text = "Open", onClick = { report("Open chosen") }, tagKey = "open")
        PantrixMenuItem(text = "Delete", onClick = { report("Delete chosen") }, tagKey = "delete")
    }
    PantrixMenu(
        visible = wide,
        onDismissRequest = {
            wide = false
            report("Long menu closed")
        },
        anchor = {
            PantrixButton(
                text = "Long",
                onClick = { wide = true },
                variant = ButtonVariant.Outline,
                tagKey = "open_wide"
            )
        },
        tagKey = "wide",
        presentation = OverlayPresentation.Popover
    ) {
        PantrixMenuItem(
            text = "Open Report.pdf for editing together with everyone in the shared folder",
            onClick = { report("Long row chosen") },
            tagKey = "long"
        )
        PantrixMenuItem(text = "Copy", onClick = { report("Copy chosen") }, tagKey = "copy")
    }
}

Its own style

style changes this menu's geometry alone: the fields of MenuStyle that are set change, and the rest comes from the theme's and the platform's style. Here the panel's corners are the theme's large corner (ShapeRole.Large), the panel is at least 240dp wide, its rows are 56dp tall and their icons 28dp.

The style given to a menu draws its rows too; PantrixMenuItem takes no style of its own. The width and the height that are given scale with the theme's density; the corners and the icon's size do not.

presentation = OverlayPresentation.Popover: a sheet's corners and width are the sheet's, and there the panel's style draws only the rows; the menu is a panel so that the style shows in a narrow window too.

var open by rememberSaveable { mutableStateOf(false) }
PantrixMenu(
    visible = open,
    onDismissRequest = { open = false },
    anchor = {
        PantrixButton(
            text = "Account",
            onClick = { open = true },
            variant = ButtonVariant.Outline,
            tagKey = "open_account"
        )
    },
    style = MenuStyle(shape = ShapeRole.Large, minWidth = 240.dp, itemHeight = 56.dp, iconSize = 28.dp),
    tagKey = "account",
    presentation = OverlayPresentation.Popover
) {
    PantrixMenuItem(text = "Profile", onClick = {}, icon = PantrixIcons.Person, tagKey = "profile")
    PantrixMenuItem(text = "Inbox", onClick = {}, icon = PantrixIcons.Inbox, tagKey = "inbox")
    PantrixMenuItem(text = "Favourites", onClick = {}, icon = PantrixIcons.Star, tagKey = "favourites")
}

Its own colours

colors gives the menu the colours that are set in place of the theme's: here the app's own purple, which is in none of the theme's roles. Only the fields of MenuColors that are set change, and the rest stay the theme's: the row that deletes is still in the theme's danger colour.

The theme's secondary and disabled text colours are for the theme's own grounds alone. On this fill the shortcut is drawn in the row's colour, and a row that cannot be chosen in a share of that colour. The line between the groups is given no colour: on the fill it too is a share of the row's colour.

On a fill that no role has, content is given too: the theme does not know what reads on that fill. Where the fill is the colour of one of the theme's roles, such as PantrixTheme.colors.primary.container, the rows take the colour the theme pairs with it by themselves.

var open by rememberSaveable { mutableStateOf(false) }
val report = LocalExampleEvents.current
val own = ownColors()
PantrixMenu(
    visible = open,
    onDismissRequest = {
        open = false
        report("Pro menu closed")
    },
    anchor = {
        PantrixButton(
            text = "Pro",
            onClick = { open = true },
            variant = ButtonVariant.Outline,
            tagKey = "open_pro_menu"
        )
    },
    colors = MenuColors(container = own.container, content = own.onContainer, border = own.color),
    tagKey = "pro_menu"
) {
    PantrixMenuItem(
        text = "Upgrade plan",
        onClick = { report("Upgrade plan chosen") },
        icon = PantrixIcons.Star,
        shortcut = "Ctrl+U",
        tagKey = "upgrade"
    )
    PantrixMenuItem(
        text = "Invoices",
        onClick = { report("Invoices chosen") },
        icon = PantrixIcons.Folder,
        enabled = false,
        tagKey = "invoices"
    )
    PantrixDivider()
    PantrixMenuItem(
        text = "End subscription",
        onClick = { report("End subscription chosen") },
        icon = PantrixIcons.Close,
        destructive = true,
        tagKey = "cancel_plan"
    )
}

In a part in another theme

A part in the opposite colours of the page, and a menu opened from inside it. The part is a second theme (PantrixTheme(darkTheme = …)): dark on a light page, light on a dark one. The menu is called inside that theme, so it is in that theme's colours: though its panel is composed in a layer of its own at the root of the window, it carries the theme of the place it was called from.

var open by rememberSaveable { mutableStateOf(false) }
val report = LocalExampleEvents.current
val pageIsDark = PantrixTheme.colors.background.luminance() < 0.5f
PantrixTheme(darkTheme = !pageIsDark) {
    PantrixSurface(modifier = Modifier.fillMaxWidth()) {
        Column(modifier = Modifier.padding(PantrixTheme.spacing.md)) {
            PantrixMenu(
                visible = open,
                onDismissRequest = {
                    open = false
                    report("The part's menu closed")
                },
                anchor = {
                    PantrixButton(
                        text = "Open from this part",
                        onClick = { open = true },
                        tagKey = "open_menu_in_part"
                    )
                },
                tagKey = "menu_in_part"
            ) {
                PantrixMenuItem(
                    text = "Refresh this part",
                    onClick = { report("Refresh this part chosen") },
                    icon = PantrixIcons.Refresh,
                    tagKey = "refresh_part"
                )
                PantrixMenuItem(
                    text = "Remove this part",
                    onClick = { report("Remove this part chosen") },
                    icon = PantrixIcons.Close,
                    destructive = true,
                    tagKey = "remove_part"
                )
            }
        }
    }
}

In a dialog

A menu opened from inside a dialog. The menu is above the dialog, in a layer of its own: Escape and back close the menu first, and the dialog the next time. A press outside the menu but inside the dialog also closes the menu alone.

var dialog by rememberSaveable { mutableStateOf(false) }
var menu by rememberSaveable { mutableStateOf(false) }
val report = LocalExampleEvents.current
PantrixButton(
    text = "File details…",
    onClick = { dialog = true },
    variant = ButtonVariant.Outline,
    tagKey = "open_file_dialog"
)
PantrixDialog(
    visible = dialog,
    onDismissRequest = {
        dialog = false
        report("File details closed")
    },
    title = "Report.pdf",
    message = "2,4 MB. Modified yesterday.",
    actions = {
        PantrixButton(
            text = "Close",
            onClick = {
                dialog = false
                report("File details closed")
            },
            variant = ButtonVariant.Text,
            tagKey = "close_file_dialog"
        )
    },
    tagKey = "file_dialog"
) {
    PantrixMenu(
        visible = menu,
        onDismissRequest = {
            menu = false
            report("Share menu closed")
        },
        anchor = {
            PantrixButton(
                text = "Share",
                onClick = { menu = true },
                variant = ButtonVariant.Outline,
                tagKey = "open_share"
            )
        },
        tagKey = "share"
    ) {
        PantrixMenuItem(
            text = "Copy link",
            onClick = { report("Copy link chosen") },
            tagKey = "copy_link"
        )
        PantrixMenuItem(
            text = "Send by email",
            onClick = { report("Send by email chosen") },
            tagKey = "send_mail"
        )
    }
}

A sheet in a narrow window

The same menu by the window's width: pick "Narrow" in the card's window selector and the menu opens as a sheet from the bottom; in "Wide" it is a panel beside the button. In the sheet the rows are as wide as the sheet, a drag down or a press on the scrim closes it, and "Delete" stays red. On Android and iOS the shortcuts are not written in the sheet: there are no keys to press.

The menu is chosen as it opens: while it is open the sheet stays a sheet and the panel a panel, even if the window changes; the next opening goes by the new window. This is the default; presentation = OverlayPresentation.Adaptive is written only so that it shows.

var open by rememberSaveable { mutableStateOf(false) }
val report = LocalExampleEvents.current
// The card's window is as tall as the example: the example is kept tall so that a sheet opened in a narrow window has room.
Column(Modifier.fillMaxWidth().height(280.dp)) {
    Row(verticalAlignment = Alignment.CenterVertically) {
        PantrixText(text = "Slides.key")
        PantrixMenu(
            visible = open,
            onDismissRequest = { open = false },
            anchor = {
                PantrixIconButton(
                    imageVector = PantrixIcons.MoreHorizontal,
                    contentDescription = "Actions for Slides.key",
                    onClick = { open = true },
                    tagKey = "open_adaptive_menu"
                )
            },
            tagKey = "adaptive_menu",
            presentation = OverlayPresentation.Adaptive
        ) {
            PantrixMenuItem(
                text = "Share",
                onClick = { report("Share chosen") },
                icon = PantrixIcons.Upload,
                shortcut = "Ctrl+Shift+S",
                tagKey = "share"
            )
            PantrixMenuItem(
                text = "Copy",
                onClick = { report("Copy chosen") },
                icon = PantrixIcons.Copy,
                shortcut = "Ctrl+C",
                tagKey = "copy"
            )
            PantrixDivider()
            PantrixMenuItem(
                text = "Delete",
                onClick = { report("Delete chosen") },
                destructive = true,
                tagKey = "delete"
            )
        }
    }
}

API

visible

A list of actions for one element, in a panel beside it: what a "more" button opens, or the actions of a row.

ParameterTypeDefaultWhat it is
visibleBooleanwhether the menu is shown. It enters and leaves with the theme's motion.
onDismissRequest() -> Unitcalled for a press outside, Escape, back and a row that is chosen. The menu stays until visible is false.
anchor@Composable () -> Unitthe control the menu belongs to, laid out where this is called and as that place asks: usually the button that opens it.
modifierModifierModifierapplied to the panel, inside its layer. Not to the anchor.
sidePopoverSidePopoverSide.Bottomthe side of the anchor the panel prefers. PopoverSide.Start and PopoverSide.End follow the layout direction.
alignmentPopoverAlignmentPopoverAlignment.Starthow the panel lines up with the anchor along that side: from its start edge unless told otherwise, as a menu hangs from what opened it.
styleMenuStyle?nullthis call's own style, which its rows are drawn with too: over every enclosing one and under the density. Only the fields it sets change, on every platform.
colorsMenuColors?nullthis call's own colours, its rows' among them. Only the ones it sets change; the rest stay the theme's.
tagKeyString?nulla locale-independent key for automation: the panel's tag is Menu_<tagKey>, and its rows are named inside it, Menu_<tagKey>.MenuItem_<row's key>. A menu shown as a sheet has the same tags, on the sheet.
presentationOverlayPresentationOverlayPresentation.Adaptivehow the menu is shown: a sheet in a narrow window and a panel beside its anchor in a wider one, by default; always the panel, or always the sheet. Chosen as the menu opens, it stays until it has closed.
content@Composable ColumnScope.() -> Unitthe rows: PantrixMenuItems, with a PantrixDivider between groups of them. Given no colour, that line is told by the rows' colour on a fill, as the panel's hairline is.

MenuStyle

PantrixMenu's geometry: the panel it draws, how far that sits from its anchor and from the edges of the window, and the rows in it.

ParameterTypeDefaultWhat it is
shapeShapeSpecShapeSpec.UnspecifiedThe panel's corners: medium on Android and iOS, small on desktop and the web.
minWidthDpDp.UnspecifiedThe narrowest the panel gets, however short its labels.
maxWidthDpDp.UnspecifiedThe widest the panel gets: a longer label is cut short.
maxHeightDpDp.UnspecifiedThe tallest the panel gets: more rows than fit scroll.
elevationDpDp.UnspecifiedHow high the panel floats over the app, which is how large its shadow is.
gapDpDp.UnspecifiedBetween the anchor and the panel.
marginDpDp.UnspecifiedKept clear between the panel and the edges of the window, and of what covers the window there: a system bar, a cutout, the keyboard.
verticalPaddingDpDp.UnspecifiedAbove the first row and below the last.
itemHeightDpDp.UnspecifiedThe least a row is tall, where the platform's touch target is not taller.
itemHorizontalPaddingDpDp.UnspecifiedInside a row, before its icon and after its shortcut.
iconSizeDpDp.UnspecifiedA row's icon.
iconGapDpDp.UnspecifiedBetween a row's icon and its label, and between its label and its shortcut.

MenuColors

What a PantrixMenu is painted with, for a call that does not take the theme's colours: a colour that is set is used, and one that is left out stays the theme's.

ParameterTypeDefaultWhat it is
containerColorColor.UnspecifiedThe panel's fill. The theme's surface.
contentColorColor.UnspecifiedA row's label and icon. The theme's textPrimary. A row's shortcut is the theme's textSecondary on the theme's own fill, and this colour on a fill of the caller's.
destructiveColorColor.UnspecifiedThe label and icon of a row that deletes or cannot be undone. The theme's danger colour.
borderColorColor.UnspecifiedThe hairline around the panel, which tells it from a surface of its own colour under it, as in the dark theme, where a shadow shows little. The theme's divider on the theme's own grounds. On a fill, where that line would not read, a share of content: the edge a subtle button has. A transparent colour draws none.

Accessibility