Pantrix UI

Select

One choice out of a list the control opens: a country, a currency, a plan.

import com.pantrixui.components.overlay.select.PantrixSelect

Examples

One choice

At its plainest: the options, selected and onSelect. The box shows what is chosen, and placeholder while nothing is; label is above the box and supportingText below it.

Press the box: the list opens under it, as wide as the box, and the box is drawn as a field with the focus. A press on an option chooses it and closes the list; a press outside it, the back key and Escape only close it. The card's "Last event" line shows what onSelect last asked for; most of the other examples have the same line.

var country by rememberSaveable { mutableStateOf<String?>(null) }
val report = LocalExampleEvents.current
PantrixSelect(
    options = COUNTRIES,
    selected = country,
    onSelect = { code ->
        country = code
        report("Country selected: ${COUNTRIES.first { it.value == code }.label}")
    },
    modifier = Modifier.widthIn(max = 360.dp).fillMaxWidth(),
    label = "Country",
    placeholder = "Choose a country",
    supportingText = "Your invoice is issued with this country's tax.",
    tagKey = "country"
)

private val COUNTRIES = listOf(
    SelectOption(value = "tr", label = "Türkiye", tagKey = "tr"),
    SelectOption(value = "de", label = "Germany", tagKey = "de"),
    SelectOption(value = "gb", label = "United Kingdom", tagKey = "gb"),
    SelectOption(value = "jp", label = "Japan", tagKey = "jp"),
    SelectOption(value = "us", label = "United States of America", tagKey = "us")
)

Searchable

searchable = true puts a search field in the list: as you type, the options narrow to those whose label holds what is typed, and the list gets as short as the rows that are left. searchPlaceholder is what the field shows while it is empty.

The focus stays in the search field: the arrows move a mark among the rows that match, and the keyboard's action key chooses the marked row. As soon as something is typed the mark is on the first match. On a desktop and in a desktop's browser the field takes the focus when the list opens; on a phone and a tablet it waits to be touched. When the keyboard comes up the list stays under the box and gets shorter.

var language by rememberSaveable { mutableStateOf<String?>(null) }
val report = LocalExampleEvents.current
PantrixSelect(
    options = LANGUAGES,
    selected = language,
    onSelect = { name ->
        language = name
        report("Language selected: $name")
    },
    modifier = Modifier.widthIn(max = 360.dp).fillMaxWidth(),
    label = "Select a language (searchable)",
    placeholder = "Choose a language",
    searchable = true,
    searchPlaceholder = "Search languages…",
    tagKey = "language"
)

private val LANGUAGES = listOf(
    SelectOption(value = "Kotlin", label = "Kotlin", tagKey = "kotlin"),
    SelectOption(value = "Java", label = "Java", tagKey = "java"),
    SelectOption(value = "Swift", label = "Swift", tagKey = "swift"),
    SelectOption(value = "Dart", label = "Dart", tagKey = "dart"),
    SelectOption(value = "TypeScript", label = "TypeScript", tagKey = "typescript"),
    SelectOption(value = "JavaScript", label = "JavaScript", tagKey = "javascript"),
    SelectOption(value = "Python", label = "Python", tagKey = "python"),
    SelectOption(value = "Go", label = "Go", tagKey = "go"),
    SelectOption(value = "Rust", label = "Rust", tagKey = "rust"),
    SelectOption(value = "C#", label = "C#", tagKey = "csharp"),
    SelectOption(value = "Ruby", label = "Ruby", tagKey = "ruby")
)

Partly disabled

SelectOption(enabled = false) disables a single option. The option stays in the list and is read, but it is written in the disabled colour, a press on it does nothing and the keyboard skips it: it is not taken out of the list, so that what cannot be chosen is known to be there too.

