diff --git a/static/app/components/guidedSteps/guidedSteps.spec.tsx b/static/app/components/guidedSteps/guidedSteps.spec.tsx
new file mode 100644
index 00000000000000..4d3d629b6dcc91
--- /dev/null
+++ b/static/app/components/guidedSteps/guidedSteps.spec.tsx
@@ -0,0 +1,68 @@
+import {render, screen, userEvent, within} from 'sentry-test/reactTestingLibrary';
+
+import {GuidedSteps} from 'sentry/components/guidedSteps/guidedSteps';
+
+describe('GuidedSteps', function () {
+ it('can navigate through steps and shows previous ones as completed', async function () {
+ render(
+
+
+ This is the first step.
+
+
+
+ This is the second step.
+
+
+
+ This is the third step.
+
+
+
+ );
+
+ expect(screen.getByText('This is the first step.')).toBeInTheDocument();
+ expect(screen.queryByText('This is the second step.')).not.toBeInTheDocument();
+ expect(screen.queryByText('This is the third step.')).not.toBeInTheDocument();
+
+ await userEvent.click(screen.getByText('Next'));
+
+ expect(screen.queryByText('This is the first step.')).not.toBeInTheDocument();
+ expect(screen.getByText('This is the second step.')).toBeInTheDocument();
+ expect(screen.queryByText('This is the third step.')).not.toBeInTheDocument();
+
+ // First step is shown as completed
+ expect(
+ within(screen.getByTestId('guided-step-1')).getByTestId('icon-check-mark')
+ ).toBeInTheDocument();
+ });
+
+ it('starts at the first incomplete step', function () {
+ render(
+
+
+ This is the first step.
+
+
+
+ This is the second step.
+
+
+
+ This is the third step.
+
+
+
+ );
+
+ // First step is shown as completed
+ expect(
+ within(screen.getByTestId('guided-step-1')).getByTestId('icon-check-mark')
+ ).toBeInTheDocument();
+
+ // Second step is shown as active
+ expect(screen.queryByText('This is the first step.')).not.toBeInTheDocument();
+ expect(screen.getByText('This is the second step.')).toBeInTheDocument();
+ expect(screen.queryByText('This is the third step.')).not.toBeInTheDocument();
+ });
+});
diff --git a/static/app/components/guidedSteps/guidedSteps.stories.tsx b/static/app/components/guidedSteps/guidedSteps.stories.tsx
new file mode 100644
index 00000000000000..0e4600a9539a65
--- /dev/null
+++ b/static/app/components/guidedSteps/guidedSteps.stories.tsx
@@ -0,0 +1,114 @@
+import {Fragment} from 'react';
+
+import {Button} from 'sentry/components/button';
+import {
+ GuidedSteps,
+ useGuidedStepsContext,
+} from 'sentry/components/guidedSteps/guidedSteps';
+import JSXNode from 'sentry/components/stories/jsxNode';
+import SizingWindow from 'sentry/components/stories/sizingWindow';
+import storyBook from 'sentry/stories/storyBook';
+
+export default storyBook(GuidedSteps, story => {
+ story('Default', () => (
+
+
+ To create a GuideStep component, you should use as
+ the container and as direct children.
+
+
+ You have complete control over what to render in the step titles and step content.
+ You may use to render the default
+ back/next buttons, but can also render your own.
+
+
+
+
+ This is the first step.
+
+
+
+ This is the second step.
+
+
+
+ This is the third step.
+
+
+
+
+
+ ));
+
+ story('Custom button behavior', () => {
+ function SkipToLastButton() {
+ const {setCurrentStep, totalSteps} = useGuidedStepsContext();
+ return (
+
+
+
+ );
+ }
+
+ return (
+
+
+ A hook is provided to access and control the step state:{' '}
+ useGuidedStepsContext(). This can be used to create step buttons
+ with custom behavior.
+
+
+
+
+ This is the first step.
+
+
+
+ This is the second step.
+
+
+
+ This is the third step.
+
+
+
+
+
+ );
+ });
+
+ story('Controlling completed state', () => {
+ return (
+
+
+ By default, previous steps are considered completed. However, if the completed
+ state is known it can be controlled using the isCompleted property
+ on . The GuidedStep component will begin on
+ the first incomplete step.
+
+
+
+
+ Congrats, you finished the first step!
+
+
+
+ You haven't completed the second step yet, here's how you do it.
+
+
+
+
+
+ You haven't completed the third step yet, here's how you do it.
+
+
+
+
+
+
+
+ );
+ });
+});
diff --git a/static/app/components/guidedSteps/guidedSteps.tsx b/static/app/components/guidedSteps/guidedSteps.tsx
new file mode 100644
index 00000000000000..fe3845c7b7a4c3
--- /dev/null
+++ b/static/app/components/guidedSteps/guidedSteps.tsx
@@ -0,0 +1,221 @@
+import {
+ Children,
+ createContext,
+ isValidElement,
+ useCallback,
+ useContext,
+ useMemo,
+ useState,
+} from 'react';
+import styled from '@emotion/styled';
+
+import {type BaseButtonProps, Button} from 'sentry/components/button';
+import {IconCheckmark} from 'sentry/icons';
+import {t} from 'sentry/locale';
+import {space} from 'sentry/styles/space';
+
+type GuidedStepsProps = {
+ children: React.ReactElement | React.ReactElement[];
+ className?: string;
+ onStepChange?: (step: number) => void;
+};
+
+interface GuidedStepsContextState {
+ currentStep: number;
+ setCurrentStep: (step: number) => void;
+ totalSteps: number;
+}
+
+interface StepProps {
+ children: React.ReactNode;
+ title: string;
+ isCompleted?: boolean;
+ stepNumber?: number;
+}
+
+const GuidedStepsContext = createContext({
+ currentStep: 0,
+ setCurrentStep: () => {},
+ totalSteps: 0,
+});
+
+export function useGuidedStepsContext() {
+ return useContext(GuidedStepsContext);
+}
+
+function Step({
+ stepNumber = 1,
+ title,
+ children,
+ isCompleted: completedOverride,
+}: StepProps) {
+ const {currentStep} = useGuidedStepsContext();
+ const isActive = currentStep === stepNumber;
+ const isCompleted = completedOverride ?? currentStep > stepNumber;
+
+ return (
+
+ {stepNumber}
+