Pantrix UI

Button

A button with a text label, the form most buttons take.

import com.pantrixui.components.elements.button.PantrixButton

Examples

Variants

variant picks the button's colour and how prominent it is. A screen has one Solid: the eye goes to it first. Outline, Soft and Subtle are secondary actions; Text is a tertiary action with no surface, like "Cancel". Destructive says the action cannot be undone, and is never the only button of a dialog.

FlowRow(
    horizontalArrangement = Arrangement.spacedBy(PantrixTheme.spacing.sm),
    verticalArrangement = Arrangement.spacedBy(PantrixTheme.spacing.sm)
) {
    ButtonVariant.entries.forEach { variant ->
        PantrixButton(text = variant.name, onClick = {}, variant = variant, tagKey = "variant_${variant.name}")
    }
}

Sizes

size picks one of five heights: 24, 32, 40, 48 and 56 dp. The default is Medium. The two ends are for particular places: ExtraSmall in a dense table row or beside a field, ExtraLarge for the single action a phone screen is built around.

The height is the button's minimum height. When the system's text size is made larger and the label becomes taller than the button, the button grows with it and the label is not cut.

FlowRow(
    horizontalArrangement = Arrangement.spacedBy(PantrixTheme.spacing.sm),
    verticalArrangement = Arrangement.spacedBy(PantrixTheme.spacing.sm),
    itemVerticalAlignment = Alignment.CenterVertically
) {
    ButtonSize.entries.forEach { size ->
        PantrixButton(
            text = size.name,
            onClick = {},
            variant = ButtonVariant.Outline,
            size = size,
            tagKey = "size_${size.name}"
        )
    }
}

Buttons with icons

leadingIcon stands before the label and trailingIcon after it. The icon is given no colour: it takes the button's content colour by itself. Since it is decoration, its contentDescription is null; a screen reader reads the label alone.

FlowRow(
    horizontalArrangement = Arrangement.spacedBy(PantrixTheme.spacing.sm),
    verticalArrangement = Arrangement.spacedBy(PantrixTheme.spacing.sm)
) {
    PantrixButton(
        text = "New folder",
        onClick = {},
        leadingIcon = { PantrixIcon(imageVector = PantrixIcons.Folder, contentDescription = null) },
        tagKey = "new_folder"
    )
    PantrixButton(
        text = "Continue",
        onClick = {},
        variant = ButtonVariant.Soft,
        trailingIcon = { PantrixIcon(imageVector = PantrixIcons.ArrowForward, contentDescription = null) },
        tagKey = "continue"
    )
    PantrixButton(
        text = "Refresh",
        onClick = {},
        variant = ButtonVariant.Outline,
        leadingIcon = { PantrixIcon(imageVector = PantrixIcons.Refresh, contentDescription = null) },
        tagKey = "refresh"
    )
}

Loading and disabled

loading shows a spinner in place of the leading icon and stops presses until the action is done: the same action does not start twice. Press "Save": it shows as loading for two seconds, then the app's snackbar lets you know.

enabled = false draws the button faded, and the button takes no press. The two are not the same thing: a loading button says an action is under way, a disabled one that the action cannot be done right now.

val snackbars = LocalPantrixSnackbars.current
var saving by remember { mutableStateOf(false) }
LaunchedEffect(saving) {
    if (saving) {
        delay(SAVE_MILLIS)
        saving = false
        snackbars?.show(message = "Saved")
    }
}
FlowRow(
    horizontalArrangement = Arrangement.spacedBy(PantrixTheme.spacing.sm),
    verticalArrangement = Arrangement.spacedBy(PantrixTheme.spacing.sm)
) {
    PantrixButton(text = "Save", onClick = { saving = true }, loading = saving, tagKey = "save")
    PantrixButton(text = "Loading", onClick = {}, variant = ButtonVariant.Outline, loading = true, tagKey = "loading_outline")
    PantrixButton(text = "Disabled", onClick = {}, enabled = false, tagKey = "disabled")
    PantrixButton(text = "Disabled", onClick = {}, variant = ButtonVariant.Outline, enabled = false, tagKey = "disabled_outline")
}

private const val SAVE_MILLIS = 2_000L

Full width

fullWidth stretches the button to the width of the space it is in: the submit button under a form, the two buttons of a sheet. Here the column is 360dp at most, and the buttons fill it.