var option by rememberSaveable { mutableStateOf<String?>(null) }
val report = LocalExampleEvents.current
PantrixSelect(
    options = PARTLY_DISABLED,
    selected = option,
    onSelect = { value ->
        option = value
        report("Selected: ${PARTLY_DISABLED.first { it.value == value }.label}")
    },
    modifier = Modifier.widthIn(max = 360.dp).fillMaxWidth(),
    label = "Option",
    placeholder = "Choose an option",
    tagKey = "partly_disabled"
)

private val PARTLY_DISABLED = listOf(
    SelectOption(value = "active_1", label = "Active option 1", tagKey = "active_1"),
    SelectOption(value = "disabled_1", label = "Disabled option", enabled = false, tagKey = "disabled_1"),
    SelectOption(value = "active_2", label = "Active option 2", tagKey = "active_2"),
    SelectOption(value = "disabled_2", label = "Disabled option 2", enabled = false, tagKey = "disabled_2")
)

Options with icons

SelectOption(icon = …) puts an icon at the start of the option: the icon is in the label's colour, and a screen reader passes over it. The selected row is marked by its fill and by the tick at its end. The box shows only the label of what is chosen.

var folder by rememberSaveable { mutableStateOf<String?>("documents") }
val report = LocalExampleEvents.current
PantrixSelect(
    options = FOLDERS,
    selected = folder,
    onSelect = { value ->
        folder = value
        report("Folder selected: ${FOLDERS.first { it.value == value }.label}")
    },
    modifier = Modifier.widthIn(max = 360.dp).fillMaxWidth(),
    label = "Folder",
    tagKey = "folder"
)

private val FOLDERS = listOf(
    SelectOption(value = "documents", label = "Documents", icon = PantrixIcons.Folder, tagKey = "documents"),
    SelectOption(value = "favourites", label = "Favourites", icon = PantrixIcons.Star, tagKey = "favourites"),
    SelectOption(value = "inbox", label = "Inbox", icon = PantrixIcons.Inbox, tagKey = "inbox")
)

Workspace switcher

This is not a PantrixSelect. The options of PantrixSelect are a one-line label and an icon; when an avatar, a second line of text and an action row at the bottom are needed, the switcher is built with PantrixPopover and these are written as the popover's content: with PantrixTextField, PantrixAvatar and PantrixDivider.

The trigger is a row with an avatar, a name and a plan. The panel that opens has a search field, a heading, rows with an avatar and a second line of text, a tick at the end of the selected row and, at the bottom, a row that creates a new workspace. A press on a row selects it and closes the panel; the search starts empty each time the panel opens.

var open by rememberSaveable { mutableStateOf(false) }
var currentId by rememberSaveable { mutableStateOf(WORKSPACES.first().id) }
val report = LocalExampleEvents.current
val query = rememberTextFieldState()
val current = WORKSPACES.first { it.id == currentId }
val typed = query.text.toString().trim()
val shown = WORKSPACES.filter { it.name.contains(typed, ignoreCase = true) }
PantrixPopover(
    visible = open,
    onDismissRequest = {
        open = false
        report("Workspace panel closed")
    },
    anchor = {
        SelectWorkspaceTrigger(
            workspace = current,
            onClick = {
                // The search is emptied as the panel opens, not as it closes: the list inside does not move while the panel fades.
                query.clearText()
                open = true
            }
        )
    },
    alignment = PopoverAlignment.Start,
    style = PopoverStyle(minWidth = 280.dp, maxWidth = 280.dp),
    tagKey = "workspace"
) {
    PantrixTextField(
        state = query,
        modifier = Modifier.fillMaxWidth(),
        placeholder = "Search workspaces…",
        contentDescription = "Search workspaces",
        tagKey = "workspace_search"
    )
    PantrixText(
        text = "Workspaces",
        style = PantrixTheme.typography.label,
        color = PantrixTheme.colors.textSecondary
    )
    if (shown.isEmpty()) {
        PantrixText(text = "No workspaces match", color = PantrixTheme.colors.textSecondary)
    }
    shown.forEach { workspace ->
        SelectWorkspaceRow(workspace = workspace, selected = workspace.id == currentId) {
            currentId = workspace.id
            open = false
            report("Workspace: ${workspace.name}")
        }
    }
    PantrixDivider()
    SelectWorkspaceCreateRow {
        open = false
        report("Create workspace was pressed")
    }
}

