Pantrix UI

Carousel

Slides one beside another, one of them on screen: pictures, cards, the steps of an introduction. A swipe turns them; what is under them says where the carousel is, and turns them for a reader with no finger on the screen.

import com.pantrixui.components.data.carousel.PantrixCarousel

Examples

Featured: with dots

Its plainest form: four slides with dots under them. The slide's index is the screen's state; the carousel takes it with currentPage and reports it back with onPageChange when a swipe ends. The line below reads the same state.

The height was given to the carousel (180dp): the slides fill the room the dots leave. On desktop and in a browser on a desk the previous and next buttons show at the two sides as well; on a phone, in its browser too, a swipe is enough.

var slide by rememberSaveable { mutableIntStateOf(0) }
PantrixCarousel(
    pageCount = TITLES.size,
    currentPage = slide,
    onPageChange = { slide = it },
    modifier = Modifier.height(180.dp),
    contentDescription = "Featured",
    tagKey = "featured"
) { index ->
    CarouselSlide(index, modifier = Modifier.fillMaxSize())
}
PantrixText(text = "On screen: ${TITLES[slide]}", color = PantrixTheme.colors.textSecondary)

@Composable
private fun CarouselSlide(
    index: Int,
    modifier: Modifier = Modifier
) {
    val role = roles()[index % ROLES]
    PantrixSurface(modifier = modifier, color = role.container, contentColor = role.onContainer) {
        Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
            PantrixText(text = TITLES[index % TITLES.size], style = PantrixTheme.typography.title)
        }
    }
}

@Composable
private fun roles(): List<PantrixColor> {
    val colors = PantrixTheme.colors
    return listOf(colors.info, colors.success, colors.warning, colors.danger)
}

private const val ROLES = 4

private val TITLES = listOf("New arrivals", "Pick of the week", "On sale", "Last chance")

Cards whose neighbours show, with a count and buttons

With peek = 24.dp a slide's neighbours show at its two sides: what a swipe will bring is already in sight. indicator = CarouselIndicator.Count writes a count in place of the dots; controls = CarouselControls.Always shows the buttons on a phone too.

var slide by rememberSaveable { mutableIntStateOf(1) }
PantrixCarousel(
    pageCount = TITLES.size,
    currentPage = slide,
    onPageChange = { slide = it },
    contentDescription = "Offers",
    style = CarouselStyle(
        peek = 24.dp,
        indicator = CarouselIndicator.Count,
        controls = CarouselControls.Always
    ),
    tagKey = "offers"
) { index ->
    CarouselSlide(index, modifier = Modifier.fillMaxWidth().height(96.dp))
}

@Composable
private fun CarouselSlide(
    index: Int,
    modifier: Modifier = Modifier
) {
    val role = roles()[index % ROLES]
    PantrixSurface(modifier = modifier, color = role.container, contentColor = role.onContainer) {
        Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
            PantrixText(text = TITLES[index % TITLES.size], style = PantrixTheme.typography.title)
        }
    }
}

private val TITLES = listOf("New arrivals", "Pick of the week", "On sale", "Last chance")

@Composable
private fun roles(): List<PantrixColor> {
    val colors = PantrixTheme.colors
    return listOf(colors.info, colors.success, colors.warning, colors.danger)
}

private const val ROLES = 4

An introduction that goes on by its own button

An introduction that cannot be swiped: swipeEnabled = false, with the indicator and the buttons off too. The steps go on only with "Continue". Which step it is on is the screen's own state; the button adds one to it, and the carousel turns there.

var step by rememberSaveable { mutableIntStateOf(0) }
val last = STEPS.lastIndex
PantrixCarousel(
    pageCount = STEPS.size,
    currentPage = step,
    onPageChange = { step = it },
    swipeEnabled = false,
    contentDescription = "Introduction",
    style = CarouselStyle(indicator = CarouselIndicator.None, controls = CarouselControls.Never),
    tagKey = "introduction"
) { index ->
    PantrixSurface(modifier = Modifier.fillMaxWidth(), border = PantrixTheme.colors.divider) {
        PantrixText(text = STEPS[index], modifier = Modifier.padding(PantrixTheme.spacing.lg))
    }
}
Row(
    horizontalArrangement = Arrangement.spacedBy(PantrixTheme.spacing.md),
    verticalAlignment = Alignment.CenterVertically
) {
    PantrixButton(
        text = if (step == last) "Start over" else "Continue",
        onClick = { step = if (step == last) 0 else step + 1 },
        variant = ButtonVariant.Soft,
        size = ButtonSize.Small,
        tagKey = "introduction_next"
    )
    PantrixText(text = "Step ${step + 1} of ${STEPS.size}", color = PantrixTheme.colors.textSecondary)
}