Column(
    modifier = Modifier.widthIn(max = 360.dp),
    verticalArrangement = Arrangement.spacedBy(PantrixTheme.spacing.sm)
) {
    PantrixButton(text = "Complete order", onClick = {}, size = ButtonSize.Large, fullWidth = true, tagKey = "complete_order")
    PantrixButton(text = "Back to shopping", onClick = {}, variant = ButtonVariant.Text, fullWidth = true, tagKey = "back_to_shop")
}

In another role and in a colour of its own

A variant's colours come from the theme's primary role, and Destructive's from the danger role. PantrixColor.buttonColors(variant) gives the same variant in another role: the top row is four variants in the success role.

The colour of the button in the bottom row is in none of the theme's roles: ButtonColors takes the fill, the content's colour and, if wanted, the edge directly.

val success = PantrixTheme.colors.success
val own = ownColors()
FlowRow(
    horizontalArrangement = Arrangement.spacedBy(PantrixTheme.spacing.sm),
    verticalArrangement = Arrangement.spacedBy(PantrixTheme.spacing.sm)
) {
    listOf(ButtonVariant.Solid, ButtonVariant.Soft, ButtonVariant.Outline, ButtonVariant.Text).forEach { variant ->
        PantrixButton(
            text = "Approve",
            onClick = {},
            colors = success.buttonColors(variant),
            tagKey = "confirm_${variant.name.lowercase()}"
        )
    }
}
PantrixButton(
    text = "Join the campaign",
    onClick = {},
    colors = ButtonColors(container = own.color, content = own.onColor),
    tagKey = "own_colors"
)

Shape and style

shape changes the corners of one button: one of the theme's shapes, or any Shape. style changes the button's geometry: here, with pressedScale, a button that shrinks while it is pressed. Only the fields that are given change; the rest comes from the theme's and the platform's style.

FlowRow(
    horizontalArrangement = Arrangement.spacedBy(PantrixTheme.spacing.sm),
    verticalArrangement = Arrangement.spacedBy(PantrixTheme.spacing.sm)
) {
    PantrixButton(text = "Square", onClick = {}, shape = PantrixTheme.shapes.none, tagKey = "square")
    PantrixButton(text = "Slightly rounded", onClick = {}, shape = PantrixTheme.shapes.sm, tagKey = "rounded")
    PantrixButton(text = "Fully rounded", onClick = {}, shape = PantrixTheme.shapes.full, tagKey = "pill")
    PantrixButton(
        text = "Shrinks when pressed",
        onClick = {},
        variant = ButtonVariant.Soft,
        style = ButtonStyle(pressedScale = 0.94f),
        tagKey = "press_scale"
    )
}

Content of its own

The PantrixButton that takes content instead of a label: the content is a row (RowScope) and is drawn in the button's content colour. Here a counter beside the label goes up with every press.

In this form contentDescription is required: whatever is in the button, a screen reader reads one sentence, "Like, 12 likes".

var likes by rememberSaveable { mutableIntStateOf(12) }
PantrixButton(
    onClick = { likes += 1 },
    contentDescription = "Like, $likes likes",
    variant = ButtonVariant.Soft,
    tagKey = "like"
) {
    PantrixIcon(imageVector = PantrixIcons.Star, contentDescription = null)
    PantrixText(text = "Like")
    PantrixText(text = likes.toString(), style = PantrixTheme.typography.footnote)
}

A button that submits a form

A button given a form is the form's submit button. A press validates the form first: while the field is empty onClick is not called, and the field shows its error and takes the focus. If the form is valid, form.submit runs, and while it does the button shows as loading by itself: loading is not given separately.

Leave the field empty and press "Invite", then type a name and try again.

val snackbars = LocalPantrixSnackbars.current
val scope = rememberCoroutineScope()
val form = rememberFormState()
Column(
    modifier = Modifier.widthIn(max = 360.dp),
    verticalArrangement = Arrangement.spacedBy(PantrixTheme.spacing.md)
) {
    PantrixTextField(
        field = form.field("name", initial = "", Rules.notBlank("Enter the name of the person to invite")),
        modifier = Modifier.fillMaxWidth(),
        form = form,
        label = "Name"
    )
    PantrixButton(
        text = "Invite",
        onClick = {
            scope.launch {
                form.submit {
                    delay(SAVE_MILLIS)
                    snackbars?.show(message = "Invitation sent")
                }
            }
        },
        form = form,
        tagKey = "invite"
    )
}