@Composable
private fun SelectWorkspaceTrigger(
    workspace: SelectWorkspace,
    onClick: () -> Unit
) {
    val spacing = PantrixTheme.spacing
    Row(
        modifier = Modifier
            .widthIn(max = 280.dp)
            .fillMaxWidth()
            .clip(PantrixTheme.shapes.md)
            .testTag("workspace_trigger")
            .clickable(role = Role.DropdownList, onClick = onClick)
            .padding(spacing.sm),
        horizontalArrangement = Arrangement.spacedBy(spacing.sm),
        verticalAlignment = Alignment.CenterVertically
    ) {
        PantrixAvatar(contentDescription = null, initials = workspace.initials, tone = AvatarTone.Primary)
        Column(modifier = Modifier.weight(1f)) {
            PantrixText(text = workspace.name, style = PantrixTheme.typography.label, maxLines = 1)
            PantrixText(
                text = workspace.plan,
                style = PantrixTheme.typography.footnote,
                color = PantrixTheme.colors.textSecondary
            )
        }
        PantrixIcon(
            imageVector = PantrixIcons.ChevronDown,
            contentDescription = null,
            tint = PantrixTheme.colors.textSecondary
        )
    }
}

@Composable
private fun SelectWorkspaceRow(
    workspace: SelectWorkspace,
    selected: Boolean,
    onClick: () -> Unit
) {
    val spacing = PantrixTheme.spacing
    Row(
        modifier = Modifier
            .fillMaxWidth()
            .clip(PantrixTheme.shapes.md)
            .testTag("workspace_${workspace.id}")
            .selectable(selected = selected, role = Role.RadioButton, onClick = onClick)
            .padding(vertical = spacing.xs),
        horizontalArrangement = Arrangement.spacedBy(spacing.sm),
        verticalAlignment = Alignment.CenterVertically
    ) {
        PantrixAvatar(contentDescription = null, initials = workspace.initials, tone = AvatarTone.Primary)
        Column(modifier = Modifier.weight(1f)) {
            PantrixText(text = workspace.name, style = PantrixTheme.typography.label, maxLines = 1)
            PantrixText(
                text = "${workspace.projects} projects",
                style = PantrixTheme.typography.footnote,
                color = PantrixTheme.colors.textSecondary
            )
        }
        if (selected) {
            PantrixIcon(
                imageVector = PantrixIcons.Check,
                contentDescription = null,
                tint = PantrixTheme.colors.primary.color
            )
        }
    }
}

@Composable
private fun SelectWorkspaceCreateRow(onClick: () -> Unit) {
    val spacing = PantrixTheme.spacing
    Row(
        modifier = Modifier
            .fillMaxWidth()
            .clip(PantrixTheme.shapes.md)
            .testTag("workspace_create")
            .clickable(role = Role.Button, onClick = onClick)
            .padding(vertical = spacing.sm),
        horizontalArrangement = Arrangement.spacedBy(spacing.sm),
        verticalAlignment = Alignment.CenterVertically
    ) {
        PantrixIcon(imageVector = PantrixIcons.Plus, contentDescription = null)
        PantrixText(text = "Create workspace", style = PantrixTheme.typography.label)
    }
}

private class SelectWorkspace(
    val id: String,
    val name: String,
    val initials: String,
    val plan: String,
    val projects: Int
)

private val WORKSPACES = listOf(
    SelectWorkspace(id = "rorty_live", name = "Rorty LIVE", initials = "RL", plan = "free", projects = 4),
    SelectWorkspace(id = "rorty_staging", name = "Rorty Staging", initials = "RS", plan = "free", projects = 2),
    SelectWorkspace(id = "pantrix_labs", name = "Pantrix Labs", initials = "PL", plan = "pro", projects = 7)
)

