Pantrix UI

Top Bar

The bar at the top of a screen: the screen's title, with a navigation icon before it and actions after it.

import com.pantrixui.components.container.topbar.PantrixTopBar

Examples

Title only

The least there is to write: title alone. With no navigation icon, a title at the start begins at the bar's own inset; on iOS the title is in the middle.

PantrixSurface(modifier = Modifier.fillMaxWidth(), border = PantrixTheme.colors.divider) {
    PantrixTopBar(title = "Inbox", tagKey = "plain")
}

Back button, actions and content scrolling under the bar

A conversation screen: a back button, two actions and a list that scrolls under the bar. navigationIcon puts a PantrixBackButton before the title; the subtitle says how many times the back button has been pressed. actions puts two icon buttons after the title: drawn as they are, in the brand's colour.

Scroll the list: as soon as it leaves its top, a line appears along the bar's bottom edge (scrolled), and it stays for as long as the list can scroll back. The line is inside the bar's height; nothing moves as it comes and goes.

val list = rememberLazyListState()
var back by rememberSaveable { mutableIntStateOf(0) }
PantrixSurface(modifier = Modifier.fillMaxWidth(), border = PantrixTheme.colors.divider) {
    Column(modifier = Modifier.height(220.dp)) {
        PantrixTopBar(
            title = "Conversation",
            subtitle = if (back == 0) null else "Back presses: $back",
            navigationIcon = { PantrixBackButton(onClick = { back++ }, tagKey = "back") },
            actions = {
                PantrixIconButton(
                    imageVector = PantrixIcons.Search,
                    contentDescription = "Search",
                    onClick = {},
                    tagKey = "search"
                )
                PantrixIconButton(
                    imageVector = PantrixIcons.MoreHorizontal,
                    contentDescription = "More",
                    onClick = {},
                    tagKey = "more"
                )
            },
            scrolled = list.canScrollBackward,
            tagKey = "conversation"
        )
        LazyColumn(
            // The list is as wide as the bar: left narrower, a finger on the space beside it would scroll
            // the page, not the list.
            modifier = Modifier.fillMaxWidth(),
            state = list,
            contentPadding = PaddingValues(PantrixTheme.spacing.lg),
            verticalArrangement = Arrangement.spacedBy(PantrixTheme.spacing.md)
        ) {
            items(12) { index -> PantrixText(text = "Message ${index + 1}") }
        }
    }
}

Subtitle

subtitle puts a line of smaller text under the title: a count, a state. The navigation icon here is not a back button but an icon button that opens a menu.

PantrixSurface(modifier = Modifier.fillMaxWidth(), border = PantrixTheme.colors.divider) {
    PantrixTopBar(
        title = "Projects",
        subtitle = "12 open",
        navigationIcon = {
            PantrixIconButton(
                imageVector = PantrixIcons.Menu,
                contentDescription = "Open menu",
                onClick = {},
                tagKey = "menu"
            )
        },
        tagKey = "subtitle"
    )
}

Title and subtitle in text styles of their own

The title and the subtitle can each be given a text style: titleStyle and subtitleStyle. Unless given, they are the theme's title and footnote styles; here callout and caption are given. Where the style has no colour of its own, the text stays in the bar's colour. A larger style makes the bar as tall as its line needs.

PantrixSurface(modifier = Modifier.fillMaxWidth(), border = PantrixTheme.colors.divider) {
    PantrixTopBar(
        title = "Projects",
        subtitle = "12 open",
        titleStyle = PantrixTheme.typography.callout,
        subtitleStyle = PantrixTheme.typography.caption,
        navigationIcon = {
            PantrixIconButton(
                imageVector = PantrixIcons.Menu,
                contentDescription = "Open menu",
                onClick = {},
                tagKey = "type_menu"
            )
        },
        tagKey = "own_type"
    )
}

A title that does not fit is cut short

The title does not run under the navigation icon or the actions: a title too long for the room left between them is cut short. The bar here is at most 360dp wide; the title does not fit between the back button and the star, and its end is cut off.