private const val SAVE_MILLIS = 2_000L

API

text

A button with a text label, the form most buttons take.

ParameterTypeDefaultWhat it is
textString
onClick() -> Unit
modifierModifierModifierthe button's modifier.
variantButtonVariantButtonVariant.Solid
sizeButtonSizeButtonSize.Medium
enabledBooleantruewhether the button can be pressed: one that cannot is drawn in the disabled colours and does nothing.
loadingBooleanfalse
fullWidthBooleanfalsetrue stretches the button across the width it is given, its content in the middle.
leadingIcon(@Composable () -> Unit)?null
trailingIcon(@Composable () -> Unit)?nullwhat stands after the label: an arrow, or a count.
styleButtonStyle?null
shapeShapePantrixTheme.style(key = ButtonStyle, override = style).shape.toShape()the button's corners: by default the resolved style's. Given here, it wins over style's.
colorsButtonColorsvariant.colors()this call's own colours, in place of variant's.
contentDescriptionString?null
formFormState?null
tagKeyString?nullthe button's automation key: the button is Button_<tagKey>, and without one a key of where it is called. Visible text is never the key.
interactionSourceMutableInteractionSource?nullthe button's interactions, hoisted: its focus, its hover and its presses. It keeps a source of its own when this is null.

onClick

Slot form, for content a string cannot express.

ParameterTypeDefaultWhat it is
onClick() -> Unitwhat a press does. Given a form, it is called once the form is valid.
contentDescriptionString
modifierModifierModifierthe button's modifier.
variantButtonVariantButtonVariant.Solidthe button's prominence, and the colours that come with it.
sizeButtonSizeButtonSize.Mediumthe button's height, its padding and the type its label is set in.
enabledBooleantruewhether the button can be pressed: one that cannot is drawn in the disabled colours and does nothing.
loadingBooleanfalsewhether the button takes no press while what it began is under way, and tells a screen reader it is loading. What content shows meanwhile is the caller's.
fullWidthBooleanfalsetrue stretches the button across the width it is given, its content in the middle.
styleButtonStyle?null
shapeShapePantrixTheme.style(ButtonStyle, style).shape.toShape()the button's corners: by default the resolved style's. Given here, it wins over style's.
colorsButtonColorsvariant.colors()this call's own colours, in place of variant's.
formFormState?nullthe form the button submits: a press validates it first, and the button takes no press while it is submitting.
tagKeyString?nullthe button's automation key: the button is Button_<tagKey>, and without one a key of where it is called. Visible text is never the key.
interactionSourceMutableInteractionSource?nullthe button's interactions, hoisted: its focus, its hover and its presses. It keeps a source of its own when this is null.
content@Composable RowScope.() -> Unit

ButtonStyle

PantrixButton's geometry: its shape, its press feedback, and a height, padding and icon size for each ButtonSize. PantrixIconButton reads the sizes only.

ParameterTypeDefaultWhat it is
shapeShapeSpecShapeSpec.UnspecifiedThe button's shape: ShapeRole.Full on Android, desktop and the web, ShapeRole.Medium on iOS.
pressedScaleFloatFloat.NaNScale while pressed. 1f means none: the state layer carries the feedback.
extraSmallButtonSizeStyleButtonSizeStyle()ButtonSize.ExtraSmall's geometry.
smallButtonSizeStyleButtonSizeStyle()ButtonSize.Small's geometry.
mediumButtonSizeStyleButtonSizeStyle()ButtonSize.Medium's geometry.
largeButtonSizeStyleButtonSizeStyle()ButtonSize.Large's geometry.
extraLargeButtonSizeStyleButtonSizeStyle()ButtonSize.ExtraLarge's geometry.

ButtonColors

What a button is painted with. ButtonVariant.colors gives a variant's in primary, and PantrixColor.buttonColors the same prominences in any role.

ParameterTypeDefaultWhat it is
containerColorThe fill; transparent for an outline or a text button.
contentColorThe label's and the icon's colour.
borderColorColor.UnspecifiedThe edge's colour, or Color.Unspecified for a button without an edge.

Accessibility