Form field

The PantrixSelect that takes a field is bound to a form's field: the field's value is the choice, its error takes the place of the supporting text, and its id is the automation key (Select_plan). A choice that has to be made is built with a field that starts at null and Rules.required.

Press "Send" without picking anything: the rule's message takes the place of the supporting text and the focus comes to the select. A choice changes the field and marks it "touched": once a plan is chosen the message goes away. "Reset" takes the form back to the start.

val form = rememberFormState()
val scope = rememberCoroutineScope()
val plan = form.field<String?>("plan", initial = null, Rules.required("Choose a plan to continue"))
val report = LocalExampleEvents.current
PantrixSelect(
    field = plan,
    options = PLANS,
    modifier = Modifier.widthIn(max = 360.dp).fillMaxWidth(),
    label = "Plan",
    placeholder = "Choose a plan",
    supportingText = "You can change it later."
)
Row(horizontalArrangement = Arrangement.spacedBy(PantrixTheme.spacing.sm)) {
    PantrixButton(
        text = "Send",
        onClick = {
            scope.launch {
                form.submit { report("Form sent: ${PLANS.first { it.value == plan.value }.label}") }
            }
        },
        form = form,
        tagKey = "submit_plan"
    )
    PantrixButton(
        text = "Reset",
        onClick = {
            form.reset()
            report("Form reset")
        },
        variant = ButtonVariant.Text,
        tagKey = "reset_plan"
    )
}

private val PLANS = listOf(
    SelectOption(value = "free", label = "Free", tagKey = "free"),
    SelectOption(value = "pro", label = "Pro", tagKey = "pro"),
    SelectOption(value = "team", label = "Team", tagKey = "team")
)

Error and disabled

error = true turns the box's border, its label and the text below it to the theme's danger role; the text below is announced to a screen reader as the error. enabled = false draws the box in the disabled colours: the box takes no focus and the list does not open.

Both are drawn as a text field is in those states. A disabled select still shows what is selected.

PantrixSelect(
    options = PLANS,
    selected = null,
    onSelect = {},
    modifier = Modifier.widthIn(max = 360.dp).fillMaxWidth(),
    label = "Plan",
    placeholder = "Choose a plan",
    supportingText = "Choose a plan to continue",
    error = true,
    tagKey = "plan_in_error"
)
PantrixSelect(
    options = PLANS,
    selected = "team",
    onSelect = {},
    modifier = Modifier.widthIn(max = 360.dp).fillMaxWidth(),
    enabled = false,
    label = "Plan",
    supportingText = "Only an administrator can change the plan.",
    tagKey = "plan_disabled"
)

private val PLANS = listOf(
    SelectOption(value = "free", label = "Free", tagKey = "free"),
    SelectOption(value = "pro", label = "Pro", tagKey = "pro"),
    SelectOption(value = "team", label = "Team", tagKey = "team")
)

A long list

When the options pass the list's tallest height, the list scrolls. The list opens with the selected row in view near its middle: here the thirty-sixth of sixty years is selected, and the list opens already scrolled to it.

Opened by the keyboard, the list has the focus on the selected row. Opened by a touch or a click, no row shows the focus; the first arrow key moves on from the selected row.

var year by rememberSaveable { mutableStateOf<Int?>(1995) }
val report = LocalExampleEvents.current
PantrixSelect(
    options = YEARS,
    selected = year,
    onSelect = {
        year = it
        report("Year selected: $it")
    },
    modifier = Modifier.widthIn(max = 360.dp).fillMaxWidth(),
    label = "Year of birth",
    tagKey = "year"
)

private val YEARS = (1960..2019).map { year ->
    SelectOption(value = year, label = year.toString(), tagKey = year.toString())
}

Width

A select given no width is as wide as a text field's least width: the first box is like this. In a row, with Modifier.weight(1f), it is as wide as its share of the row: the two boxes in the middle share the row.

