
A Jetpack Compose OutlinedButton is the composable Jetpack reaches for whenever a screen needs an action that doesn't want the visual weight of a filled background — a Cancel button next to a filled Save button, or a secondary choice beside the main call to action. Jetpack's OutlinedButton composable draws a thin border around its content instead of a solid fill, so a Jetpack Compose OutlinedButton stays visible without competing with whatever primary button sits next to it. Any time a screen needs a lower-emphasis action, Jetpack's OutlinedButton composable is the tool built for that job, and Jetpack ships it ready to use out of the box. Jetpack keeps this OutlinedButton style available on every platform Jetpack Compose targets, from phones to tablets.
This blog walks through every parameter the Jetpack Compose OutlinedButton composable exposes — onClick, modifier, enabled, shape, colors, elevation, border, contentPadding, interactionSource, and content — plus how an OutlinedButton pairs with a primary action. Every code sample below is fully self-contained, so you can paste it into an online Kotlin compiler and watch the OutlinedButton composable behave exactly as Jetpack describes it.
The OutlinedButton composable is a clickable Material button that Jetpack Compose draws with a border instead of a filled background. Unlike a plain Button, which fills its whole shape with a solid color, a Jetpack Compose OutlinedButton keeps a transparent interior and lets a thin outline mark its tappable bounds. Jetpack ships the OutlinedButton composable in the material3 package, so a Jetpack Compose OutlinedButton automatically inherits your app's Material color scheme the moment Jetpack draws it.
Here's the simplest Jetpack Compose OutlinedButton example you'll run into, showing Jetpack's OutlinedButton composable at its most basic:
import androidx.compose.material3.OutlinedButton
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
@Composable
fun BasicOutlinedButtonExample() {
OutlinedButton(onClick = { }) {
Text("Cancel")
}
}
Running this Jetpack Compose OutlinedButton example draws a thin bordered rectangle with the word Cancel centered inside it. The Text lives inside the OutlinedButton as its content lambda — Jetpack's OutlinedButton composable itself never draws the label, it only supplies the click behavior, the border, and the shape Jetpack Compose wraps around whatever content you place inside it. This is why a Jetpack Compose OutlinedButton can wrap a Text, an Icon, or a Row of both and still behave consistently every time Jetpack renders it.
Jetpack designed this OutlinedButton so a developer never has to draw that border by hand. An OutlinedButton stays this simple no matter which screen it appears on, because Jetpack keeps the same OutlinedButton behavior everywhere. Jetpack documents the OutlinedButton composable alongside its other Material button variants, so the mental model Jetpack teaches for one bordered button applies the moment Jetpack draws another.
The onClick parameter is the one required argument Jetpack's OutlinedButton composable takes, and it's a lambda that Jetpack Compose invokes every time a user taps inside the button's bordered bounds. Because onClick takes no arguments and returns nothing, a jetpack compose outlinedbutton onclick handler is usually just a short block that flips a piece of state or fires a callback up to a parent composable.
import androidx.compose.material3.OutlinedButton
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
@Composable
fun ClickCounterOutlinedButton() {
var count by remember { mutableIntStateOf(0) }
OutlinedButton(onClick = { count++ }) {
Text("Tapped $count times")
}
}
Each tap on this Jetpack Compose OutlinedButton runs the onClick lambda, which increments the count variable and triggers Jetpack Compose to recompose the Text label. Because onClick fires on every completed tap, this jetpack compose outlinedbutton onclick pattern is the same one Jetpack expects for dismissing a dialog or canceling a form — only the lambda body changes from screen to screen.
Jetpack keeps the OutlinedButton lightweight on purpose, so an OutlinedButton never does more than call the lambda you hand it through onClick. Whether the OutlinedButton sits in a dialog or a form footer, Jetpack routes every tap through that same lambda. Jetpack expects this onClick contract to stay identical across every button it ships, which is why moving from a filled Button to an OutlinedButton never means relearning how Jetpack handles a tap.
The enabled parameter is a Boolean that controls whether a Jetpack Compose OutlinedButton responds to taps at all, defaulting to true. Setting enabled to false on Jetpack's OutlinedButton composable disables the click handler, fades both the border and the content color, and swaps in the disabled variants from the button's ButtonColors so Jetpack Compose gives users a clear visual signal.
import androidx.compose.material3.OutlinedButton
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
@Composable
fun DisableableOutlinedButton(agreedToTerms: Boolean) {
OutlinedButton(
onClick = { },
enabled = agreedToTerms
) {
Text("Continue")
}
}
When agreedToTerms is false, this Jetpack Compose OutlinedButton dims its border and label while ignoring taps, because Jetpack Compose skips the click handler whenever enabled is false. As soon as agreedToTerms flips to true, the OutlinedButton composable snaps back to its enabled colors and responds to onClick again, exactly as Jetpack designed it to. This pattern is common wherever a Jetpack Compose OutlinedButton should only be actionable once a precondition, like an accepted checkbox, is met.
Jetpack applies this disabled styling automatically, so every OutlinedButton dims the same way without extra styling code. Jetpack treats enabled as a first-class signal on every button it ships, so the same Boolean that dims an OutlinedButton also dims a filled Button, and Jetpack never needs a second flag for that behavior.
Jetpack treats the border as the single most defining trait of a Jetpack Compose OutlinedButton, since every other parameter also exists on a plain filled Button. The border parameter is what actually makes a Jetpack Compose OutlinedButton look like an OutlinedButton — it accepts a BorderStroke describing the outline's width and color, defaulting to ButtonDefaults.outlinedButtonBorder. Because border is separate from colors, a Jetpack Compose OutlinedButton lets you change the outline's thickness or hue without touching the content color at all.
import androidx.compose.foundation.BorderStroke
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedButton
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.unit.dp
@Composable
fun ThickBorderOutlinedButton() {
OutlinedButton(
onClick = { },
border = BorderStroke(2.dp, MaterialTheme.colorScheme.error)
) {
Text("Delete Account")
}
}
Passing a BorderStroke of 2.dp in the error color gives this Jetpack Compose OutlinedButton a thicker, red outline that signals a destructive action without filling the whole button red. Because BorderStroke bundles width and color into one object, a Jetpack Compose OutlinedButton only ever needs one border argument, and swapping that single instance is enough to restyle the entire border. The default ButtonDefaults.outlinedButtonBorder already reacts to the enabled parameter on its own, graying the border automatically when a Jetpack Compose OutlinedButton becomes disabled.
Jetpack never forces a fixed border width, so a thin 1.dp outline and a bold 3.dp outline are equally valid. Passing null instead of a BorderStroke removes the outline entirely, though at that point an OutlinedButton starts to look plainer than the outlined style Jetpack intended.
Jetpack lets a Jetpack Compose OutlinedButton style its label and background independently of the border, and that job belongs to the colors parameter. It accepts a ButtonColors object bundling four values a Jetpack Compose OutlinedButton needs: containerColor, contentColor, disabledContainerColor, and disabledContentColor. Jetpack builds this object through ButtonDefaults.outlinedButtonColors(), letting a Jetpack Compose OutlinedButton override only the values it cares about.
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedButton
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
@Composable
fun ColoredOutlinedButton() {
OutlinedButton(
onClick = { },
colors = ButtonDefaults.outlinedButtonColors(
contentColor = MaterialTheme.colorScheme.tertiary,
disabledContentColor = MaterialTheme.colorScheme.outline
)
) {
Text("Learn More")
}
}
This jetpack compose outlinedbutton colors example tints the label with the tertiary color from the current theme while keeping the container transparent, the default containerColor an OutlinedButton starts with. Because ButtonColors is a plain data holder, a Jetpack Compose OutlinedButton can reuse the exact same colors object across many buttons, keeping a whole screen visually consistent without repeating color arguments on every single OutlinedButton. If you do set a containerColor, remember a Jetpack Compose OutlinedButton was designed around a transparent interior, so a strong fill can start to look closer to a filled Button than an OutlinedButton.
Jetpack keeps colors optional on the OutlinedButton, so a plain, unstyled OutlinedButton is always the default starting point. Jetpack pulls every default color straight from MaterialTheme, so an OutlinedButton automatically matches whatever palette Jetpack has configured for the rest of the app.
A Jetpack Compose OutlinedButton doesn't have to stay rectangular, and Jetpack leaves that decision to the shape parameter. It accepts a Shape controlling the outline and the button's silhouette, defaulting to ButtonDefaults.outlinedShape. Because the border traces whatever shape you pass in, changing shape on a Jetpack Compose OutlinedButton reshapes both the outline and the tap target together, unlike colors and border which stay independent of each other.
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.OutlinedButton
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
@Composable
fun RoundedOutlinedButton() {
OutlinedButton(
onClick = { },
shape = RoundedCornerShape(50)
) {
Text("Subscribe")
}
}
Passing RoundedCornerShape(50) turns this Jetpack Compose OutlinedButton into a fully pill-shaped outline, since a 50 percent corner radius rounds both ends into semicircles. A Jetpack Compose OutlinedButton can just as easily take a RoundedCornerShape with a small fixed dp value for gently rounded corners, or a CutCornerShape for angular corners, and Jetpack always redraws the border to match whatever shape it receives. Because shape affects the click region too, an OutlinedButton with sharp corners has a slightly different tappable silhouette than the same OutlinedButton with rounded corners.
Jetpack applies shape consistently across the whole OutlinedButton, so the border, the content clipping, and the ripple all follow that same silhouette. Every Jetpack Compose OutlinedButton on a screen can share one shape value for a uniform look, or each OutlinedButton can define its own. Jetpack reuses this same Shape type across nearly every Material composable it ships, so a shape a developer already knows from styling a Card transfers directly to an OutlinedButton.
Jetpack gives a Jetpack Compose OutlinedButton one more spacing knob beyond shape and border, and that's contentPadding. It accepts a PaddingValues controlling the space between the OutlinedButton's border and its inner content, defaulting to ButtonDefaults.ContentPadding. Because this padding sits inside the border, adjusting contentPadding on a Jetpack Compose OutlinedButton changes how roomy the label feels without moving the button's outer edges.
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.material3.OutlinedButton
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.unit.dp
@Composable
fun RoomyOutlinedButton() {
OutlinedButton(
onClick = { },
contentPadding = PaddingValues(horizontal = 32.dp, vertical = 16.dp)
) {
Text("Get Started")
}
}
This wider PaddingValues gives the Jetpack Compose OutlinedButton extra breathing room around the Get Started label, making the button taller and wider without touching its shape or border thickness. Shrinking contentPadding below the default pulls the border in tight around the content, so a Jetpack Compose OutlinedButton reads as more compact. Jetpack ships a sensible default so most screens never need to touch this parameter, and Jetpack only expects an override for the occasional oversized or undersized OutlinedButton.
A few smaller parameters round out what the Jetpack Compose OutlinedButton composable can do, each useful in narrower situations that don't need a full section of their own.
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Download
import androidx.compose.material3.Icon
import androidx.compose.material3.OutlinedButton
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
@Composable
fun IconAndInteractionOutlinedButton() {
val interactionSource = remember { MutableInteractionSource() }
OutlinedButton(
onClick = { },
interactionSource = interactionSource,
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp)
) {
Icon(imageVector = Icons.Filled.Download, contentDescription = null)
Text("Download File")
}
}
This Jetpack Compose OutlinedButton combines a custom interactionSource with a modifier that stretches it across the available width, while the content lambda places an Icon next to a Text inside the same RowScope. Jetpack leaves elevation at null here on purpose, keeping the OutlinedButton visually flat, exactly the look an outlined style is meant to have. Jetpack rarely asks a developer to touch elevation or interactionSource on an everyday OutlinedButton, but Jetpack still exposes both for the rarer screen that needs finer control.
| Parameter | Type | Default | Purpose |
|---|---|---|---|
| onClick | () -> Unit | required | Runs whenever the Jetpack Compose OutlinedButton is tapped |
| modifier | Modifier | Modifier | Sizes, styles, and positions the OutlinedButton |
| enabled | Boolean | true | Turns click handling and full-color styling on or off |
| shape | Shape | ButtonDefaults.outlinedShape | Controls the outline and tap-target silhouette |
| colors | ButtonColors | ButtonDefaults.outlinedButtonColors() | Sets container and content colors for enabled and disabled states |
| elevation | ButtonElevation? | null | Optional shadow, unused by default on an OutlinedButton |
| border | BorderStroke? | ButtonDefaults.outlinedButtonBorder | Draws the width and color of the outline |
| contentPadding | PaddingValues | ButtonDefaults.ContentPadding | Spaces the content away from the border |
| interactionSource | MutableInteractionSource? | null | Exposes press, hover, and focus events |
| content | @Composable RowScope.() -> Unit | required | The Text, Icon, or Row drawn inside the button |
Reading across this table shows how a Jetpack Compose OutlinedButton is really a regular Button with two ideas layered on: a border that replaces the fill, and elevation that defaults to nothing. Once these ten pieces are familiar, reaching for a Jetpack Compose OutlinedButton becomes a quick, repeatable decision.
Jetpack keeps every OutlinedButton parameter list consistent with its other button composables, which is part of why an OutlinedButton is easy to reach for in a hurry. Jetpack documents these parameters the same way across every release, so an OutlinedButton written today keeps behaving the same way as Jetpack updates the library around it.
A common pattern places a Jetpack Compose OutlinedButton next to a filled Button, giving a screen one primary action and one lower-emphasis secondary action. This combines onClick, enabled, and border into a small confirmation dialog footer built around a Jetpack Compose OutlinedButton.
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Button
import androidx.compose.material3.OutlinedButton
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
@Composable
fun ConfirmationDialogFooter() {
var statusText by remember { mutableStateOf("Waiting for input") }
Row(
modifier = Modifier
.fillMaxWidth()
.padding(16.dp),
horizontalArrangement = Arrangement.End
) {
OutlinedButton(
onClick = { statusText = "Cancelled" },
modifier = Modifier.padding(end = 8.dp)
) {
Text("Cancel")
}
Button(onClick = { statusText = "Confirmed" }) {
Text("Confirm")
}
}
Text(statusText, modifier = Modifier.padding(start = 16.dp))
}
Placing the Jetpack Compose OutlinedButton before the filled Button in this Row pushes the Cancel action into a secondary role, since the bordered OutlinedButton naturally draws less attention than the solid Confirm button beside it. Tapping either button updates the same statusText, but only the OutlinedButton's onClick sets it to Cancelled, keeping the two actions independent. Stacking a Jetpack Compose OutlinedButton next to a Button this way is how most Compose dialogs separate a reversible action from a committing one — see the Material3 button guidelines for more on which action deserves the filled treatment.
Jetpack scales this pattern easily — add a third OutlinedButton for a "Learn More" link and the layout logic stays the same. Jetpack designed this pairing so a screen never has two competing filled buttons fighting for attention, and an OutlinedButton is how Jetpack expects that secondary role to be filled.
This final example pulls several pieces together — onClick, enabled, border, and colors — into one runnable screen built entirely around the Jetpack Compose OutlinedButton composable.
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedButton
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
MaterialTheme {
Surface {
SubscriptionScreen()
}
}
}
}
}
@Composable
fun SubscriptionScreen() {
var isSubscribed by remember { mutableStateOf(false) }
var hasValidEmail by remember { mutableStateOf(true) }
Column(
modifier = Modifier.padding(24.dp),
verticalArrangement = Arrangement.spacedBy(12.dp)
) {
Text(if (isSubscribed) "You're subscribed" else "Not subscribed yet")
OutlinedButton(
onClick = { isSubscribed = true },
enabled = hasValidEmail,
border = BorderStroke(
1.5.dp,
if (isSubscribed) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.outline
),
colors = ButtonDefaults.outlinedButtonColors(
contentColor = MaterialTheme.colorScheme.primary
)
) {
Text("Subscribe to Newsletter")
}
OutlinedButton(
onClick = { isSubscribed = false; hasValidEmail = false },
colors = ButtonDefaults.outlinedButtonColors(
contentColor = MaterialTheme.colorScheme.error
)
) {
Text("Unsubscribe")
}
}
}
Launching this Jetpack Compose OutlinedButton example shows a status line above two OutlinedButton instances, where the Subscribe button's border color switches to primary the moment isSubscribed turns true, and the Unsubscribe button disables the first OutlinedButton by flipping hasValidEmail to false. Every piece Jetpack Compose covers above — onClick, enabled, border, and colors — comes together in this one screen, the kind of flexible, low-emphasis action button a Jetpack Compose OutlinedButton is meant to build.
Jetpack intends every OutlinedButton built this way to feel consistent, whether it's one OutlinedButton in a dialog footer or several scattered across a settings screen. Jetpack built the OutlinedButton composable to be this predictable, so the same onClick, border, and colors knowledge carries over the next time Jetpack's OutlinedButton shows up in a different part of an app.