PantrixSurface(modifier = Modifier.widthIn(max = 360.dp), border = PantrixTheme.colors.divider) {
    PantrixTopBar(
        title = "Third-quarter reports, as they were sent to the board of directors",
        navigationIcon = { PantrixBackButton(onClick = {}, tagKey = "long_back") },
        actions = {
            PantrixIconButton(
                imageVector = PantrixIcons.Star,
                contentDescription = "Add to favourites",
                onClick = {},
                tagKey = "favourite"
            )
        },
        tagKey = "long"
    )
}

Where the title stands: at the start and in the middle

Where the title stands follows the platform: at the start on Android, desktop and the web, in the middle of the bar on iOS. Here the same bar is drawn twice, and the place is given with style: TopBarStyle(titleAlignment = ...). Both show, whichever platform you are on.

A title in the middle is in the middle of the bar whatever stands beside it: the back button does not push it aside.

PantrixSurface(modifier = Modifier.fillMaxWidth(), border = PantrixTheme.colors.divider) {
    PantrixTopBar(
        title = "Settings",
        navigationIcon = { PantrixBackButton(onClick = {}, tagKey = "start_back") },
        style = TopBarStyle(titleAlignment = TopBarTitleAlignment.Start),
        tagKey = "start"
    )
}
PantrixSurface(modifier = Modifier.fillMaxWidth(), border = PantrixTheme.colors.divider) {
    PantrixTopBar(
        title = "Settings",
        navigationIcon = { PantrixBackButton(onClick = {}, tagKey = "center_back") },
        style = TopBarStyle(titleAlignment = TopBarTitleAlignment.Center),
        tagKey = "center"
    )
}

The back button's icon

PantrixBackButton takes the icon it draws from the bar's style: TopBarStyle.backIcon. The platform's own icon is an arrow on Android, desktop and the web, a chevron on iOS. Here each of the two bars is given an icon with style; a back button in a bar's navigationIcon uses the style that bar was given.

Both turn round in a right-to-left layout: flip the reading direction from the "Typeface and direction" menu in the app's top bar and look.

PantrixSurface(modifier = Modifier.fillMaxWidth(), border = PantrixTheme.colors.divider) {
    PantrixTopBar(
        title = "Arrow",
        navigationIcon = { PantrixBackButton(onClick = {}, tagKey = "arrow_back") },
        style = TopBarStyle(backIcon = BackButtonIcon.Arrow),
        tagKey = "arrow"
    )
}
PantrixSurface(modifier = Modifier.fillMaxWidth(), border = PantrixTheme.colors.divider) {
    PantrixTopBar(
        title = "Chevron",
        navigationIcon = { PantrixBackButton(onClick = {}, tagKey = "chevron_back") },
        style = TopBarStyle(backIcon = BackButtonIcon.Chevron),
        tagKey = "chevron"
    )
}

With nowhere to go back to

On the first screen of a flow there is nowhere to go back to: the enabled of PantrixBackButton is false and the button cannot be pressed. Move on with the button at the end: the back button is enabled from the second step on.

var step by rememberSaveable { mutableIntStateOf(0) }
PantrixSurface(modifier = Modifier.fillMaxWidth(), border = PantrixTheme.colors.divider) {
    PantrixTopBar(
        title = "Step ${step + 1} / 3",
        navigationIcon = { PantrixBackButton(onClick = { step -= 1 }, enabled = step > 0, tagKey = "step_back") },
        actions = {
            PantrixIconButton(
                imageVector = PantrixIcons.ArrowForward,
                contentDescription = "Next step",
                onClick = { step += 1 },
                enabled = step < 2,
                tagKey = "step_next"
            )
        },
        tagKey = "steps"
    )
}

A large title that collapses as the list scrolls

A collapsing title: the state is the app's and is kept with rememberPantrixTopBarState(); the list hands the bar its scroll with Modifier.topBarScroll(state). While the bar is open the title is written large, in the theme's headline style, in a band under the row. The large title stands at the bar's start on every platform; the title in the row stands where the platform has it, in the middle on iOS.

Scroll the list up: first the band closes and the title comes into the row, and only then does the list scroll. The bar opens again only once the list is back at its top. Left between its two heights by a finger, it goes on to the nearer; after a mouse wheel it goes on the way the wheel was last turned. A screen reader reads the title in the row: that one is there all the time, and the title is not read twice.