The box at the bottom is given Modifier.width(IntrinsicSize.Max): the select is asked how wide it would be, and answers with what its options need. The box is as wide as its longest option and no wider, and stays so whichever is chosen. A select in a toolbar or in a table's foot is built this way: the select of PantrixPageSize is one too. Choose another currency: the box stands still.

The list is always as wide as its box. A label that does not fit the box is cut off on one line; in the box that is as wide as its options no option is cut, nor in its row of the list.

var alone by rememberSaveable { mutableStateOf<String?>("usd") }
var from by rememberSaveable { mutableStateOf<String?>("try") }
var to by rememberSaveable { mutableStateOf<String?>("eur") }
var fitted by rememberSaveable { mutableStateOf<String?>("gbp") }
val report = LocalExampleEvents.current
PantrixSelect(
    options = CURRENCIES,
    selected = alone,
    onSelect = { code ->
        alone = code
        report("Currency: ${CURRENCIES.first { it.value == code }.label}")
    },
    label = "Currency",
    tagKey = "currency"
)
Row(
    modifier = Modifier.widthIn(max = 480.dp).fillMaxWidth(),
    horizontalArrangement = Arrangement.spacedBy(PantrixTheme.spacing.sm)
) {
    PantrixSelect(
        options = CURRENCIES,
        selected = from,
        onSelect = { code ->
            from = code
            report("Source currency: ${CURRENCIES.first { it.value == code }.label}")
        },
        modifier = Modifier.weight(1f),
        label = "Source",
        tagKey = "currency_from"
    )
    PantrixSelect(
        options = CURRENCIES,
        selected = to,
        onSelect = { code ->
            to = code
            report("Target currency: ${CURRENCIES.first { it.value == code }.label}")
        },
        modifier = Modifier.weight(1f),
        label = "Target",
        tagKey = "currency_to"
    )
}
PantrixSelect(
    options = CURRENCIES,
    selected = fitted,
    onSelect = { code ->
        fitted = code
        report("In the box as wide as its options: ${CURRENCIES.first { it.value == code }.label}")
    },
    modifier = Modifier.width(IntrinsicSize.Max),
    contentDescription = "Currency",
    tagKey = "currency_fitted"
)

private val CURRENCIES = listOf(
    SelectOption(value = "try", label = "Turkish lira", tagKey = "try"),
    SelectOption(value = "eur", label = "Euro", tagKey = "eur"),
    SelectOption(value = "usd", label = "US dollar", tagKey = "usd"),
    SelectOption(value = "gbp", label = "Pound sterling", tagKey = "gbp")
)

Without a label

A select in a toolbar or at the head of a list has no label. contentDescription tells a screen reader its name; the list that opens is announced by that name too.

When no label is given, no room is kept above the box: the select takes only as much room as its box.

var sort by rememberSaveable { mutableStateOf<String?>("newest") }
PantrixSelect(
    options = listOf(
        SelectOption(value = "newest", label = "Newest", tagKey = "newest"),
        SelectOption(value = "cheapest", label = "Cheapest", tagKey = "cheapest"),
        SelectOption(value = "nearest", label = "Nearest", tagKey = "nearest")
    ),
    selected = sort,
    onSelect = { sort = it },
    contentDescription = "Sort order",
    tagKey = "sort"
)

Its own colours

colors is the list's colours and fieldColors the box's; both change only the fields that are given, and what is not given stays the theme's. Here the selected row's fill and text (SelectColors) and the box's focused border (TextFieldColors) are in the app's own colour.

Open the list: the box's border is in the app's purple, and the selected row in that purple's soft fill. A disabled select is drawn in the theme's disabled colours, even when colours are given.

