diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/platform/components/coachmark/CoachMarkContainer.kt b/app/src/main/java/com/x8bit/bitwarden/ui/platform/components/coachmark/CoachMarkContainer.kt
new file mode 100644
index 000000000..b172f6e76
--- /dev/null
+++ b/app/src/main/java/com/x8bit/bitwarden/ui/platform/components/coachmark/CoachMarkContainer.kt
@@ -0,0 +1,246 @@
+package com.x8bit.bitwarden.ui.platform.components.coachmark
+
+import androidx.compose.foundation.background
+import androidx.compose.foundation.gestures.detectTapGestures
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.rememberCoroutineScope
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.drawBehind
+import androidx.compose.ui.geometry.CornerRadius
+import androidx.compose.ui.geometry.Rect
+import androidx.compose.ui.geometry.RoundRect
+import androidx.compose.ui.graphics.ClipOp
+import androidx.compose.ui.graphics.Path
+import androidx.compose.ui.graphics.drawscope.clipPath
+import androidx.compose.ui.input.pointer.pointerInput
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+import com.x8bit.bitwarden.R
+import com.x8bit.bitwarden.ui.platform.components.button.BitwardenStandardIconButton
+import com.x8bit.bitwarden.ui.platform.components.text.BitwardenClickableText
+import com.x8bit.bitwarden.ui.platform.components.util.rememberVectorPainter
+import com.x8bit.bitwarden.ui.platform.theme.BitwardenTheme
+import kotlinx.coroutines.launch
+
+private const val ROUNDED_RECT_RADIUS = 8f
+
+/**
+ * A composable container that manages and displays coach mark highlights.
+ *
+ * This composable provides a full-screen overlay that can highlight specific
+ * areas of the UI and display tooltips to guide the user through a sequence
+ * of steps or features.
+ *
+ * @param T The type of the enum used to represent the unique keys for each coach mark highlight.
+ * @param state The [CoachMarkState] that manages the sequence and state of the coach marks.
+ * @param modifier The modifier to be applied to the container.
+ * @param content The composable content that defines the coach mark highlights within the
+ * [CoachMarkScope].
+ */
+@Composable
+@Suppress("LongMethod")
+fun <T : Enum<T>> CoachMarkContainer(
+    state: CoachMarkState<T>,
+    modifier: Modifier = Modifier,
+    content: @Composable CoachMarkScope<T>.() -> Unit,
+) {
+    val scope = rememberCoroutineScope()
+    Box(
+        modifier = Modifier
+            .fillMaxSize()
+            .then(modifier),
+    ) {
+
+        CoachMarkScopeInstance(coachMarkState = state).content()
+        val boundedRectangle by state.currentHighlightBounds
+        val isVisible by state.isVisible
+        val currentHighlightShape by state.currentHighlightShape
+
+        val highlightPath = remember(boundedRectangle, currentHighlightShape) {
+                if (boundedRectangle == Rect.Zero) {
+                    return@remember Path()
+                }
+                val highlightArea = Rect(
+                    topLeft = boundedRectangle.topLeft,
+                    bottomRight = boundedRectangle.bottomRight,
+                )
+                Path().apply {
+                    when (currentHighlightShape) {
+                        CoachMarkHighlightShape.SQUARE -> addRoundRect(
+                            RoundRect(
+                                rect = highlightArea,
+                                cornerRadius = CornerRadius(
+                                    x = ROUNDED_RECT_RADIUS,
+                                ),
+                            ),
+                        )
+
+                        CoachMarkHighlightShape.OVAL -> addOval(highlightArea)
+                    }
+                }
+            }
+        if (boundedRectangle != Rect.Zero && isVisible) {
+            val backgroundColor = BitwardenTheme.colorScheme.background.scrim
+            Box(
+                modifier = Modifier
+                    .pointerInput(Unit) {
+                        detectTapGestures(
+                            onTap = {
+                                scope.launch {
+                                    state.showToolTipForCurrentCoachMark()
+                                }
+                            },
+                        )
+                    }
+                    .fillMaxSize()
+                    .drawBehind {
+                        clipPath(
+                            path = highlightPath,
+                            clipOp = ClipOp.Difference,
+                            block = {
+                                drawRect(
+                                    color = backgroundColor,
+                                )
+                            },
+                        )
+                    },
+            )
+        }
+        // Once the bounds and shape update show the tooltip for the active coach mark.
+        LaunchedEffect(state.currentHighlightBounds.value, state.currentHighlightShape.value) {
+            if (state.currentHighlightBounds.value != Rect.Zero) {
+                state.showToolTipForCurrentCoachMark()
+            }
+        }
+        // On the initial composition of the screen check to see if the coach mark was visible and
+        // then show the associated coach mark.
+        LaunchedEffect(Unit) {
+            if (state.isVisible.value) {
+                state.currentHighlight.value?.let {
+                    state.showCoachMark(it)
+                }
+            }
+        }
+    }
+}
+
+@Preview
+@Composable
+@Suppress("LongMethod")
+private fun BitwardenCoachMarkContainer_preview() {
+    BitwardenTheme {
+        val state = rememberCoachMarkState(Foo.entries)
+        val scope = rememberCoroutineScope()
+        CoachMarkContainer(
+            state = state,
+        ) {
+            Column(
+                modifier = Modifier
+                    .background(BitwardenTheme.colorScheme.background.primary)
+                    .padding(top = 100.dp)
+                    .padding(horizontal = 16.dp)
+                    .fillMaxSize(),
+            ) {
+
+                BitwardenClickableText(
+                    label = "Start Coach Mark Flow",
+                    onClick = {
+                        scope.launch {
+                            state.showCoachMark(Foo.Bar)
+                        }
+                    },
+                    style = BitwardenTheme.typography.labelLarge,
+                    modifier = Modifier
+                        .padding(bottom = 16.dp)
+                        .align(Alignment.CenterHorizontally),
+                )
+                Spacer(Modifier.height(24.dp))
+                Row(
+                    modifier = Modifier
+                        .align(Alignment.CenterHorizontally)
+                        .fillMaxWidth(),
+                ) {
+                    Spacer(modifier = Modifier.weight(1f))
+                    CoachMarkHighlight(
+                        key = Foo.Bar,
+                        title = "1 of 3",
+                        description = "Use this button to generate a new unique password.",
+                        rightAction = {
+                            BitwardenClickableText(
+                                label = "Next",
+                                onClick = {
+                                    scope.launch {
+                                        state.showNextCoachMark()
+                                    }
+                                },
+                                style = BitwardenTheme.typography.labelLarge,
+                            )
+                        },
+                        shape = CoachMarkHighlightShape.OVAL,
+                    ) {
+                        BitwardenStandardIconButton(
+                            painter = rememberVectorPainter(R.drawable.ic_puzzle),
+                            contentDescription = stringResource(R.string.close),
+                            onClick = {},
+                        )
+                    }
+                }
+                Spacer(Modifier.height(24.dp))
+                CoachMarkHighlight(
+                    key = Foo.Baz,
+                    title = "Foo",
+                    description = "Baz",
+                    leftAction = {
+                        BitwardenClickableText(
+                            label = "Back",
+                            onClick = {
+                                scope.launch {
+                                    state.showPreviousCoachMark()
+                                }
+                            },
+                            style = BitwardenTheme.typography.labelLarge,
+                        )
+                    },
+                    rightAction = {
+                        BitwardenClickableText(
+                            label = "Done",
+                            onClick = {
+                                scope.launch {
+                                    state.coachingComplete()
+                                }
+                            },
+                            style = BitwardenTheme.typography.labelLarge,
+                        )
+                    },
+                ) {
+                    Text(text = "Foo Baz")
+                }
+
+                Spacer(Modifier.size(100.dp))
+            }
+        }
+    }
+}
+
+/**
+ * Example enum for demonstration purposes.
+ */
+private enum class Foo {
+    Bar,
+    Baz,
+}
diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/platform/components/coachmark/CoachMarkScope.kt b/app/src/main/java/com/x8bit/bitwarden/ui/platform/components/coachmark/CoachMarkScope.kt
new file mode 100644
index 000000000..936b66a0f
--- /dev/null
+++ b/app/src/main/java/com/x8bit/bitwarden/ui/platform/components/coachmark/CoachMarkScope.kt
@@ -0,0 +1,125 @@
+package com.x8bit.bitwarden.ui.platform.components.coachmark
+
+import androidx.compose.foundation.layout.BoxScope
+import androidx.compose.foundation.layout.RowScope
+import androidx.compose.foundation.lazy.LazyListScope
+import androidx.compose.foundation.lazy.items
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import com.x8bit.bitwarden.ui.platform.base.util.Text
+import com.x8bit.bitwarden.ui.platform.components.model.CardStyle
+
+/**
+ * Defines the scope for creating coach mark highlights within a user interface.
+ *
+ * This interface provides a way to define and display a highlight that guides the user's
+ * attention to a specific part of the UI, often accompanied by a tooltip with
+ * explanatory text and actions.
+ *
+ * @param T The type of the enum used to represent the unique keys for each coach mark highlight.
+ */
+interface CoachMarkScope<T : Enum<T>> {
+
+    /**
+     * Creates a highlight for a specific coach mark.
+     *
+     * This function defines a region of the UI to be highlighted, along with an
+     * associated tooltip that can display a title, description, and actions.
+     *
+     * @param key The unique key identifying this highlight. This key is used to
+     * manage the state and order of the coach mark sequence.
+     * @param title The title of the coach mark, displayed in the tooltip.
+     * @param description The description of the coach mark, providing more context
+     * to the user. Displayed in the tooltip.
+     * @param shape The shape of the highlight. Defaults to [CoachMarkHighlightShape.SQUARE].
+     * Use [CoachMarkHighlightShape.OVAL] for a circular highlight.
+     * @param onDismiss An optional callback that is invoked when the coach mark is dismissed
+     * (e.g., by clicking the close button). If provided, this function
+     * will be executed after the coach mark is dismissed. If not provided,
+     * no action is taken on dismissal.
+     * @param leftAction An optional composable to be displayed on the left side of the
+     * action row in the tooltip. This can be used to provide
+     * additional actions or controls.
+     * @param rightAction An optional composable to be displayed on the right side of the
+     * action row in the tooltip. This can be used to provide
+     * primary actions or navigation.
+     * @param anchorContent The composable content to be highlighted. This is the UI element
+     * that will be visually emphasized by the coach mark.
+     */
+    @Composable
+    fun CoachMarkHighlight(
+        key: T,
+        title: String,
+        description: String,
+        modifier: Modifier = Modifier,
+        shape: CoachMarkHighlightShape = CoachMarkHighlightShape.SQUARE,
+        onDismiss: (() -> Unit)? = null,
+        leftAction: (@Composable RowScope.() -> Unit)? = null,
+        rightAction: (@Composable RowScope.() -> Unit)? = null,
+        anchorContent: @Composable () -> Unit,
+    )
+
+    /**
+     * Creates a [CoachMarkScope.CoachMarkHighlight] in the context of a [LazyListScope],
+     * automatically assigns the value of [key] as the [LazyListScope.item]'s `key` value.
+     * This is used to be able to find the item to apply the coach mark to in the LazyList.
+     * Analogous with [LazyListScope.item] in the context of adding a coach mark around an entire
+     * item.
+     *
+     * @param key The key used for the CoachMark data as well as the `item.key` to find within
+     * the `LazyList`.
+     *
+     * @see [CoachMarkScope.CoachMarkHighlight]
+     *
+     * Note: If you are only intending "highlight" part of an `item` you will want to give that
+     * item the same `key` as the [key] for the coach mark.
+     */
+    @Suppress("LongParameterList")
+    fun LazyListScope.coachMarkHighlight(
+        key: T,
+        title: Text,
+        description: Text,
+        modifier: Modifier = Modifier,
+        shape: CoachMarkHighlightShape = CoachMarkHighlightShape.SQUARE,
+        onDismiss: (() -> Unit)? = null,
+        leftAction: (@Composable RowScope.() -> Unit)? = null,
+        rightAction: (@Composable RowScope.() -> Unit)? = null,
+        anchorContent: @Composable () -> Unit,
+    )
+
+    /**
+     * Allows for wrapping an entire list of [items] in a single Coach Mark Highlight. The
+     * anchor for the tooltip and the scrolling target will be the start/top of the content.
+     *
+     * @param items Typed list of items to display in the [LazyListScope.items] block.
+     * @param leadingStaticContent Optional static content to slot in a [LazyListScope.item]
+     * ahead of the list of items.
+     * @param leadingContentIsTopCard To denote that the leading content is the "top" part of a
+     * card creating using [CardStyle].
+     * @param trailingStaticContent Optional static content to slot in a [LazyListScope.item]
+     * after the list of items.
+     * @param trailingContentIsBottomCard To denote that the trailing content is the "top" part of
+     * a card creating using [CardStyle].
+     * @param itemContent The content to draw for each [R] in [items] and the necessary
+     * [CardStyle] based on its position and other factors.
+     *
+     * @see [CoachMarkScope.CoachMarkHighlight]
+     */
+    @Suppress("LongParameterList")
+    fun <R> LazyListScope.coachMarkHighlightItems(
+        key: T,
+        title: Text,
+        description: Text,
+        modifier: Modifier = Modifier,
+        shape: CoachMarkHighlightShape = CoachMarkHighlightShape.SQUARE,
+        items: List<R>,
+        onDismiss: (() -> Unit)? = null,
+        leftAction: (@Composable RowScope.() -> Unit)? = null,
+        rightAction: (@Composable RowScope.() -> Unit)? = null,
+        leadingStaticContent: (@Composable BoxScope.() -> Unit)? = null,
+        leadingContentIsTopCard: Boolean = false,
+        trailingStaticContent: (@Composable BoxScope.() -> Unit)? = null,
+        trailingContentIsBottomCard: Boolean = false,
+        itemContent: @Composable (R, CardStyle) -> Unit,
+    )
+}
diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/platform/components/coachmark/CoachMarkScopeInstance.kt b/app/src/main/java/com/x8bit/bitwarden/ui/platform/components/coachmark/CoachMarkScopeInstance.kt
new file mode 100644
index 000000000..6ffc45695
--- /dev/null
+++ b/app/src/main/java/com/x8bit/bitwarden/ui/platform/components/coachmark/CoachMarkScopeInstance.kt
@@ -0,0 +1,362 @@
+package com.x8bit.bitwarden.ui.platform.components.coachmark
+
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.BoxScope
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.RowScope
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.lazy.LazyListScope
+import androidx.compose.foundation.lazy.itemsIndexed
+import androidx.compose.material3.ExperimentalMaterial3Api
+import androidx.compose.material3.RichTooltip
+import androidx.compose.material3.Text
+import androidx.compose.material3.TooltipBox
+import androidx.compose.material3.TooltipDefaults
+import androidx.compose.material3.TooltipScope
+import androidx.compose.material3.TooltipState
+import androidx.compose.material3.rememberTooltipState
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+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.composed
+import androidx.compose.ui.geometry.Rect
+import androidx.compose.ui.layout.boundsInRoot
+import androidx.compose.ui.layout.onGloballyPositioned
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.semantics.SemanticsPropertyKey
+import androidx.compose.ui.semantics.SemanticsPropertyReceiver
+import androidx.compose.ui.semantics.semantics
+import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.DpSize
+import androidx.compose.ui.unit.dp
+import com.x8bit.bitwarden.R
+import com.x8bit.bitwarden.ui.platform.base.util.Text
+import com.x8bit.bitwarden.ui.platform.base.util.toListItemCardStyle
+import com.x8bit.bitwarden.ui.platform.components.button.BitwardenStandardIconButton
+import com.x8bit.bitwarden.ui.platform.components.model.CardStyle
+import com.x8bit.bitwarden.ui.platform.components.util.rememberVectorPainter
+import com.x8bit.bitwarden.ui.platform.theme.BitwardenTheme
+import org.jetbrains.annotations.VisibleForTesting
+
+/**
+ * Creates an instance of [CoachMarkScope] for a given [CoachMarkState].
+ */
+@OptIn(ExperimentalMaterial3Api::class)
+class CoachMarkScopeInstance<T : Enum<T>>(
+    private val coachMarkState: CoachMarkState<T>,
+) : CoachMarkScope<T> {
+
+    @Composable
+    override fun CoachMarkHighlight(
+        key: T,
+        title: String,
+        description: String,
+        modifier: Modifier,
+        shape: CoachMarkHighlightShape,
+        onDismiss: (() -> Unit)?,
+        leftAction: @Composable() (RowScope.() -> Unit)?,
+        rightAction: @Composable() (RowScope.() -> Unit)?,
+        anchorContent: @Composable () -> Unit,
+    ) {
+        val toolTipState = rememberTooltipState(
+            initialIsVisible = false,
+            isPersistent = true,
+        )
+        CoachMarkHighlightInternal(
+            key = key,
+            title = title,
+            description = description,
+            shape = shape,
+            onDismiss = onDismiss,
+            leftAction = leftAction,
+            rightAction = rightAction,
+            toolTipState = toolTipState,
+            modifier = modifier.onGloballyPositioned {
+                coachMarkState.updateHighlight(
+                    key = key,
+                    bounds = it.boundsInRoot(),
+                    toolTipState = toolTipState,
+                    shape = shape,
+                )
+            },
+            anchorContent = anchorContent,
+        )
+    }
+
+    override fun LazyListScope.coachMarkHighlight(
+        key: T,
+        title: Text,
+        description: Text,
+        modifier: Modifier,
+        shape: CoachMarkHighlightShape,
+        onDismiss: (() -> Unit)?,
+        leftAction: @Composable() (RowScope.() -> Unit)?,
+        rightAction: @Composable() (RowScope.() -> Unit)?,
+        anchorContent: @Composable () -> Unit,
+    ) {
+        item(key = key) {
+            this@CoachMarkScopeInstance.CoachMarkHighlight(
+                key = key,
+                title = title(),
+                description = description(),
+                modifier = modifier,
+                shape = shape,
+                onDismiss = onDismiss,
+                leftAction = leftAction,
+                rightAction = rightAction,
+                anchorContent = anchorContent,
+            )
+        }
+    }
+
+    override fun <R> LazyListScope.coachMarkHighlightItems(
+        key: T,
+        title: Text,
+        description: Text,
+        modifier: Modifier,
+        shape: CoachMarkHighlightShape,
+        items: List<R>,
+        onDismiss: (() -> Unit)?,
+        leftAction: @Composable (RowScope.() -> Unit)?,
+        rightAction: @Composable (RowScope.() -> Unit)?,
+        leadingStaticContent: @Composable (BoxScope.() -> Unit)?,
+        leadingContentIsTopCard: Boolean,
+        trailingStaticContent: @Composable (BoxScope.() -> Unit)?,
+        trailingContentIsBottomCard: Boolean,
+        itemContent: @Composable (item: R, cardStyle: CardStyle) -> Unit,
+    ) {
+        val hasLeadingContent = (leadingStaticContent != null)
+        val topCardAlreadyExists = hasLeadingContent && leadingContentIsTopCard
+        val bottomCardAlreadyExists = (trailingStaticContent != null) && trailingContentIsBottomCard
+        item(key = key) {
+            this@CoachMarkScopeInstance.CoachMarkHighlightInternal(
+                key = key,
+                title = title(),
+                description = description(),
+                shape = shape,
+                onDismiss = onDismiss,
+                leftAction = leftAction,
+                rightAction = rightAction,
+            ) {
+                Box(
+                    modifier = modifier.calculateBoundsAndAddForKey(key = key, isFirstItem = true),
+                ) {
+                    leadingStaticContent?.let { it() } ?: run {
+                        if (items.isNotEmpty()) {
+                            itemContent(
+                                items[0],
+                                items.toCoachMarkListItemCardStyle(
+                                    index = 0,
+                                    topCardAlreadyExists = false,
+                                    bottomCardAlreadyExists = bottomCardAlreadyExists,
+                                ),
+                            )
+                        }
+                    }
+                }
+            }
+        }
+        itemsIndexed(items) { index, item ->
+            // if there is no leading content we already added the first item.
+            if (!hasLeadingContent && index == 0) return@itemsIndexed
+            Box(
+                modifier = modifier.calculateBoundsAndAddForKey(key),
+            ) {
+                val cardStyle = items.toCoachMarkListItemCardStyle(
+                    index = index,
+                    topCardAlreadyExists = topCardAlreadyExists,
+                    bottomCardAlreadyExists = bottomCardAlreadyExists,
+                )
+                itemContent(item, cardStyle)
+            }
+        }
+
+        trailingStaticContent?.let {
+            item {
+                Box(
+                    modifier = modifier.calculateBoundsAndAddForKey(key),
+                    content = it,
+                )
+            }
+        }
+    }
+
+    @Composable
+    private fun CoachMarkHighlightInternal(
+        key: T,
+        title: String,
+        description: String,
+        shape: CoachMarkHighlightShape,
+        onDismiss: (() -> Unit)?,
+        leftAction: @Composable() (RowScope.() -> Unit)?,
+        rightAction: @Composable() (RowScope.() -> Unit)?,
+        modifier: Modifier = Modifier,
+        toolTipState: TooltipState = rememberTooltipState(
+            initialIsVisible = false,
+            isPersistent = true,
+        ),
+        anchorContent: @Composable () -> Unit,
+    ) {
+        TooltipBox(
+            positionProvider = TooltipDefaults.rememberRichTooltipPositionProvider(
+                spacingBetweenTooltipAndAnchor = 12.dp,
+            ),
+            tooltip = {
+                CoachMarkToolTip(
+                    title = title,
+                    description = description,
+                    onDismiss = {
+                        coachMarkState.coachingComplete()
+                        onDismiss?.invoke()
+                    },
+                    leftAction = leftAction,
+                    rightAction = rightAction,
+                )
+            },
+            enableUserInput = false,
+            focusable = false,
+            state = toolTipState,
+            modifier = modifier,
+            content = anchorContent,
+        )
+
+        LaunchedEffect(Unit) {
+            coachMarkState.updateHighlight(
+                key = key,
+                bounds = null,
+                toolTipState = toolTipState,
+                shape = shape,
+            )
+        }
+    }
+
+    private fun Modifier.calculateBoundsAndAddForKey(
+        key: T,
+        isFirstItem: Boolean = false,
+    ): Modifier = composed {
+        var bounds by remember {
+            mutableStateOf(Rect.Zero)
+        }
+        LaunchedEffect(bounds) {
+            if (bounds != Rect.Zero) {
+                coachMarkState.addToExistingBounds(
+                    key = key,
+                    isFirstItem = isFirstItem,
+                    additionalBounds = bounds,
+                )
+            }
+        }
+        this.onGloballyPositioned {
+            bounds = it.boundsInRoot()
+        }
+    }
+}
+
+@OptIn(ExperimentalMaterial3Api::class)
+@Composable
+private fun TooltipScope.CoachMarkToolTip(
+    title: String,
+    description: String,
+    onDismiss: (() -> Unit),
+    leftAction: (@Composable RowScope.() -> Unit)?,
+    rightAction: (@Composable RowScope.() -> Unit)?,
+) {
+    RichTooltip(
+        modifier = Modifier
+            .padding(horizontal = 4.dp)
+            .semantics { isCoachMarkToolTip = true },
+        caretSize = DpSize(width = 24.dp, height = 16.dp),
+        title = {
+            Row(
+                modifier = Modifier.fillMaxWidth(),
+            ) {
+                Text(
+                    text = title,
+                    style = BitwardenTheme.typography.eyebrowMedium,
+                    color = BitwardenTheme.colorScheme.text.secondary,
+                )
+                Spacer(modifier = Modifier.weight(1f))
+                BitwardenStandardIconButton(
+                    painter = rememberVectorPainter(R.drawable.ic_close),
+                    contentDescription = stringResource(R.string.close),
+                    onClick = onDismiss,
+                    modifier = Modifier.size(16.dp),
+                )
+            }
+        },
+        action = {
+            Row(
+                Modifier.fillMaxWidth(),
+            ) {
+                leftAction?.invoke(this)
+                Spacer(modifier = Modifier.weight(1f))
+                rightAction?.invoke(this)
+            }
+        },
+        colors = TooltipDefaults.richTooltipColors(
+            containerColor = BitwardenTheme.colorScheme.background.secondary,
+        ),
+    ) {
+        Text(
+            text = description,
+            style = BitwardenTheme.typography.bodyMedium,
+            color = BitwardenTheme.colorScheme.text.primary,
+        )
+    }
+}
+
+/**
+ * Returns the appropriate [CardStyle] based on the current [index] in the list being used
+ * for a coachMarkHighlightItems list.
+ */
+private fun <T> Collection<T>.toCoachMarkListItemCardStyle(
+    index: Int,
+    topCardAlreadyExists: Boolean,
+    bottomCardAlreadyExists: Boolean,
+    hasDivider: Boolean = true,
+    dividerPadding: Dp = 16.dp,
+): CardStyle = when {
+    topCardAlreadyExists && bottomCardAlreadyExists -> {
+        CardStyle.Middle(hasDivider = hasDivider, dividerPadding = dividerPadding)
+    }
+
+    topCardAlreadyExists && !bottomCardAlreadyExists -> {
+        if (this.size == 1) {
+            CardStyle.Bottom
+        } else if (index == this.size - 1) {
+            CardStyle.Bottom
+        } else {
+            CardStyle.Middle(hasDivider = hasDivider, dividerPadding = dividerPadding)
+        }
+    }
+
+    !topCardAlreadyExists && bottomCardAlreadyExists -> {
+        if (this.size == 1) {
+            CardStyle.Top(hasDivider = hasDivider, dividerPadding = dividerPadding)
+        } else if (index == 0) {
+            CardStyle.Top(hasDivider = hasDivider, dividerPadding = dividerPadding)
+        } else {
+            CardStyle.Middle(hasDivider = hasDivider, dividerPadding = dividerPadding)
+        }
+    }
+
+    else -> this.toListItemCardStyle(
+        index = index,
+        hasDivider = hasDivider,
+        dividerPadding = dividerPadding,
+    )
+}
+
+/**
+ * SemanticPropertyKey used for Unit tests where checking if any displayed CoachMarkToolTips
+ */
+@VisibleForTesting
+val IsCoachMarkToolTipKey = SemanticsPropertyKey<Boolean>("IsCoachMarkToolTip")
+private var SemanticsPropertyReceiver.isCoachMarkToolTip by IsCoachMarkToolTipKey
diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/platform/components/coachmark/CoachMarkState.kt b/app/src/main/java/com/x8bit/bitwarden/ui/platform/components/coachmark/CoachMarkState.kt
new file mode 100644
index 000000000..a18385dc0
--- /dev/null
+++ b/app/src/main/java/com/x8bit/bitwarden/ui/platform/components/coachmark/CoachMarkState.kt
@@ -0,0 +1,465 @@
+package com.x8bit.bitwarden.ui.platform.components.coachmark
+
+import androidx.compose.foundation.gestures.Orientation
+import androidx.compose.foundation.gestures.scrollBy
+import androidx.compose.foundation.lazy.LazyListLayoutInfo
+import androidx.compose.foundation.lazy.LazyListState
+import androidx.compose.material3.ExperimentalMaterial3Api
+import androidx.compose.material3.TooltipState
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.State
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.saveable.Saver
+import androidx.compose.runtime.saveable.listSaver
+import androidx.compose.runtime.saveable.rememberSaveable
+import androidx.compose.ui.geometry.Rect
+import timber.log.Timber
+import java.util.concurrent.ConcurrentHashMap
+import kotlin.math.max
+import kotlin.math.min
+
+/**
+ * Represents a highlight within a coach mark sequence.
+ *
+ * @param T The type of the enum key used to identify the highlight.
+ * @property key The unique key identifying this highlight.
+ * @property highlightBounds The rectangular bounds of the area to highlight.
+ * @property toolTipState The state of the tooltip associated with this highlight.
+ * @property shape The shape of the highlight (e.g., square, oval).
+ */
+@OptIn(ExperimentalMaterial3Api::class)
+data class CoachMarkHighlightState<T : Enum<T>>(
+    val key: T,
+    val highlightBounds: Rect?,
+    val toolTipState: TooltipState,
+    val shape: CoachMarkHighlightShape,
+)
+
+/**
+ * Defines the available shapes for a coach mark highlight.
+ */
+enum class CoachMarkHighlightShape {
+    /**
+     * A square-shaped highlight.
+     */
+    SQUARE,
+
+    /**
+     * An oval-shaped highlight.
+     */
+    OVAL,
+}
+
+/**
+ * Manages the state of a coach mark sequence, guiding users through a series of highlights.
+ *
+ * This class handles the ordered list of highlights, the currently active highlight,
+ * and the overall visibility of the coach mark overlay.
+ *
+ * @param T The type of the enum used to represent the coach mark keys.
+ * @property orderedList The ordered list of coach mark keys that define the sequence.
+ * @param initialCoachMarkHighlight The initial coach mark to be highlighted, or null if
+ * none should be highlighted at start.
+ * @param isCoachMarkVisible is any coach mark currently visible.
+ */
+@OptIn(ExperimentalMaterial3Api::class)
+open class CoachMarkState<T : Enum<T>>(
+    val orderedList: List<T>,
+    initialCoachMarkHighlight: T? = null,
+    isCoachMarkVisible: Boolean = false,
+) {
+    private val highlights: MutableMap<T, CoachMarkHighlightState<T>?> = ConcurrentHashMap()
+    private val mutableCurrentHighlight = mutableStateOf(initialCoachMarkHighlight)
+    val currentHighlight: State<T?> = mutableCurrentHighlight
+    private val mutableCurrentHighlightBounds = mutableStateOf(Rect.Zero)
+    val currentHighlightBounds: State<Rect> = mutableCurrentHighlightBounds
+    private val mutableCurrentHighlightShape = mutableStateOf(CoachMarkHighlightShape.SQUARE)
+    val currentHighlightShape: State<CoachMarkHighlightShape> = mutableCurrentHighlightShape
+
+    private val mutableIsVisible = mutableStateOf(isCoachMarkVisible)
+    val isVisible: State<Boolean> = mutableIsVisible
+
+    /**
+     * Updates the highlight information for a given key. If the key matches the current shown
+     * [key] then also update the public state for the highlight bounds and shape.
+     *
+     * @param key The key of the highlight to update.
+     * @param bounds The rectangular bounds of the area to highlight. If null, defaults to
+     * Rect.Zero.
+     * @param toolTipState The state of the tooltip associated with this highlight.
+     * @param shape The shape of the highlight (e.g., square, oval). Defaults to
+     * [CoachMarkHighlightShape.SQUARE].
+     */
+    fun updateHighlight(
+        key: T,
+        bounds: Rect?,
+        toolTipState: TooltipState,
+        shape: CoachMarkHighlightShape = CoachMarkHighlightShape.SQUARE,
+    ) {
+        highlights[key] = CoachMarkHighlightState(
+            key = key,
+            highlightBounds = bounds,
+            toolTipState = toolTipState,
+            shape = shape,
+        ).also {
+            if (key == currentHighlight.value) {
+                updateCoachMarkStateInternal(it)
+            }
+        }
+    }
+
+    /**
+     * For the provided [key] add a new rectangle to any existing bounds unless it is
+     * the first item then it is used as the "starting" rectangle.
+     *
+     * @param key the [CoachMarkHighlightState] to modify.
+     * @param isFirstItem if this new calculation is coming from the "first" or base item.
+     * @param additionalBounds the rectangle to add to the existing bounds.
+     */
+    fun addToExistingBounds(key: T, isFirstItem: Boolean, additionalBounds: Rect) {
+        val highlight = highlights[key]
+        highlight?.let {
+            val newRect = it.highlightBounds?.union(additionalBounds)
+                .takeIf { !isFirstItem } ?: additionalBounds
+            highlights[key] = it.copy(highlightBounds = newRect)
+            if (key == currentHighlight.value) {
+                updateCoachMarkStateInternal(getCurrentHighlight())
+            }
+        }
+    }
+
+    /**
+     * Show the the tooltip for the currently shown tooltip.
+     */
+    suspend fun showToolTipForCurrentCoachMark() {
+        val currentCoachMark = getCurrentHighlight()
+        currentCoachMark?.toolTipState?.show()
+    }
+
+    /**
+     * Indicates that the coach mark associated with the provided key should be shown and
+     * starts that process of updating the state.
+     *
+     * @param coachMarkToShow The key of the coach mark to show.
+     */
+    open suspend fun showCoachMark(coachMarkToShow: T) {
+        // Clean up the previous tooltip if one is showing.
+        if (currentHighlight.value != coachMarkToShow && isVisible.value) {
+            getCurrentHighlight()?.toolTipState?.cleanUp()
+        }
+        mutableCurrentHighlight.value = coachMarkToShow
+        val highlightToShow = getCurrentHighlight()
+        highlightToShow?.let {
+            updateCoachMarkStateInternal(it)
+        }
+    }
+
+    /**
+     * Shows the next highlight in the sequence.
+     * If there is no previous highlight, it will show the first highlight.
+     * If the previous highlight is the last in the list, nothing will happen.
+     */
+    suspend fun showNextCoachMark() {
+        val previousHighlight = getCurrentHighlight()
+        previousHighlight?.toolTipState?.cleanUp()
+        val index = orderedList.indexOf(previousHighlight?.key)
+        if (index < 0 && previousHighlight != null) return
+        mutableCurrentHighlight.value = orderedList.getOrNull(index + 1)
+        mutableCurrentHighlight.value?.let {
+            showCoachMark(it)
+        }
+    }
+
+    /**
+     * Shows the previous coach mark in the sequence.
+     *  If the current highlighted coach mark is the first in the list, the coach mark will
+     * be hidden.
+     */
+    suspend fun showPreviousCoachMark() {
+        val currentHighlight = getCurrentHighlight()
+        currentHighlight?.toolTipState?.cleanUp() ?: return
+        val index = orderedList.indexOf(currentHighlight.key)
+        if (index == 0) {
+            mutableCurrentHighlight.value = null
+            mutableIsVisible.value = false
+            return
+        }
+        mutableCurrentHighlight.value = orderedList.getOrNull(index - 1)
+        mutableCurrentHighlight.value?.let {
+            showCoachMark(it)
+        }
+    }
+
+    /**
+     * Completes the coaching sequence, clearing all highlights and resetting the state.
+     *
+     * @param onComplete An optional callback to invoke once all the other clean up logic has
+     * taken place.
+     */
+    fun coachingComplete(onComplete: (() -> Unit)? = null) {
+        getCurrentHighlight()?.toolTipState?.cleanUp()
+        mutableCurrentHighlight.value = null
+        mutableCurrentHighlightBounds.value = Rect.Zero
+        mutableCurrentHighlightShape.value = CoachMarkHighlightShape.SQUARE
+        mutableIsVisible.value = false
+        onComplete?.invoke()
+    }
+
+    /**
+     * Gets the current highlight information.
+     *
+     * @return The current [CoachMarkHighlightState] or null if no highlight is active.
+     */
+    private fun getCurrentHighlight(): CoachMarkHighlightState<T>? {
+        return currentHighlight.value?.let { highlights[it] }
+    }
+
+    private fun updateCoachMarkStateInternal(highlight: CoachMarkHighlightState<T>?) {
+        mutableIsVisible.value = highlight != null
+        mutableCurrentHighlightShape.value = highlight?.shape ?: CoachMarkHighlightShape.SQUARE
+        if (currentHighlightBounds.value != highlight?.highlightBounds) {
+            mutableCurrentHighlightBounds.value = highlight?.highlightBounds ?: Rect.Zero
+        }
+    }
+
+    /**
+     * Cleans up the tooltip state by dismissing it if visible and calling onDispose.
+     */
+    private fun TooltipState.cleanUp() {
+        if (isVisible) {
+            dismiss()
+        }
+        onDispose()
+    }
+
+    @Suppress("UndocumentedPublicClass")
+    companion object {
+        /**
+         * Creates a [Saver] for [CoachMarkState] to enable saving and restoring its state.
+         *
+         * @return A [Saver] that can save and restore [CoachMarkState].
+         */
+        inline fun <reified T : Enum<T>> saver(): Saver<CoachMarkState<T>, Any> =
+            listSaver(
+                save = { coachMarkState ->
+                    listOf(
+                        coachMarkState.orderedList.map { it.name },
+                        coachMarkState.currentHighlight.value?.name,
+                        coachMarkState.isVisible.value,
+                    )
+                },
+                restore = { restoredList ->
+                    val enumList = restoredList[0] as List<*>
+                    val currentHighlightName = restoredList[1] as String?
+                    val enumValues = enumValues<T>()
+                    val list = enumList.mapNotNull { name ->
+                        enumValues.find { it.name == name }
+                    }
+                    val currentHighlight = currentHighlightName?.let { name ->
+                        enumValues.find { it.name == name }
+                    }
+                    val isVisible = restoredList[2] as Boolean
+                    CoachMarkState(
+                        orderedList = list,
+                        initialCoachMarkHighlight = currentHighlight,
+                        isCoachMarkVisible = isVisible,
+                    )
+                },
+            )
+    }
+}
+
+/**
+ * A [CoachMarkState] that depends on a [LazyListState] to automatically scroll to the current
+ * Coach Mark if not on currently on the screen.
+ */
+class LazyListCoachMarkState<T : Enum<T>>(
+    private val lazyListState: LazyListState,
+    orderedList: List<T>,
+    initialCoachMarkHighlight: T? = null,
+    isCoachMarkVisible: Boolean = false,
+) : CoachMarkState<T>(orderedList, initialCoachMarkHighlight, isCoachMarkVisible) {
+
+    override suspend fun showCoachMark(coachMarkToShow: T) {
+        super.showCoachMark(coachMarkToShow)
+        lazyListState.searchForKey(coachMarkToShow)
+    }
+
+    private suspend fun LazyListState.searchForKey(keyToFind: T): Boolean =
+        layoutInfo.visibleItemsInfo.any { it.key == keyToFind }
+            .takeIf { itemAlreadyVisible ->
+                if (itemAlreadyVisible) {
+                    val offset =
+                        layoutInfo.visibleItemsInfo.find { visItem ->
+
+                            visItem.key == keyToFind
+                        }
+                            ?.offset
+                    when {
+                        offset == null -> Unit
+                        ((layoutInfo.viewportEndOffset - offset) <
+                            END_VIEW_PORT_PIXEL_THRESHOLD) -> {
+                            scrollBy(layoutInfo.quarterViewPortScrollAmount())
+                        }
+
+                        ((offset - layoutInfo.viewportStartOffset) <
+                            START_VIEW_PORT_PIXEL_THRESHOLD) -> {
+                            scrollBy(-(layoutInfo.quarterViewPortScrollAmount()))
+                        }
+
+                        else -> Unit
+                    }
+                }
+                itemAlreadyVisible
+            }
+            ?: scrollUpToKey(keyToFind).takeIf { it }
+            ?: scrollDownToKey(keyToFind)
+
+    private suspend fun LazyListState.scrollUpToKey(
+        targetKey: T,
+    ): Boolean {
+        val scrollAmount = (-1).toFloat()
+        var found = false
+        var keepSearching = true
+        while (keepSearching && !found) {
+            val layoutInfo = this.layoutInfo
+            val visibleItems = layoutInfo.visibleItemsInfo
+            if (visibleItems.any { it.key == targetKey }) {
+                scrollBy(-(layoutInfo.halfViewPortScrollAmount()))
+                found = true
+            } else {
+                if (!canScrollBackward) {
+                    keepSearching = false
+                } else {
+                    this.scrollBy(scrollAmount)
+                }
+            }
+        }
+        Timber.i("$targetKey has been found: $found by scrolling up.")
+        return found
+    }
+
+    private suspend fun LazyListState.scrollDownToKey(
+        targetKey: T,
+    ): Boolean {
+        val scrollAmount = 1.toFloat()
+        var found = false
+        var keepSearching = true
+        while (keepSearching && !found) {
+            val layoutInfo = this.layoutInfo
+            val visibleItems = layoutInfo.visibleItemsInfo
+            if (visibleItems.any { it.key == targetKey }) {
+                scrollBy(layoutInfo.halfViewPortScrollAmount())
+                found = true
+            } else {
+                if (!this.canScrollForward) {
+                    // Reached the end of the list without finding the key
+                    keepSearching = false
+                } else {
+                    this.scrollBy(scrollAmount)
+                }
+            }
+        }
+        Timber.i("$targetKey has been found: $found by scrolling down.")
+        return found
+    }
+
+    @Suppress("MagicNumber")
+    private fun LazyListLayoutInfo.halfViewPortScrollAmount(): Float = when (this.orientation) {
+        Orientation.Vertical -> (viewportSize.height / 2f)
+        Orientation.Horizontal -> (viewportSize.width / 2f)
+    }
+
+    @Suppress("MagicNumber")
+    private fun LazyListLayoutInfo.quarterViewPortScrollAmount(): Float = when (this.orientation) {
+        Orientation.Vertical -> (viewportSize.height / 4f)
+        Orientation.Horizontal -> (viewportSize.width / 4f)
+    }
+
+    @Suppress("UndocumentedPublicClass")
+    companion object {
+        /**
+         * Creates a [Saver] for [CoachMarkState] to enable saving and restoring its state.
+         *
+         * @return A [Saver] that can save and restore [CoachMarkState].
+         */
+        inline fun <reified T : Enum<T>> saver(
+            lazyListState: LazyListState,
+        ): Saver<CoachMarkState<T>, Any> =
+            listSaver(
+                save = { coachMarkState ->
+                    listOf(
+                        coachMarkState.orderedList.map { it.name },
+                        coachMarkState.currentHighlight.value?.name,
+                        coachMarkState.isVisible.value,
+                    )
+                },
+                restore = { restoredList ->
+                    val enumList = restoredList[0] as List<*>
+                    val currentHighlightName = restoredList[1] as String?
+                    val enumValues = enumValues<T>()
+                    val list = enumList.mapNotNull { name ->
+                        enumValues.find { it.name == name }
+                    }
+                    val currentHighlight = currentHighlightName?.let { name ->
+                        enumValues.find { it.name == name }
+                    }
+                    val isVisible = restoredList[2] as Boolean
+                    LazyListCoachMarkState(
+                        lazyListState = lazyListState,
+                        orderedList = list,
+                        initialCoachMarkHighlight = currentHighlight,
+                        isCoachMarkVisible = isVisible,
+                    )
+                },
+            )
+    }
+}
+
+/**
+ * Remembers and saves the state of a [CoachMarkState].
+ *
+ * @param T The type of the enum used to represent the coach mark keys.
+ * @param orderedList The ordered list of coach mark keys.
+ * @return A [CoachMarkState] instance.
+ */
+@Composable
+inline fun <reified T : Enum<T>> rememberCoachMarkState(orderedList: List<T>): CoachMarkState<T> {
+    return rememberSaveable(saver = CoachMarkState.saver<T>()) {
+        CoachMarkState(orderedList)
+    }
+}
+
+/**
+ * Remembers and saves the state of a [LazyListCoachMarkState].
+ *
+ * @param T The type of the enum used to represent the coach mark keys.
+ * @param orderedList The ordered list of coach mark keys.
+ * @param lazyListState The lazy list state to be used by the created instance.
+ * @return A [LazyListCoachMarkState] instance.
+ */
+@Composable
+inline fun <reified T : Enum<T>> rememberLazyListCoachMarkState(
+    orderedList: List<T>,
+    lazyListState: LazyListState,
+): CoachMarkState<T> {
+    return rememberSaveable(saver = LazyListCoachMarkState.saver<T>(lazyListState)) {
+        LazyListCoachMarkState(lazyListState = lazyListState, orderedList = orderedList)
+    }
+}
+
+/**
+ * Combine two [Rect] to create the largest result rectangle between them.
+ * This will include any space between the [Rect] as well.
+ */
+private fun Rect.union(other: Rect): Rect {
+    return Rect(
+        left = min(left, other.left),
+        top = min(top, other.top),
+        right = max(right, other.right),
+        bottom = max(bottom, other.bottom),
+    )
+}
+
+private const val END_VIEW_PORT_PIXEL_THRESHOLD = 150
+private const val START_VIEW_PORT_PIXEL_THRESHOLD = 40