private val STEPS = listOf(
    "Welcome to Pantrix. This introduction takes three steps.",
    "The components speak the same design language on every platform.",
    "You are ready: you can go back to the list of components and open one."
)

On a fill

A carousel on a fill that is not one of the theme's grounds. No colour was given: the fill says what colour is written on it. The dot of the slide on screen and the buttons are in that colour, and the other dots in a share of it.

var slide by rememberSaveable { mutableIntStateOf(0) }
PantrixSurface(modifier = Modifier.fillMaxWidth(), color = PantrixTheme.colors.primary.color) {
    PantrixCarousel(
        pageCount = 3,
        currentPage = slide,
        onPageChange = { slide = it },
        modifier = Modifier.padding(PantrixTheme.spacing.md),
        contentDescription = "On a fill",
        style = CarouselStyle(controls = CarouselControls.Always),
        tagKey = "filled"
    ) { index ->
        CarouselSlide(index, modifier = Modifier.fillMaxWidth().height(72.dp))
    }
}

@Composable
private fun CarouselSlide(
    index: Int,
    modifier: Modifier = Modifier
) {
    val role = roles()[index % ROLES]
    PantrixSurface(modifier = modifier, color = role.container, contentColor = role.onContainer) {
        Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
            PantrixText(text = TITLES[index % TITLES.size], style = PantrixTheme.typography.title)
        }
    }
}

@Composable
private fun roles(): List<PantrixColor> {
    val colors = PantrixTheme.colors
    return listOf(colors.info, colors.success, colors.warning, colors.danger)
}

private const val ROLES = 4

private val TITLES = listOf("New arrivals", "Pick of the week", "On sale", "Last chance")

A gallery that goes round by itself

Like a picture gallery: with loop the slides go round, and with autoAdvanceMillis the carousel turns by itself every six seconds. placement = CarouselPlacement.Overlaid puts the buttons and the dots over the slide, on the carousel's own fill (dark in the light theme, light in the dark one); they take no room.

The button beside the dots stops the turning and starts it again. While a slide is held, while the mouse is over the carousel and while the keyboard's focus is in it, the carousel waits by itself.

var slide by rememberSaveable { mutableIntStateOf(0) }
PantrixCarousel(
    pageCount = TITLES.size,
    currentPage = slide,
    onPageChange = { slide = it },
    modifier = Modifier.height(200.dp),
    contentDescription = "Gallery",
    loop = true,
    autoAdvanceMillis = GALLERY_MILLIS,
    style = CarouselStyle(controls = CarouselControls.Always, placement = CarouselPlacement.Overlaid),
    tagKey = "gallery"
) { index ->
    CarouselSlide(index, modifier = Modifier.fillMaxSize())
}

@Composable
private fun CarouselSlide(
    index: Int,
    modifier: Modifier = Modifier
) {
    val role = roles()[index % ROLES]
    PantrixSurface(modifier = modifier, color = role.container, contentColor = role.onContainer) {
        Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
            PantrixText(text = TITLES[index % TITLES.size], style = PantrixTheme.typography.title)
        }
    }
}

private val TITLES = listOf("New arrivals", "Pick of the week", "On sale", "Last chance")

private const val GALLERY_MILLIS = 6_000

@Composable
private fun roles(): List<PantrixColor> {
    val colors = PantrixTheme.colors
    return listOf(colors.info, colors.success, colors.warning, colors.danger)
}

private const val ROLES = 4

Slides of different heights

The slides differ in height, and the carousel was given no height. The carousel is as tall as the slide on screen; on the way from one slide to another its height changes along with it, without a jump. The line below shows this: it moves down and up with the slide.

var slide by rememberSaveable { mutableIntStateOf(0) }
PantrixCarousel(
    pageCount = NOTES.size,
    currentPage = slide,
    onPageChange = { slide = it },
    contentDescription = "Notes",
    loop = true,
    tagKey = "notes"
) { index ->
    PantrixSurface(modifier = Modifier.fillMaxWidth(), border = PantrixTheme.colors.divider) {
        PantrixText(text = NOTES[index], modifier = Modifier.padding(PantrixTheme.spacing.lg))
    }
}
PantrixText(
    text = "This line moves with the height of the slides.",
    color = PantrixTheme.colors.textSecondary
)

private val NOTES = listOf(
    "A short note.",
    "A longer note: it runs to several lines, because it has more to say. As the carousel comes to this slide, " +
        "its height grows gradually too; the line below moves down.",
    "A medium-sized note: it takes about two lines and is shorter than the one before."
)

In its own colours

In its own colours: CarouselColors paints the dots, the dots' track, the buttons and the scrim over the slide. Here the dots are in the colour of success, not the brand's; every colour not given is the theme's.