val own = ownColors()
var plan by rememberSaveable { mutableStateOf<String?>("pro") }
val report = LocalExampleEvents.current
PantrixSelect(
    options = PLANS,
    selected = plan,
    onSelect = { value ->
        plan = value
        report("Plan selected: ${PLANS.first { it.value == value }.label}")
    },
    modifier = Modifier.widthIn(max = 360.dp).fillMaxWidth(),
    label = "Plan",
    colors = SelectColors(selectedContainer = own.container, selectedContent = own.onContainer),
    fieldColors = TextFieldColors(focusedBorder = own.color),
    tagKey = "own_plan"
)

private val PLANS = listOf(
    SelectOption(value = "free", label = "Free", tagKey = "free"),
    SelectOption(value = "pro", label = "Pro", tagKey = "pro"),
    SelectOption(value = "team", label = "Team", tagKey = "team")
)

Style

style is the list's geometry and fieldStyle the box's. Here the list's rows are 56dp tall (itemHeight), the list is 180dp at its tallest and the options that do not fit scroll inside it (maxHeight), and there is a 12dp gap between it and the box (gap). The box's corners are fully round: TextFieldStyle(shape = ShapeRole.Full).

Only the fields that are given change; the rest comes from the theme's and the platform's style. The list is as wide as its box whatever the style says, and a row is never shorter than the platform's touch target.

var currency by rememberSaveable { mutableStateOf<String?>("try") }
PantrixSelect(
    options = CURRENCIES,
    selected = currency,
    onSelect = { currency = it },
    modifier = Modifier.widthIn(max = 360.dp).fillMaxWidth(),
    label = "Currency",
    style = SelectStyle(maxHeight = 180.dp, gap = 12.dp, itemHeight = 56.dp),
    fieldStyle = TextFieldStyle(shape = ShapeRole.Full),
    tagKey = "styled_currency"
)

private val CURRENCIES = listOf(
    SelectOption(value = "try", label = "Turkish lira", tagKey = "try"),
    SelectOption(value = "eur", label = "Euro", tagKey = "eur"),
    SelectOption(value = "usd", label = "US dollar", tagKey = "usd"),
    SelectOption(value = "gbp", label = "Pound sterling", tagKey = "gbp")
)

In a dialog

A select inside a dialog: the list opens over the dialog. Escape and the back key close the list first, and the dialog stays open; the dialog closes on the second press.

Press "Edit address", open the list and press Escape twice: first the list closes, then the dialog. The select here is searchable.

var dialog by rememberSaveable { mutableStateOf(false) }
var country by rememberSaveable { mutableStateOf<String?>("tr") }
val report = LocalExampleEvents.current
PantrixButton(
    text = "Edit address",
    onClick = { dialog = true },
    variant = ButtonVariant.Outline,
    tagKey = "open_address_dialog"
)
PantrixDialog(
    visible = dialog,
    onDismissRequest = {
        dialog = false
        report("Address dialog closed")
    },
    title = "Delivery address",
    message = "Choose the country the order will be sent to.",
    actions = {
        PantrixButton(
            text = "OK",
            onClick = {
                dialog = false
                report("Address dialog closed")
            },
            variant = ButtonVariant.Text,
            tagKey = "close_address_dialog"
        )
    },
    tagKey = "address_dialog"
) {
    PantrixSelect(
        options = COUNTRIES,
        selected = country,
        onSelect = { code ->
            country = code
            report("Delivery country: ${COUNTRIES.first { it.value == code }.label}")
        },
        modifier = Modifier.fillMaxWidth(),
        label = "Country",
        searchable = true,
        searchPlaceholder = "Search countries…",
        tagKey = "delivery_country"
    )
}

private val COUNTRIES = listOf(
    SelectOption(value = "tr", label = "Türkiye", tagKey = "tr"),
    SelectOption(value = "de", label = "Germany", tagKey = "de"),
    SelectOption(value = "gb", label = "United Kingdom", tagKey = "gb"),
    SelectOption(value = "jp", label = "Japan", tagKey = "jp"),
    SelectOption(value = "us", label = "United States of America", tagKey = "us")
)

A sheet in a narrow window