The bar and the list are inside a scaffold, as they would be in an app: the list fills the whole scaffold and is given the bar's height as its top inset. As the bar closes and opens the list itself stays where it is; only its inset changes. The button below takes the list back to its top and opens the bar by setting collapsedFraction to zero.

val topBar = rememberPantrixTopBarState()
val list = rememberLazyListState()
val scope = rememberCoroutineScope()
val spacing = PantrixTheme.spacing
PantrixSurface(modifier = Modifier.fillMaxWidth(), border = PantrixTheme.colors.divider) {
    PantrixScaffold(
        modifier = Modifier.height(320.dp),
        topBar = { insets ->
            PantrixTopBar(
                title = "Inbox",
                subtitle = "3 unread",
                navigationIcon = { PantrixBackButton(onClick = {}, tagKey = "collapsing_back") },
                actions = {
                    PantrixIconButton(
                        imageVector = PantrixIcons.Search,
                        contentDescription = "Search",
                        onClick = {},
                        tagKey = "collapsing_search"
                    )
                },
                scrolled = list.canScrollBackward,
                state = topBar,
                insets = insets,
                tagKey = "collapsing"
            )
        },
        containerColor = PantrixTheme.colors.surface,
        // A box inside the page: the system bars are in the page's scaffold, and this one has no insets.
        windowInsets = WindowInsets(0, 0, 0, 0)
    ) { padding ->
        LazyColumn(
            modifier = Modifier.fillMaxSize().topBarScroll(topBar),
            state = list,
            contentPadding = PaddingValues(
                start = spacing.lg,
                top = padding.calculateTopPadding() + spacing.lg,
                end = spacing.lg,
                bottom = spacing.lg
            ),
            verticalArrangement = Arrangement.spacedBy(spacing.md)
        ) {
            items(24) { index -> PantrixText(text = "Conversation ${index + 1}") }
        }
    }
}
PantrixButton(
    text = "Back to top and open the bar",
    onClick = {
        scope.launch {
            list.scrollToItem(0)
            topBar.collapsedFraction = 0f
        }
    },
    variant = ButtonVariant.Outline,
    size = ButtonSize.Small,
    tagKey = "collapsing_to_top"
)

The insets the bar keeps clear

insets is the room the bar keeps clear at its edges: room for the status bar and a cutout. A PantrixScaffold hands it to its top bar itself; here a top inset of 28dp is given by hand. The bar's fill runs behind the inset, and the title and the back button stay below it: the bar is as tall as its style's height plus the inset.

PantrixSurface(modifier = Modifier.fillMaxWidth(), border = PantrixTheme.colors.divider) {
    PantrixTopBar(
        title = "Settings",
        navigationIcon = { PantrixBackButton(onClick = {}, tagKey = "insets_back") },
        colors = TopBarColors(container = PantrixTheme.colors.primary.container),
        insets = PaddingValues(top = 28.dp),
        tagKey = "insets"
    )
}

In its own colours

colors gives the bar colours of its own in place of the theme's. The first bar's fill is the colour of one of the theme's roles, and that alone is given: the theme pairs the colour of the title, the subtitle and the back button with it.

The second bar is in a colour the theme does not have, the app's own purple: the colour that reads on it is given with content. The title, the subtitle and the back button take the content colour. The icon button has colours of its own: the colour that reads on the fill is given to it separately.

val own = ownColors()
PantrixSurface(modifier = Modifier.fillMaxWidth(), border = PantrixTheme.colors.divider) {
    PantrixTopBar(
        title = "Inbox",
        subtitle = "Only the fill is given",
        navigationIcon = { PantrixBackButton(onClick = {}, tagKey = "role_back") },
        colors = TopBarColors(container = PantrixTheme.colors.primary.color),
        tagKey = "role"
    )
}
PantrixSurface(modifier = Modifier.fillMaxWidth(), border = PantrixTheme.colors.divider) {
    PantrixTopBar(
        title = "Inbox",
        subtitle = "3 unread",
        navigationIcon = { PantrixBackButton(onClick = {}, tagKey = "own_back") },
        actions = {
            PantrixIconButton(
                imageVector = PantrixIcons.Search,
                contentDescription = "Search",
                onClick = {},
                colors = ButtonColors(container = Color.Transparent, content = own.onColor),
                tagKey = "own_search"
            )
        },
        colors = TopBarColors(container = own.color, content = own.onColor),
        tagKey = "own"
    )
}

