DEV Community

myougaTheAxo
myougaTheAxo

Posted on

Swipe Actions in Compose: SwipeToDismissBox Archive & Delete Patterns

Swipe Actions in Compose: SwipeToDismissBox Archive & Delete Patterns

Swipe-to-dismiss is a powerful UX pattern in mobile apps. Jetpack Compose provides SwipeToDismissBox to implement bidirectional swipe gestures with rich visual feedback.

SwipeToDismissBox Basics

SwipeToDismissBox allows users to swipe an item left or right to trigger actions. Each direction can have different behaviors (e.g., archive on left, delete on right).

var dismissed by remember { mutableStateOf(false) }

if (!dismissed) {
    SwipeToDismissBox(
        modifier = Modifier.fillMaxWidth(),
        state = rememberSwipeToDismissBoxState(),
        backgroundContent = {
            // Background shown during swipe
        },
        content = {
            // Your item content
        },
        onDismissed = { direction ->
            dismissed = true
            when (direction) {
                EndToStart -> { /* Delete */ }
                StartToEnd -> { /* Archive */ }
            }
        }
    )
}
Enter fullscreen mode Exit fullscreen mode

Directional Background Colors

Use animateColorAsState to smoothly transition background colors based on swipe direction:

SwipeToDismissBox(
    modifier = Modifier.fillMaxWidth(),
    state = rememberSwipeToDismissBoxState(),
    backgroundContent = {
        val color by animateColorAsState(
            targetValue = when (state.dismissDirection) {
                EndToStart -> Color.Red      // Delete (right to left)
                StartToEnd -> Color.Blue     // Archive (left to right)
                null -> Color.Gray
            },
            label = "swipe_bg_color"
        )

        Box(
            modifier = Modifier
                .fillMaxSize()
                .background(color),
            contentAlignment = Alignment.Center
        ) {
            Icon(
                imageVector = when (state.dismissDirection) {
                    EndToStart -> Icons.Default.Delete
                    StartToEnd -> Icons.Default.Archive
                    null -> Icons.Default.SwipeLeft
                },
                contentDescription = null,
                tint = Color.White
            )
        }
    },
    content = {
        ListItem(
            headlineContent = { Text("Item Title") },
            supportingContent = { Text("Swipe left to delete, right to archive") }
        )
    }
)
Enter fullscreen mode Exit fullscreen mode

Snackbar Undo Pattern

Provide users with an undo option after dismissal:

val snackbarHostState = remember { SnackbarHostState() }
var dismissedItems by remember { mutableStateOf(emptyList<String>()) }

LaunchedEffect(dismissed) {
    if (dismissed) {
        val result = snackbarHostState.showSnackbar(
            message = "Item archived",
            actionLabel = "Undo"
        )
        if (result == SnackbarResult.ActionPerformed) {
            dismissed = false
            dismissedItems = dismissedItems.dropLast(1)
        }
    }
}

SnackbarHost(hostState = snackbarHostState)
Enter fullscreen mode Exit fullscreen mode

Controlling Swipe Direction

Fine-tune swipe behavior with enableDismissFromStartToEnd and enableDismissFromEndToStart:

SwipeToDismissBox(
    state = rememberSwipeToDismissBoxState(
        positionalThreshold = 0.25f
    ),
    enableDismissFromStartToEnd = true,    // Allow swipe left → right
    enableDismissFromEndToStart = true,    // Allow swipe right → left
    backgroundContent = { /* ... */ },
    content = { /* ... */ }
)
Enter fullscreen mode Exit fullscreen mode

Complete Example: Email List Item

@Composable
fun EmailListItem(
    email: Email,
    onArchive: () -> Unit,
    onDelete: () -> Unit
) {
    var dismissed by remember { mutableStateOf(false) }
    val snackbarHostState = remember { SnackbarHostState() }

    if (!dismissed) {
        SwipeToDismissBox(
            modifier = Modifier.fillMaxWidth(),
            state = rememberSwipeToDismissBoxState(),
            backgroundContent = {
                val color by animateColorAsState(
                    targetValue = when (dismissState.dismissDirection) {
                        EndToStart -> Color(0xFFD32F2F)   // Red (delete)
                        StartToEnd -> Color(0xFF1976D2)   // Blue (archive)
                        null -> Color.Transparent
                    },
                    label = "email_swipe_bg"
                )

                Box(
                    modifier = Modifier
                        .fillMaxSize()
                        .background(color),
                    contentAlignment = Alignment.Center
                ) {
                    Icon(
                        imageVector = when (dismissState.dismissDirection) {
                            EndToStart -> Icons.Default.Delete
                            StartToEnd -> Icons.Default.Archive
                            null -> Icons.Default.SwipeLeft
                        },
                        contentDescription = null,
                        tint = Color.White,
                        modifier = Modifier.size(40.dp)
                    )
                }
            },
            content = {
                Card(
                    modifier = Modifier
                        .fillMaxWidth()
                        .padding(8.dp)
                ) {
                    ListItem(
                        headlineContent = { Text(email.subject, fontWeight = FontWeight.Bold) },
                        supportingContent = { Text(email.preview) },
                        trailingContent = { Text(email.date) }
                    )
                }
            },
            onDismissed = { direction ->
                dismissed = true
                when (direction) {
                    EndToStart -> {
                        onDelete()
                        // Optionally show snackbar for undo
                    }
                    StartToEnd -> {
                        onArchive()
                    }
                }
            }
        )
    }
}
Enter fullscreen mode Exit fullscreen mode

Key Takeaways

  • SwipeToDismissBox provides elegant swipe-dismiss UX with directional control
  • Use animateColorAsState for smooth background color transitions
  • Always provide undo patterns with Snackbar for destructive actions
  • Control swipe directions independently with enableDismissFromStartToEnd and enableDismissFromEndToStart
  • Test on actual devices to ensure swipe sensitivity and responsiveness

${ CTA }

Top comments (0)