presentation = OverlayPresentation.Adaptive: in a narrow window the list is a sheet from the bottom, its title the field's label and its rows as wide as the sheet; in a wider one it is under the box. Pick "Narrow" and press the field. The default is under the box in every window; PantrixMultiSelect takes the same parameter.

var city by rememberSaveable { mutableStateOf<String?>(null) }
// 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(400.dp)) {
    PantrixSelect(
        options = listOf("Istanbul", "Ankara", "Izmir", "Bursa", "Antalya").map {
            SelectOption(value = it, label = it)
        },
        selected = city,
        onSelect = { city = it },
        modifier = Modifier.fillMaxWidth(),
        label = "City",
        tagKey = "adaptive_city",
        presentation = OverlayPresentation.Adaptive
    )
}

In a part in another theme

The list is in the colours of the theme it is called in. The part here is a second theme in the opposite colours to the page (PantrixTheme(darkTheme = …)): dark if the page is light, light if it is dark. The select is called inside that theme, so both its box and the list it opens are in that theme's colours.

A theme draws no ground: the part's ground is the PantrixSurface inside the theme. This is the last example on the page: when there is no room left below it, the list opens above the box.

var plan by rememberSaveable { mutableStateOf<String?>(null) }
val report = LocalExampleEvents.current
val pageIsDark = PantrixTheme.colors.background.luminance() < 0.5f
PantrixTheme(darkTheme = !pageIsDark) {
    PantrixSurface(modifier = Modifier.widthIn(max = 392.dp).fillMaxWidth()) {
        Column(modifier = Modifier.padding(PantrixTheme.spacing.md)) {
            PantrixSelect(
                options = PLANS,
                selected = plan,
                onSelect = { value ->
                    plan = value
                    report("Plan selected in the part: ${PLANS.first { it.value == value }.label}")
                },
                modifier = Modifier.fillMaxWidth(),
                label = "Plan",
                placeholder = "Choose a plan",
                tagKey = "plan_in_part"
            )
        }
    }
}

private val PLANS = listOf(
    SelectOption(value = "free", label = "Free", tagKey = "free"),
    SelectOption(value = "pro", label = "Pro", tagKey = "pro"),
    SelectOption(value = "team", label = "Team", tagKey = "team")
)

API

options

One choice out of a list the control opens: a country, a currency, a plan.

ParameterTypeDefaultWhat it is
optionsList<SelectOption<T>>the choices, in the order they are shown. With none, the list says so.
selectedT?what is chosen now: the first option whose value it equals. A value no option has is nothing chosen, and so is null, unless an option stands for it.
onSelect(T) -> Unitasked for another choice, with the option's value. It is asked for the chosen one again too.
modifierModifierModifierthe modifier of the whole field: its label, its box and what is under it. fillMaxWidth() fills a form's column, and width(IntrinsicSize.Max) is as wide as the options need.
enabledBooleantruewhether the list can be opened. A select that cannot be takes no focus, is drawn in the disabled colours and says so.
labelString?nullwhat the choice is, written above the box: the field's name for a screen reader, and what its list is announced as.
placeholderString?nullshown in the box while nothing is chosen.
supportingTextString?nullhelp under the box. With error it is the message that says what to put right, which a screen reader announces as the error.
errorBooleanfalsewhether the choice is wrong: the border, the label and the supporting text turn to the danger role, and a screen reader is told.
searchableBooleanfalsewhether the list has a field to search its options by.
searchPlaceholderString?nullwhat the search field shows while it is empty, and what it is called. The theme's word for a search where it is null.
styleSelectStyle?nullthis call's own style, the one its list is drawn with: over every enclosing one and under the density. Only the fields it sets change, on every platform.
fieldStyleTextFieldStyle?nullthis call's own TextFieldStyle, the one its box is drawn with: over every enclosing one and under the density. Only the fields it sets change, on every platform.
colorsSelectColors?nullits list's own colours, in place of the theme's roles: the ones it sets are used, and the rest stay the theme's.
fieldColorsTextFieldColors?nullits box's own colours, as a text field takes them. A disabled select is drawn in the theme's disabled colours whatever is given.
contentDescriptionString?nullwhat a screen reader says for the field: its name where it has no label, as a select in a toolbar has none.
tagKeyString?nullthe field's automation key: the field is Select_<tagKey>, its list SelectList_<tagKey>, the list's search field SelectSearch_<tagKey>, and its options are named inside it, Select_<tagKey>.SelectOption_<option's key>. It reports that its list opened and that an option was chosen, by the option's key and never by its label.
interactionSourceMutableInteractionSource?nullthe field's interactions, hoisted: its focus, its hover and its presses. It keeps a source of its own when this is null.
presentationOverlayPresentationOverlayPresentation.Popoverhow the list is shown: under the field, by default; as a sheet from the bottom of the window, its title the field's label; or the one in a narrow window and the other in a wider one. Chosen as the list opens, it stays until the list has closed.