var slide by rememberSaveable { mutableIntStateOf(0) }
PantrixCarousel(
    pageCount = TITLES.size,
    currentPage = slide,
    onPageChange = { slide = it },
    modifier = Modifier.height(160.dp),
    colors = CarouselColors(
        indicator = PantrixTheme.colors.success.color,
        indicatorTrack = PantrixTheme.colors.success.container,
        control = PantrixTheme.colors.success.color
    ),
    contentDescription = "Featured",
    tagKey = "green"
) { index ->
    CarouselSlide(index, modifier = Modifier.fillMaxSize())
}

@Composable
private fun CarouselSlide(
    index: Int,
    modifier: Modifier = Modifier
) {
    val role = roles()[index % ROLES]
    PantrixSurface(modifier = modifier, color = role.container, contentColor = role.onContainer) {
        Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
            PantrixText(text = TITLES[index % TITLES.size], style = PantrixTheme.typography.title)
        }
    }
}

@Composable
private fun roles(): List<PantrixColor> {
    val colors = PantrixTheme.colors
    return listOf(colors.info, colors.success, colors.warning, colors.danger)
}

private const val ROLES = 4

private val TITLES = listOf("New arrivals", "Pick of the week", "On sale", "Last chance")

API

pageCount

Slides one beside another, one of them on screen: pictures, cards, the steps of an introduction. A swipe turns them; what is under them says where the carousel is, and turns them for a reader with no finger on the screen.

ParameterTypeDefaultWhat it is
pageCountInthow many slides there are.
currentPageIntthe slide on screen, counted from zero, as a pager's is. One past either end is the nearest slide. A PantrixPagination counts its pages from one, as it writes them: its page is this and one.
onPageChange(page: Int) -> Unittold the slide a swipe ended on, and the slide a button, a dot or a key asks for, or the carousel itself when it turns by itself.
modifierModifierModifierapplied to the carousel: the slides and what is under them.
contentDescriptionString?nullwhat the carousel holds, for a screen reader: "Featured products".
swipeEnabledBooleantruewhether a swipe turns the slides. An introduction that goes on by its own button turns it off.
loopBooleanfalsewhether the slides go round, the last on to the first. A carousel of one slide has nowhere to go.
autoAdvanceMillisInt0how long the carousel stays on a slide before it turns to the next by itself, or nothing for a carousel that is turned by its reader alone. Long enough to read a slide in: five seconds or more.
styleCarouselStyle?nullthis call's own style: over every enclosing one and under the density. Only the fields it sets change, on every platform.
colorsCarouselColors?nullthis call's own colours for what is under the slides. Only the ones it sets change.
tagKeyString?nulla locale-independent key for automation: the carousel's tag is Carousel_<tagKey>, and its parts are named inside it: Carousel_<tagKey>.Slides_view, which takes the keyboard's focus, .Page_<index>, .Control_previous, .Control_next, .Control_pause and .Indicator_position. Where the slides go round and a slide's neighbours show, a carousel of two slides has the same slide at both sides, twice under one tag.
page@Composable (index: Int) -> Unita slide, by its index.

CarouselStyle

PantrixCarousel's geometry and what it shows under its slides. It is the same on every platform: the buttons come and go with the pointer the reader has, not with the platform. The rooms follow the density; a dot does not.

ParameterTypeDefaultWhat it is
pageSpacingDpDp.UnspecifiedBetween two slides, seen while one gives way to the next and beside a slide whose neighbours peek.
peekDpDp.UnspecifiedHow much room is kept at each side of the current slide, in which its neighbours show.
indicatorSizeDpDp.UnspecifiedA dot of the indicator.
indicatorGapDpDp.UnspecifiedBetween two dots.
indicatorSpacingDpDp.UnspecifiedBetween the slides and what is under them. Where that stands over the slides, between it and the slides' edges.
indicatorCarouselIndicator?nullWhat says where the carousel is.
controlsCarouselControls?nullWhere the two buttons are shown.
placementCarouselPlacement?nullWhether those stand under the slides or over them.

CarouselColors

What a PantrixCarousel is painted with under its slides, for a carousel 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. The slides are the caller's own.

ParameterTypeDefaultWhat it is
indicatorColorColor.UnspecifiedThe dot of the current slide. The primary role's color.
indicatorTrackColorColor.UnspecifiedThe dots of the other slides. The theme's borderStrong, which is told from a ground as a control's own mark is.
controlColorColor.UnspecifiedThe buttons. The theme's onSurface, an icon's colour.
overlayColorColor.UnspecifiedThe fill behind the dots and behind each button where they stand over the slides. The theme's onSurface, the tone opposite the theme's grounds, thinned so that the slide shows through it. One of the theme's role colours given here brings the colour the theme pairs with it for what is written on it.

Accessibility