API

title

The bar at the top of a screen: the screen's title, with a navigation icon before it and actions after it.

ParameterTypeDefaultWhat it is
titleStringthe screen's name, on one line.
modifierModifierModifierthe bar's modifier.
subtitleString?nulla line under the title in a smaller type, a count or a state, or null for none.
titleStyleTextStylePantrixTheme.typography.titlethe type the title is set in, in the row: the theme's title unless given. A larger one makes the bar as tall as its line needs. Its colour is the bar's unless the style has one of its own. The large title of a collapsing bar is the theme's headline, whatever this is.
subtitleStyleTextStylePantrixTheme.typography.footnotethe type the subtitle is set in, in the row and in a collapsing bar's band: the theme's footnote unless given. Its colour is the bar's unless the style has one of its own.
navigationIcon(@Composable () -> Unit)?nullwhat stands before the title: a PantrixBackButton, or a button that opens a drawer. Null for none, and the title then starts at the bar's own inset.
actions(@Composable RowScope.() -> Unit)?nullwhat stands after the title, in a row: icon buttons for what the screen can do, the most used first. A button draws in its own colours, the brand's unless it is given others: on a bar with a fill of its own, give each the colour that reads there.
scrolledBooleanfalsewhether a line is drawn along the bar's bottom edge, to part it from what is scrolled under it: pass whether the content can scroll back, as listState.canScrollBackward. The line is inside the bar's height, and nothing moves as it comes and goes.
statePantrixTopBarState?nullgives the bar a collapsing title: kept with rememberPantrixTopBarState, and handed the content's scroll with Modifier.topBarScroll. Null is a bar of one height.
styleTopBarStyle?nullthis bar's own geometry, which a PantrixBackButton in navigationIcon draws with too. Only the fields it sets change, on every platform.
colorsTopBarColors?nullthis bar's own colours, or null for the theme's: its surface, with the title in textPrimary and the subtitle in textSecondary.
insetsPaddingValuesPaddingValues()the room the bar keeps clear at its edges, the status bar's and a cutout's, as a PantrixScaffold hands its top bar. The bar's fill runs behind them.
tagKeyString?nullthe bar's automation key, the same in every language: the bar is tagged TopBar_<tagKey>, and the large title of a collapsing one TopBar_<tagKey>.Title_large.

TopBarStyle

PantrixTopBar's geometry, and the icon PantrixBackButton draws.

ParameterTypeDefaultWhat it is
heightDpDp.UnspecifiedHow tall the bar is, under the insets it keeps clear: 54dp on Android and iOS, 56dp on desktop and the web.
expandedHeightDpDp.UnspecifiedHow tall a bar with a collapsing title is before it collapses: height, and under it the band that holds the title in a large type. 106dp on Android and iOS, 104dp on desktop and the web: a band of 52dp, and of 48dp on desktop and the web. A band is taller only where its lines need it, and a style that sets this no higher than height has no band: its bar is of one height, with a state or without.
horizontalPaddingDpDp.UnspecifiedThe room between the bar's edges and the navigation icon and the actions: 8dp.
titleInsetDpDp.UnspecifiedHow far the title stands from the bar's start edge where there is no navigation icon, and from its end edge where there are no actions: 16dp. The title is never nearer an edge than horizontalPadding.
titleGapDpDp.UnspecifiedThe room between the navigation icon and the title, and between the title and the actions: 8dp.
titleAlignmentTopBarTitleAlignment?nullWhere the title stands: in the middle on iOS, at the start elsewhere.
backIconBackButtonIcon?nullWhat a PantrixBackButton draws: a chevron on iOS, an arrow elsewhere.

TopBarColors

What a PantrixTopBar is painted with, for a bar 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 bar's fill, which runs behind the insets the bar keeps clear. The theme's surface.
contentColorColor.UnspecifiedThe title, the subtitle, and the icons of the navigation icon and the actions that give no colour of their own. On the theme's own fill the title is the theme's textPrimary, the subtitle its textSecondary, and an icon its own default.
dividerColorColor.UnspecifiedThe line along the bar's bottom edge while content is scrolled under it. 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.