field

A select bound to a form's field: its choice, its error and its name come from field.

ParameterTypeDefaultWhat it is
fieldFormField<T>the field the select shows and changes.
optionsList<SelectOption<T>>the choices, in the order they are shown.
modifierModifierModifierthe modifier of the whole field: its label, its box and what is under it.
enabledBooleantruewhether the list can be opened.
labelString?nullwhat the choice is, written above the box: the field's name for a screen reader.
placeholderString?nullshown in the box while nothing is chosen.
supportingTextString?nullhelp under the box, while the field has no error to show there.
searchableBooleanfalsewhether the list has a field to search its options by.
searchPlaceholderString?nullwhat the search field shows while it is empty, and what it is called.
styleSelectStyle?nullthis call's own style, the one its list is drawn with: over every enclosing one and under the density.
fieldStyleTextFieldStyle?nullthis call's own TextFieldStyle, the one its box is drawn with.
colorsSelectColors?nullits list's own colours: the ones it sets, with the theme's for the rest.
fieldColorsTextFieldColors?nullits box's own colours, as a text field takes them.
contentDescriptionString?nullwhat a screen reader says for the field where it has no label.
interactionSourceMutableInteractionSource?nullthe field's interactions, hoisted. It keeps a source of its own when this is null.
presentationOverlayPresentationOverlayPresentation.Popoverhow the list is shown: under the field, as a sheet, or by the window's width.

SelectStyle

The list a PantrixSelect opens: the panel it is drawn in, how far that sits from the box it belongs to and from the edges of the window, and its rows.

ParameterTypeDefaultWhat it is
shapeShapeSpecShapeSpec.UnspecifiedThe panel's corners: the medium step of the shape scale on every platform, as the box's are.
maxHeightDpDp.UnspecifiedThe tallest the panel gets: more options than fit scroll.
elevationDpDp.UnspecifiedHow high the panel floats over the app, which is how large its shadow is.
gapDpDp.UnspecifiedBetween the box and its list.
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.
horizontalPaddingDpDp.UnspecifiedFrom the panel's edge to what a row starts with, and from what it ends with to the other edge. Each preset has the text field's own, so that an option's label starts where the box's text does.
iconSizeDpDp.UnspecifiedAn option's icon, and the tick beside the chosen one.
iconGapDpDp.UnspecifiedBetween a row's parts: its check box, its icon, its label and its tick.

SelectColors

What the list of a PantrixSelect 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. The box's colours are TextFieldColors.

ParameterTypeDefaultWhat it is
containerColorColor.UnspecifiedThe panel's fill. The theme's surface.
contentColorColor.UnspecifiedAn option's label and icon. The theme's textPrimary.
selectedContainerColorColor.UnspecifiedThe fill of the chosen option's row, in a select of one choice. The primary role's container. A select of several marks its choices with check boxes, and fills no row.
selectedContentColorColor.UnspecifiedThe chosen option's label, its icon and the tick beside it, in a select of one choice. The primary role's onContainer.
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, and the line between the rows and the field a list is searched by. 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