The RadioButton component provides a circular selection indicator for choosing one option from a set of mutually exclusive options.
Import
import com.nomanr.lumo.ui.components.RadioButton
import com.nomanr.lumo.ui.components.RadioButtonColors
Component Signature
@Composable
fun RadioButton(
modifier: Modifier = Modifier,
selected: Boolean,
onClick: (() -> Unit)? = null,
enabled: Boolean = true,
colors: RadioButtonColors = RadioButtonDefaults.colors(),
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
content: @Composable (() -> Unit)? = null,
)
Source: RadioButton.kt:35-110
Parameters
modifier
Modifier
default:"Modifier"
Modifier to be applied to the radio button
Whether this radio button is currently selected
onClick
(() -> Unit)?
default:"null"
Callback invoked when the radio button is clicked. If null, button becomes non-interactive
Whether the radio button is enabled and can be selected
colors
RadioButtonColors
default:"RadioButtonDefaults.colors()"
Colors for different radio button states
Source of interactions for hover, press, and focus states
content
@Composable (() -> Unit)?
default:"null"
Optional label content displayed next to the radio button
Examples
var selectedOption by remember { mutableStateOf("Option1") }
Column {
Row(verticalAlignment = Alignment.CenterVertically) {
RadioButton(
selected = selectedOption == "Option1",
onClick = { selectedOption = "Option1" }
)
Text("Option 1")
}
Row(verticalAlignment = Alignment.CenterVertically) {
RadioButton(
selected = selectedOption == "Option2",
onClick = { selectedOption = "Option2" }
)
Text("Option 2")
}
}
The content parameter allows the label to be part of the clickable area:
var selectedOption by remember { mutableStateOf("A") }
Column {
RadioButton(
selected = selectedOption == "A",
onClick = { selectedOption = "A" },
content = { Text("Option A") }
)
RadioButton(
selected = selectedOption == "B",
onClick = { selectedOption = "B" },
content = { Text("Option B") }
)
}
data class RadioOption(val id: String, val label: String)
val options = listOf(
RadioOption("small", "Small"),
RadioOption("medium", "Medium"),
RadioOption("large", "Large")
)
var selectedSize by remember { mutableStateOf(options[0].id) }
Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
Text("Select Size:", style = AppTheme.typography.h4)
options.forEach { option ->
Row(
modifier = Modifier
.fillMaxWidth()
.padding(vertical = 4.dp),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(8.dp)
) {
RadioButton(
selected = selectedSize == option.id,
onClick = { selectedSize = option.id }
)
Text(option.label)
}
}
}
RadioButton(
selected = true,
onClick = null,
enabled = false
)
RadioButton(
selected = false,
onClick = null,
enabled = false
)
Custom Colors
var selected by remember { mutableStateOf(false) }
val customColors = RadioButtonColors(
selectedColor = AppTheme.colors.error,
unselectedColor = AppTheme.colors.error,
disabledSelectedColor = AppTheme.colors.disabled,
disabledUnselectedColor = AppTheme.colors.disabled
)
RadioButton(
selected = selected,
onClick = { selected = !selected },
colors = customColors
)
The RadioButtonColors data class allows customization (RadioButton.kt:132-154):
data class RadioButtonColors(
val selectedColor: Color,
val unselectedColor: Color,
val disabledSelectedColor: Color,
val disabledUnselectedColor: Color,
)
Styling
Default Styling
From RadioButtonDefaults (RadioButton.kt:112-119):
- Radio Button Size: 20.dp
- Unselected Stroke Width: 2.dp
- Selected Stroke Width: 6.dp (creates filled appearance)
- Minimum Interactive Size: 44.dp
- Padding: 2.dp
Animation
- Animation Duration: 100ms (RadioButton.kt:113)
- Stroke width animates between selected and unselected states (RadioButton.kt:44-49)
- Color transitions smoothly using
animateColorAsState (RadioButton.kt:148-149)
Default Colors
From RadioButtonDefaults.colors() (RadioButton.kt:121-129):
- Selected: Primary color
- Unselected: Primary color
- Disabled Selected: Disabled color
- Disabled Unselected: Disabled color
Implementation Details
Visual Representation
The radio button is drawn as a circle with animated stroke width:
- Unselected: Thin stroke (2.dp) creates a circle outline
- Selected: Thick stroke (6.dp) creates a filled circle appearance
- The stroke width animates smoothly between states (RadioButton.kt:44-49)
Click Behavior
The component uses two modifiers for interaction:
- selectable: Applied to the radio button itself for keyboard/accessibility (RadioButton.kt:53-64)
- clickable: Applied to the entire row when content is provided (RadioButton.kt:69-77)
This ensures the entire label is clickable when using the content parameter.
Accessibility
- Minimum interactive size of 44.dp for touch targets (RadioButton.kt:62)
- Automatically assigned
Role.RadioButton semantic property (RadioButton.kt:57, 73)
- Ripple effect on interaction with unbounded radius (RadioButton.kt:59-62)
- Disabled state clearly indicated with reduced opacity colors
- Mutually exclusive selection enforced through state management
Best Practices
Use Cases
- Use radio buttons when users must select exactly one option from a set
- For 2-5 options, display all radio buttons at once
- For more than 5 options, consider using a dropdown menu instead
- Always provide a default selected option
- Radio Button: One selection from multiple options (mutually exclusive)
- Checkbox: Multiple independent selections
Grouping
// Ensure mutual exclusivity through shared state
var selectedOption by remember { mutableStateOf("option1") }
Column {
listOf("option1", "option2", "option3").forEach { option ->
RadioButton(
selected = selectedOption == option,
onClick = { selectedOption = option },
content = { Text(option) }
)
}
}
Source Reference
Full implementation: com.nomanr.lumo.ui.components.RadioButton.kt