Stepper

Stepper

Stepper는 번호가 매겨진 단계를 통해 진행 상황을 전달해주며, 위자드(wizard) 같은 워크플로를 제공해요. Stepper는 논리적이고 번호가 매겨진 일련의 단계를 거치면서 진행 상황을 표시해요. 네비게이션에도 사용할 수 있어요. Stepper는 단계가 저장된 후 일시적인 피드백 메시지를 표시할 수도 있어요.

  • 단계의 종류 (Types of Steps): Editable, Non-editable, Mobile, Optional
  • Stepper의 종류 (Types of Steppers): Horizontal, Vertical, Linear, Non-linear

:::info 이 컴포넌트는 더 이상 Material Design 지침에 문서화되어 있지 않지만, Material UI는 계속 지원할 거예요. :::

출처: 문서

본문

소개 (Introduction)

Stepper 컴포넌트는 논리적이고 번호가 매겨진 일련의 단계를 통해 진행 상황을 표시해요. 데스크톱과 모바일 뷰포트 모두를 위해 가로(horizontal)와 세로(vertical) 방향을 지원해요.

Stepper는 관련 컴포넌트 모음으로 구현돼요:

  • Stepper: 단계들의 컨테이너.
  • Step: 시퀀스 안의 개별 단계.
  • Step Label: Step의 레이블.
  • Step Content: Step의 선택적 콘텐츠.
  • Step Button: Step의 선택적 버튼.
  • Step Icon: Step의 선택적 아이콘.
  • Step Connector: Step들 사이의 선택적 커스터마이즈된 커넥터.

기본 (Basics)

import Stepper from '@mui/material/Stepper';
import Step from '@mui/material/Step';
import StepLabel from '@mui/material/StepLabel';

가로 Stepper (Horizontal stepper)

가로 Stepper는 한 단계의 내용이 이전 단계에 의존할 때 이상적이에요. 가로 Stepper에서는 긴 단계 이름을 사용하지 않는 것이 좋아요.

선형 (Linear)

선형(linear) Stepper는 사용자가 단계를 순서대로 완료하도록 해요.

Stepper는 현재 단계 인덱스(0부터 시작)를 activeStep prop으로 전달해 제어할 수 있어요. Stepper 방향은 orientation prop으로 설정해요.

이 예시는 두 번째 Step 컴포넌트에 optional prop을 배치해 선택적 단계를 사용하는 방법도 보여줘요. 선택적 단계를 건너뛸지 여부를 관리하는 것은 여러분의 몫이라는 점을 기억하세요. 특정 단계에 대해 이 결정을 내렸다면, active step 인덱스가 선택적 단계를 지나갔더라도 실제로 완료된 것은 아니라는 것을 나타내기 위해 completed={false}를 설정해야 해요.

import * as React from 'react';
import Box from '@mui/material/Box';
import Stepper from '@mui/material/Stepper';
import Step from '@mui/material/Step';
import StepLabel from '@mui/material/StepLabel';
import Button from '@mui/material/Button';
import Typography from '@mui/material/Typography';

const steps = ['Select campaign settings', 'Create an ad group', 'Create an ad'];

export default function HorizontalLinearStepper() {
  const [activeStep, setActiveStep] = React.useState(0);
  const [skipped, setSkipped] = React.useState(new Set<number>());

  const isStepOptional = React.useCallback((step: number) => {
    return step === 1;
  }, []);

  const isStepSkipped = (step: number) => {
    return skipped.has(step);
  };

  const handleNext = () => {
    let newSkipped = skipped;
    if (isStepSkipped(activeStep)) {
      newSkipped = new Set(newSkipped.values());
      newSkipped.delete(activeStep);
    }

    setActiveStep((prevActiveStep) => prevActiveStep + 1);
    setSkipped(newSkipped);
  };

  const handleBack = () => {
    setActiveStep((prevActiveStep) => prevActiveStep - 1);
  };

  const handleSkip = () => {
    if (!isStepOptional(activeStep)) {
      // You probably want to guard against something like this,
      // it should never occur unless someone's actively trying to break something.
      throw new Error("You can't skip a step that isn't optional.");
    }

    setActiveStep((prevActiveStep) => prevActiveStep + 1);
    setSkipped((prevSkipped) => {
      const newSkipped = new Set(prevSkipped.values());
      newSkipped.add(activeStep);
      return newSkipped;
    });
  };

  const handleReset = () => {
    setActiveStep(0);
  };

  const previousActiveStepRef = React.useRef(activeStep);
  const resetButtonRef = React.useRef<HTMLButtonElement>(null);
  const nextButtonRef = React.useRef<HTMLButtonElement>(null);

  // Manage focus when the active step changes.
  React.useEffect(() => {
    const previousActiveStep = previousActiveStepRef.current;
    previousActiveStepRef.current = activeStep;

    if (activeStep === steps.length) {
      // If the user has completed all steps and hits "Finish", focus the "Reset" button.
      resetButtonRef.current!.focus();
      return;
    }
    if (activeStep === 0 && previousActiveStep === steps.length) {
      // If the user has completed all steps and hits "Reset", focus the "Next" button.
      nextButtonRef.current!.focus();
      return;
    }
    if (isStepOptional(previousActiveStep) && !isStepOptional(activeStep)) {
      // If the user hits "Skip" and the next step is not optional, focus the "Next" button.
      nextButtonRef.current!.focus();
    }
  }, [activeStep, isStepOptional]);

  return (
    <Box sx={{ width: '100%' }}>
      <Stepper activeStep={activeStep}>
        {steps.map((label, index) => {
          const stepProps: { completed?: boolean } = {};
          const labelProps: {
            optional?: React.ReactNode;
          } = {};
          if (isStepOptional(index)) {
            labelProps.optional = (
              <Typography variant="caption">Optional</Typography>
            );
          }
          if (isStepSkipped(index)) {
            stepProps.completed = false;
          }
          return (
            <Step key={label} {...stepProps}>
              <StepLabel {...labelProps}>{label}</StepLabel>
            </Step>
          );
        })}
      </Stepper>
      {activeStep === steps.length ? (
        <React.Fragment>
          <Typography sx={{ mt: 2, mb: 1 }}>
            All steps completed - you&apos;re finished
          </Typography>
          <Box sx={{ display: 'flex', flexDirection: 'row', pt: 2 }}>
            <Box sx={{ flex: '1 1 auto' }} />
            <Button onClick={handleReset} ref={resetButtonRef}>
              Reset
            </Button>
          </Box>
        </React.Fragment>
      ) : (
        <React.Fragment>
          <Typography sx={{ mt: 2, mb: 1 }}>Step {activeStep + 1}</Typography>
          <Box sx={{ display: 'flex', flexDirection: 'row', pt: 2 }}>
            <Button
              color="inherit"
              disabled={activeStep === 0}
              onClick={handleBack}
              sx={{ mr: 1 }}
            >
              Back
            </Button>
            <Box sx={{ flex: '1 1 auto' }} />
            {isStepOptional(activeStep) && (
              <Button color="inherit" onClick={handleSkip} sx={{ mr: 1 }}>
                Skip
              </Button>
            )}
            <Button onClick={handleNext} ref={nextButtonRef}>
              {activeStep === steps.length - 1 ? 'Finish' : 'Next'}
            </Button>
          </Box>
        </React.Fragment>
      )}
    </Box>
  );
}

비선형 (Non-linear)

비선형(non-linear) Stepper는 사용자가 다중 단계 흐름에 어느 지점에서든 들어올 수 있게 해요.

이 예시는 일반 가로 Stepper와 비슷하지만, activeStep prop에 따라 단계가 더 이상 자동으로 disabled={true}로 설정되지 않아요.

여기서 StepButton을 사용하는 것은 클릭 가능한 단계 레이블과 completed 플래그 설정을 보여주기 위해서예요. 하지만 단계에 비선형 방식으로 접근할 수 있기 때문에, 모든 단계가 언제 완료되는지(또는 완료해야 하는지조차도) 결정하는 것은 여러분의 구현 몫이에요.

액셔너블(actionable) 단계는 섹션의 콘텐츠 업데이트를 제어한다는 뜻이에요. 접근성 관점에서 보면, 각 StepButton은 콘텐츠 섹션 요소를 가리키는 aria-controls 속성이 필요하다는 것을 의미해요.

import * as React from 'react';
import Box from '@mui/material/Box';
import Stepper from '@mui/material/Stepper';
import Step from '@mui/material/Step';
import StepButton from '@mui/material/StepButton';
import Button from '@mui/material/Button';
import Typography from '@mui/material/Typography';

const steps = ['Select campaign settings', 'Create an ad group', 'Create an ad'];

export default function HorizontalNonLinearStepper() {
  const [activeStep, setActiveStep] = React.useState(0);
  const [completed, setCompleted] = React.useState<{
    [k: number]: boolean;
  }>({});

  const totalSteps = steps.length;
  const completedSteps = Object.keys(completed).length;
  const isLastStep = activeStep === totalSteps - 1;
  const allStepsCompleted = completedSteps === totalSteps;

  const handleNext = () => {
    const newActiveStep =
      isLastStep && !allStepsCompleted
        ? // It's the last step, but not all steps have been completed,
          // find the first step that has not been completed
          steps.findIndex((_step, i) => !(i in completed))
        : activeStep + 1;
    setActiveStep(newActiveStep);
  };

  const handleBack = () => {
    setActiveStep((prevActiveStep) => prevActiveStep - 1);
  };

  const handleStep = (step: number) => () => {
    setActiveStep(step);
  };

  const handleComplete = () => {
    setCompleted({
      ...completed,
      [activeStep]: true,
    });
    handleNext();
  };

  const handleReset = () => {
    setActiveStep(0);
    setCompleted({});
  };

  const resetButtonRef = React.useRef<HTMLButtonElement>(null);
  const nextButtonRef = React.useRef<HTMLButtonElement>(null);
  const previousActiveStepRef = React.useRef(activeStep);
  const previousCompletedRef = React.useRef(completed);

  // Manage focus when the completed steps change.
  React.useEffect(() => {
    const previousCompleted = previousCompletedRef.current;
    previousCompletedRef.current = completed;

    if (allStepsCompleted) {
      // If the user has completed all steps and hits "Finish", focus the "Reset" button.
      resetButtonRef.current!.focus();
      return;
    }

    if (
      Object.keys(completed).length === 0 &&
      Object.keys(previousCompleted).length !== 0
    ) {
      // If the user has completed all steps and hits "Reset", focus the "Next" button.
      nextButtonRef.current!.focus();
    }
  }, [completed, allStepsCompleted]);

  // Manage focus when the active step changes.
  React.useEffect(() => {
    if (activeStep === 0 && previousActiveStepRef.current === 1) {
      // If the user navigated to first step via "Back" button, focus the "Next" button.
      nextButtonRef.current!.focus();
    }

    previousActiveStepRef.current = activeStep;
  }, [activeStep]);

  return (
    <Box sx={{ width: '100%' }}>
      <Stepper nonLinear activeStep={activeStep}>
        {steps.map((label, index) => (
          <Step key={label} completed={completed[index]}>
            <StepButton
              aria-controls="stepper-content"
              color="inherit"
              onClick={handleStep(index)}
            >
              {label}
            </StepButton>
          </Step>
        ))}
      </Stepper>
      <div id="stepper-content">
        {allStepsCompleted ? (
          <React.Fragment>
            <Typography sx={{ mt: 2, mb: 1 }}>
              All steps completed - you&apos;re finished
            </Typography>
            <Box sx={{ display: 'flex', flexDirection: 'row', pt: 2 }}>
              <Box sx={{ flex: '1 1 auto' }} />
              <Button onClick={handleReset} ref={resetButtonRef}>
                Reset
              </Button>
            </Box>
          </React.Fragment>
        ) : (
          <React.Fragment>
            <Typography sx={{ mt: 2, mb: 1, py: 1 }}>
              Step {activeStep + 1}
            </Typography>
            <Box sx={{ display: 'flex', flexDirection: 'row', pt: 2 }}>
              <Button
                color="inherit"
                disabled={activeStep === 0}
                onClick={handleBack}
                sx={{ mr: 1 }}
              >
                Back
              </Button>
              <Box sx={{ flex: '1 1 auto' }} />
              <Button onClick={handleNext} sx={{ mr: 1 }} ref={nextButtonRef}>
                Next
              </Button>
              {activeStep !== steps.length &&
                (completed[activeStep] ? (
                  <Typography variant="caption" sx={{ display: 'inline-block' }}>
                    Step {activeStep + 1} already completed
                  </Typography>
                ) : (
                  <Button onClick={handleComplete}>
                    {completedSteps === totalSteps - 1 ? 'Finish' : 'Complete Step'}
                  </Button>
                ))}
            </Box>
          </React.Fragment>
        )}
      </div>
    </Box>
  );
}

대체 레이블 (Alternative label)

Stepper 컴포넌트에 alternativeLabel prop을 설정하면 레이블을 단계 아이콘 아래에 배치할 수 있어요.

import Box from '@mui/material/Box';
import Stepper from '@mui/material/Stepper';
import Step from '@mui/material/Step';
import StepLabel from '@mui/material/StepLabel';

const steps = [
  'Select master blaster campaign settings',
  'Create an ad group',
  'Create an ad',
];

export default function HorizontalLinearAlternativeLabelStepper() {
  return (
    <Box sx={{ width: '100%' }}>
      <Stepper activeStep={1} alternativeLabel>
        {steps.map((label) => (
          <Step key={label}>
            <StepLabel>{label}</StepLabel>
          </Step>
        ))}
      </Stepper>
    </Box>
  );
}

오류 단계 (Error step)

import * as React from 'react';
import Box from '@mui/material/Box';
import Stepper from '@mui/material/Stepper';
import Step from '@mui/material/Step';
import StepLabel from '@mui/material/StepLabel';
import Typography from '@mui/material/Typography';

const steps = ['Select campaign settings', 'Create an ad group', 'Create an ad'];

export default function HorizontalStepperWithError() {
  const isStepFailed = (step: number) => {
    return step === 1;
  };

  return (
    <Box sx={{ width: '100%' }}>
      <Stepper activeStep={1}>
        {steps.map((label, index) => {
          const labelProps: {
            optional?: React.ReactNode;
            error?: boolean;
          } = {};
          if (isStepFailed(index)) {
            labelProps.optional = (
              <Typography variant="caption" color="error">
                Alert message
              </Typography>
            );
            labelProps.error = true;
          }

          return (
            <Step key={label}>
              <StepLabel {...labelProps}>{label}</StepLabel>
            </Step>
          );
        })}
      </Stepper>
    </Box>
  );
}

커스터마이즈된 가로 Stepper (Customized horizontal stepper)

컴포넌트를 커스터마이즈하는 예시예요. 이에 대해 더 배우려면 overrides 문서 페이지를 참고하세요.

import * as React from 'react';
import { styled } from '@mui/material/styles';
import Stack from '@mui/material/Stack';
import Stepper from '@mui/material/Stepper';
import Step from '@mui/material/Step';
import StepLabel from '@mui/material/StepLabel';
import Check from '@mui/icons-material/Check';
import SettingsIcon from '@mui/icons-material/Settings';
import GroupAddIcon from '@mui/icons-material/GroupAdd';
import VideoLabelIcon from '@mui/icons-material/VideoLabel';
import StepConnector, { stepConnectorClasses } from '@mui/material/StepConnector';
import { StepIconProps } from '@mui/material/StepIcon';

const QontoConnector = styled(StepConnector)(({ theme }) => ({
  [`&.${stepConnectorClasses.alternativeLabel}`]: {
    top: 10,
    left: 'calc(-50% + 16px)',
    right: 'calc(50% + 16px)',
  },
  [`&.${stepConnectorClasses.active}`]: {
    [`& .${stepConnectorClasses.line}`]: {
      borderColor: '#784af4',
    },
  },
  [`&.${stepConnectorClasses.completed}`]: {
    [`& .${stepConnectorClasses.line}`]: {
      borderColor: '#784af4',
    },
  },
  [`& .${stepConnectorClasses.line}`]: {
    borderColor: '#eaeaf0',
    borderTopWidth: 3,
    borderRadius: 1,
    ...theme.applyStyles('dark', {
      borderColor: theme.palette.grey[800],
    }),
  },
}));

const QontoStepIconRoot = styled('div')<{ ownerState: { active?: boolean } }>(
  ({ theme }) => ({
    color: '#eaeaf0',
    display: 'flex',
    height: 22,
    alignItems: 'center',
    '& .QontoStepIcon-completedIcon': {
      color: '#784af4',
      zIndex: 1,
      fontSize: 18,
    },
    '& .QontoStepIcon-circle': {
      width: 8,
      height: 8,
      borderRadius: '50%',
      backgroundColor: 'currentColor',
    },
    ...theme.applyStyles('dark', {
      color: theme.palette.grey[700],
    }),
    variants: [
      {
        props: ({ ownerState }) => ownerState.active,
        style: {
          color: '#784af4',
        },
      },
    ],
  }),
);

function QontoStepIcon(props: StepIconProps) {
  const { active, completed, className } = props;

  return (
    <QontoStepIconRoot ownerState={{ active }} className={className}>
      {completed ? (
        <Check className="QontoStepIcon-completedIcon" />
      ) : (
        <div className="QontoStepIcon-circle" />
      )}
    </QontoStepIconRoot>
  );
}

const ColorlibConnector = styled(StepConnector)(({ theme }) => ({
  [`&.${stepConnectorClasses.alternativeLabel}`]: {
    top: 22,
  },
  [`&.${stepConnectorClasses.active}`]: {
    [`& .${stepConnectorClasses.line}`]: {
      backgroundImage:
        'linear-gradient( 95deg,rgb(242,113,33) 0%,rgb(233,64,87) 50%,rgb(138,35,135) 100%)',
    },
  },
  [`&.${stepConnectorClasses.completed}`]: {
    [`& .${stepConnectorClasses.line}`]: {
      backgroundImage:
        'linear-gradient( 95deg,rgb(242,113,33) 0%,rgb(233,64,87) 50%,rgb(138,35,135) 100%)',
    },
  },
  [`& .${stepConnectorClasses.line}`]: {
    height: 3,
    border: 0,
    backgroundColor: '#eaeaf0',
    borderRadius: 1,
    ...theme.applyStyles('dark', {
      backgroundColor: theme.palette.grey[800],
    }),
  },
}));

const ColorlibStepIconRoot = styled('div')<{
  ownerState: { completed?: boolean; active?: boolean };
}>(({ theme }) => ({
  backgroundColor: '#ccc',
  zIndex: 1,
  color: '#fff',
  width: 50,
  height: 50,
  display: 'flex',
  borderRadius: '50%',
  justifyContent: 'center',
  alignItems: 'center',
  ...theme.applyStyles('dark', {
    backgroundColor: theme.palette.grey[700],
  }),
  variants: [
    {
      props: ({ ownerState }) => ownerState.active,
      style: {
        backgroundImage:
          'linear-gradient( 136deg, rgb(242,113,33) 0%, rgb(233,64,87) 50%, rgb(138,35,135) 100%)',
        boxShadow: '0 4px 10px 0 rgba(0,0,0,.25)',
      },
    },
    {
      props: ({ ownerState }) => ownerState.completed,
      style: {
        backgroundImage:
          'linear-gradient( 136deg, rgb(242,113,33) 0%, rgb(233,64,87) 50%, rgb(138,35,135) 100%)',
      },
    },
  ],
}));

function ColorlibStepIcon(props: StepIconProps) {
  const { active, completed, className } = props;

  const icons: { [index: string]: React.ReactElement<unknown> } = {
    1: <SettingsIcon />,
    2: <GroupAddIcon />,
    3: <VideoLabelIcon />,
  };

  return (
    <ColorlibStepIconRoot ownerState={{ completed, active }} className={className}>
      {icons[String(props.icon)]}
    </ColorlibStepIconRoot>
  );
}

const steps = ['Select campaign settings', 'Create an ad group', 'Create an ad'];

export default function CustomizedSteppers() {
  return (
    <Stack sx={{ width: '100%' }} spacing={4}>
      <Stepper alternativeLabel activeStep={1} connector={<QontoConnector />}>
        {steps.map((label) => (
          <Step key={label}>
            <StepLabel slots={{ stepIcon: QontoStepIcon }}>{label}</StepLabel>
          </Step>
        ))}
      </Stepper>
      <Stepper alternativeLabel activeStep={1} connector={<ColorlibConnector />}>
        {steps.map((label) => (
          <Step key={label}>
            <StepLabel slots={{ stepIcon: ColorlibStepIcon }}>{label}</StepLabel>
          </Step>
        ))}
      </Stepper>
    </Stack>
  );
}

세로 Stepper (Vertical stepper)

세로 Stepper는 좁은 화면 크기를 위해 설계됐어요. 모바일에 이상적이죠. 가로 Stepper의 모든 기능을 구현할 수 있어요.

import * as React from 'react';
import Box from '@mui/material/Box';
import Stepper from '@mui/material/Stepper';
import Step from '@mui/material/Step';
import StepLabel from '@mui/material/StepLabel';
import StepContent from '@mui/material/StepContent';
import Button from '@mui/material/Button';
import Paper from '@mui/material/Paper';
import Typography from '@mui/material/Typography';

const steps = [
  {
    label: 'Select campaign settings',
    description: `For each ad campaign that you create, you can control how much
              you're willing to spend on clicks and conversions, which networks
              and geographical locations you want your ads to show on, and more.`,
  },
  {
    label: 'Create an ad group',
    description:
      'An ad group contains one or more ads which target a shared set of keywords.',
  },
  {
    label: 'Create an ad',
    description: `Try out different ad text to see what brings in the most customers,
              and learn how to enhance your ads using features like ad extensions.
              If you run into any problems with your ads, find out how to tell if
              they're running and how to resolve approval issues.`,
  },
];

export default function VerticalLinearStepper() {
  const [activeStep, setActiveStep] = React.useState(0);

  const handleNext = () => {
    setActiveStep((prevActiveStep) => prevActiveStep + 1);
  };

  const handleBack = () => {
    setActiveStep((prevActiveStep) => prevActiveStep - 1);
  };

  const handleReset = () => {
    setActiveStep(0);
  };

  const previousActiveStepRef = React.useRef(activeStep);
  const continueButtonRef = React.useRef<HTMLButtonElement>(null);
  const backButtonRef = React.useRef<HTMLButtonElement>(null);
  const resetButtonRef = React.useRef<HTMLButtonElement>(null);

  // Manage focus when the active step changes.
  React.useEffect(() => {
    const previousActiveStep = previousActiveStepRef.current;
    previousActiveStepRef.current = activeStep;

    // If the user is going forward.
    if (previousActiveStep < activeStep) {
      if (activeStep === steps.length) {
        // If the user has completed all steps and hits "Finish", focus the "Reset" button.
        resetButtonRef.current!.focus();
      } else {
        // Focus the "Continue" button otherwise.
        continueButtonRef.current!.focus();
      }
      return;
    }
    // Otherwise, the user is going back.

    if (activeStep === 0) {
      // If the user hit "Back" on the second step, or hit "Reset", focus the "Continue" button.
      continueButtonRef.current!.focus();
      return;
    }

    // Focus the "Back" button otherwise.
    backButtonRef.current!.focus();
  }, [activeStep]);

  return (
    <Box sx={{ maxWidth: 400 }}>
      <Stepper activeStep={activeStep} orientation="vertical">
        {steps.map((step, index) => (
          <Step key={step.label}>
            <StepLabel
              optional={
                index === steps.length - 1 ? (
                  <Typography variant="caption">Last step</Typography>
                ) : null
              }
            >
              {step.label}
            </StepLabel>
            <StepContent>
              <Typography>{step.description}</Typography>
              <Box sx={{ mb: 2 }}>
                <Button
                  variant="contained"
                  onClick={handleNext}
                  sx={{ mt: 1, mr: 1 }}
                  ref={continueButtonRef}
                >
                  {index === steps.length - 1 ? 'Finish' : 'Continue'}
                </Button>
                {index !== 0 && (
                  <Button
                    onClick={handleBack}
                    sx={{ mt: 1, mr: 1 }}
                    ref={backButtonRef}
                  >
                    Back
                  </Button>
                )}
              </Box>
            </StepContent>
          </Step>
        ))}
      </Stepper>
      {activeStep === steps.length && (
        <Paper square elevation={0} sx={{ p: 3 }}>
          <Typography>All steps completed - you&apos;re finished</Typography>
          <Button onClick={handleReset} sx={{ mt: 1, mr: 1 }} ref={resetButtonRef}>
            Reset
          </Button>
        </Paper>
      )}
    </Box>
  );
}

대체 레이블 (Alternative label)

세로 Stepper 컴포넌트에 alternativeLabel prop을 사용하면 레이블과 콘텐츠의 배치를 뒤집을 수 있어요.

import * as React from 'react';
import Box from '@mui/material/Box';
import Stepper from '@mui/material/Stepper';
import Step from '@mui/material/Step';
import StepLabel from '@mui/material/StepLabel';
import StepContent from '@mui/material/StepContent';
import Button from '@mui/material/Button';
import Paper from '@mui/material/Paper';
import Typography from '@mui/material/Typography';

const steps = [
  {
    label: 'Select campaign settings',
    description: `For each ad campaign that you create, you can control how much
              you're willing to spend on clicks and conversions, which networks
              and geographical locations you want your ads to show on, and more.`,
  },
  {
    label: 'Create an ad group',
    description:
      'An ad group contains one or more ads which target a shared set of keywords.',
  },
  {
    label: 'Create an ad',
    description: `Try out different ad text to see what brings in the most customers,
              and learn how to enhance your ads using features like ad extensions.
              If you run into any problems with your ads, find out how to tell if
              they're running and how to resolve approval issues.`,
  },
];

export default function VerticalLinearAlternativeLabelStepper() {
  const [activeStep, setActiveStep] = React.useState(0);

  const handleNext = () => {
    setActiveStep((prevActiveStep) => prevActiveStep + 1);
  };

  const handleBack = () => {
    setActiveStep((prevActiveStep) => prevActiveStep - 1);
  };

  const handleReset = () => {
    setActiveStep(0);
  };

  return (
    <Box sx={{ maxWidth: 400 }}>
      <Stepper activeStep={activeStep} orientation="vertical" alternativeLabel>
        {steps.map((step, index) => (
          <Step key={step.label}>
            <StepLabel
              optional={
                index === steps.length - 1 ? (
                  <Typography variant="caption">Last step</Typography>
                ) : null
              }
            >
              {step.label}
            </StepLabel>
            <StepContent sx={{ textAlign: 'right' }}>
              <Typography>{step.description}</Typography>
              <Box sx={{ mb: 2 }}>
                <Button
                  variant="contained"
                  onClick={handleNext}
                  sx={{ mt: 1, mr: 1 }}
                >
                  {index === steps.length - 1 ? 'Finish' : 'Continue'}
                </Button>
                <Button
                  disabled={index === 0}
                  onClick={handleBack}
                  sx={{ mt: 1, mr: 1 }}
                >
                  Back
                </Button>
              </Box>
            </StepContent>
          </Step>
        ))}
      </Stepper>
      {activeStep === steps.length && (
        <Paper square elevation={0} sx={{ p: 3 }}>
          <Typography>All steps completed - you&apos;re finished</Typography>
          <Button onClick={handleReset} sx={{ mt: 1, mr: 1 }}>
            Reset
          </Button>
        </Paper>
      )}
    </Box>
  );
}

전환 (Transition)

StepContent는 기본적으로 Collapse를 사용해요. slots.transition과 slotProps.transition을 사용해 다른 전환으로 교체하거나 전환 props를 전달할 수 있어요.

성능 (Performance)

단계의 콘텐츠는 닫히면 언마운트(unmount)돼요. 콘텐츠를 검색 엔진에 노출해야 하거나, 상호작용 반응성을 최적화하면서 모달 안에서 비싼 컴포넌트 트리를 렌더링해야 한다면, 다음을 사용해 단계를 마운트된 상태로 유지하는 것이 좋은 아이디어일 수 있어요:

<StepContent slotProps={{ transition: { unmountOnExit: false } }} />

모바일 Stepper (Mobile stepper)

이 컴포넌트는 모바일 기기에 적합한 컴팩트한 Stepper를 구현해요. 세로 Stepper보다 기능이 더 제한적이에요. 영감은 모바일 단계에서 얻을 수 있어요.

모바일 Stepper는 사용 가능한 단계를 거치면서 진행 상황을 표시하는 세 가지 변형을 지원해요: text, dots, progress.

텍스트 (Text)

현재 단계와 전체 단계 수를 텍스트로 표시해요.

import * as React from 'react';
import Box from '@mui/material/Box';
import { useTheme } from '@mui/material/styles';
import MobileStepper from '@mui/material/MobileStepper';
import Paper from '@mui/material/Paper';
import Typography from '@mui/material/Typography';
import Button from '@mui/material/Button';
import KeyboardArrowLeft from '@mui/icons-material/KeyboardArrowLeft';
import KeyboardArrowRight from '@mui/icons-material/KeyboardArrowRight';

const steps = [
  {
    label: 'Select campaign settings',
    description: `For each ad campaign that you create, you can control how much
              you're willing to spend on clicks and conversions, which networks
              and geographical locations you want your ads to show on, and more.`,
  },
  {
    label: 'Create an ad group',
    description:
      'An ad group contains one or more ads which target a shared set of keywords.',
  },
  {
    label: 'Create an ad',
    description: `Try out different ad text to see what brings in the most customers,
              and learn how to enhance your ads using features like ad extensions.
              If you run into any problems with your ads, find out how to tell if
              they're running and how to resolve approval issues.`,
  },
];

export default function TextMobileStepper() {
  const theme = useTheme();
  const [activeStep, setActiveStep] = React.useState(0);
  const maxSteps = steps.length;

  const handleNext = () => {
    setActiveStep((prevActiveStep) => prevActiveStep + 1);
  };

  const handleBack = () => {
    setActiveStep((prevActiveStep) => prevActiveStep - 1);
  };

  const nextButtonRef = React.useRef<HTMLButtonElement>(null);
  const backButtonRef = React.useRef<HTMLButtonElement>(null);
  const previousActiveStepRef = React.useRef(activeStep);

  // Manage focus when the active step changes.
  React.useEffect(() => {
    const previousActiveStep = previousActiveStepRef.current;
    previousActiveStepRef.current = activeStep;

    if (activeStep === 0 && previousActiveStep === 1) {
      // If the user is going back to the first step, focus the "Next" button.
      nextButtonRef.current!.focus();
      return;
    }

    if (activeStep === maxSteps - 1 && previousActiveStep === maxSteps - 2) {
      // If the user is going to the last step, focus the "Back" button.
      backButtonRef.current!.focus();
    }
  }, [activeStep, maxSteps]);

  return (
    <Box sx={{ maxWidth: 400, flexGrow: 1 }}>
      <Paper
        square
        elevation={0}
        sx={{
          display: 'flex',
          alignItems: 'center',
          height: 50,
          pl: 2,
          bgcolor: 'background.default',
        }}
      >
        <Typography>{steps[activeStep].label}</Typography>
      </Paper>
      <Box sx={{ height: 255, maxWidth: 400, width: '100%', p: 2 }}>
        {steps[activeStep].description}
      </Box>
      <MobileStepper
        variant="text"
        steps={maxSteps}
        position="static"
        activeStep={activeStep}
        nextButton={
          <Button
            size="small"
            onClick={handleNext}
            disabled={activeStep === maxSteps - 1}
            ref={nextButtonRef}
          >
            Next
            {theme.direction === 'rtl' ? (
              <KeyboardArrowLeft />
            ) : (
              <KeyboardArrowRight />
            )}
          </Button>
        }
        backButton={
          <Button
            size="small"
            onClick={handleBack}
            disabled={activeStep === 0}
            ref={backButtonRef}
          >
            {theme.direction === 'rtl' ? (
              <KeyboardArrowRight />
            ) : (
              <KeyboardArrowLeft />
            )}
            Back
          </Button>
        }
      />
    </Box>
  );
}

점 (Dots)

단계 수가 적을 때는 점(dots)을 사용해요.

import * as React from 'react';
import { useTheme } from '@mui/material/styles';
import MobileStepper from '@mui/material/MobileStepper';
import Button from '@mui/material/Button';
import KeyboardArrowLeft from '@mui/icons-material/KeyboardArrowLeft';
import KeyboardArrowRight from '@mui/icons-material/KeyboardArrowRight';

const steps = 6;

export default function DotsMobileStepper() {
  const theme = useTheme();
  const [activeStep, setActiveStep] = React.useState(0);

  const handleNext = () => {
    setActiveStep((prevActiveStep) => prevActiveStep + 1);
  };

  const handleBack = () => {
    setActiveStep((prevActiveStep) => prevActiveStep - 1);
  };

  const nextButtonRef = React.useRef<HTMLButtonElement>(null);
  const backButtonRef = React.useRef<HTMLButtonElement>(null);
  const previousActiveStepRef = React.useRef(activeStep);

  // Manage focus when the active step changes.
  React.useEffect(() => {
    const previousActiveStep = previousActiveStepRef.current;
    previousActiveStepRef.current = activeStep;

    if (activeStep === 0 && previousActiveStep === 1) {
      // If the user is going back to the first step, focus the "Next" button.
      nextButtonRef.current!.focus();
      return;
    }
    if (activeStep === steps - 1 && previousActiveStep === steps - 2) {
      // If the user is going to the last step, focus the "Back" button.
      backButtonRef.current!.focus();
    }
  }, [activeStep]);

  return (
    <MobileStepper
      variant="dots"
      steps={steps}
      position="static"
      activeStep={activeStep}
      sx={{ maxWidth: 400, flexGrow: 1 }}
      slotProps={{
        progress: {
          'aria-label': 'stepper dotted progress',
        },
      }}
      nextButton={
        <Button
          size="small"
          onClick={handleNext}
          disabled={activeStep === 5}
          ref={nextButtonRef}
        >
          Next
          {theme.direction === 'rtl' ? (
            <KeyboardArrowLeft />
          ) : (
            <KeyboardArrowRight />
          )}
        </Button>
      }
      backButton={
        <Button
          size="small"
          onClick={handleBack}
          disabled={activeStep === 0}
          ref={backButtonRef}
        >
          {theme.direction === 'rtl' ? (
            <KeyboardArrowRight />
          ) : (
            <KeyboardArrowLeft />
          )}
          Back
        </Button>
      }
    />
  );
}

진행률 (Progress)

단계가 많거나, 프로세스 중에 (이전 단계에 대한 응답에 따라) 삽입해야 할 단계가 있다면 진행률 표시줄(progress bar)을 사용해요.

import * as React from 'react';
import { useTheme } from '@mui/material/styles';
import MobileStepper from '@mui/material/MobileStepper';
import Button from '@mui/material/Button';
import KeyboardArrowLeft from '@mui/icons-material/KeyboardArrowLeft';
import KeyboardArrowRight from '@mui/icons-material/KeyboardArrowRight';

export default function ProgressMobileStepper() {
  const theme = useTheme();
  const [activeStep, setActiveStep] = React.useState(0);

  const handleNext = () => {
    setActiveStep((prevActiveStep) => prevActiveStep + 1);
  };

  const handleBack = () => {
    setActiveStep((prevActiveStep) => prevActiveStep - 1);
  };

  const nextButtonRef = React.useRef<HTMLButtonElement>(null);
  const backButtonRef = React.useRef<HTMLButtonElement>(null);
  const previousActiveStepRef = React.useRef(activeStep);

  // Manage focus when the active step changes.
  React.useEffect(() => {
    const previousActiveStep = previousActiveStepRef.current;

    if (activeStep === 0 && previousActiveStep === 1) {
      // If the user is going back to the first step, focus the "Next" button.
      nextButtonRef.current!.focus();
    } else if (activeStep === 5 && previousActiveStep === 4) {
      // If the user is going to the last step, focus the "Back" button.
      backButtonRef.current!.focus();
    }

    previousActiveStepRef.current = activeStep;
  }, [activeStep]);

  return (
    <MobileStepper
      variant="progress"
      steps={6}
      position="static"
      activeStep={activeStep}
      sx={{ maxWidth: 400, flexGrow: 1 }}
      slotProps={{
        progress: {
          'aria-label': 'stepper linear progress',
        },
      }}
      nextButton={
        <Button
          size="small"
          onClick={handleNext}
          disabled={activeStep === 5}
          ref={nextButtonRef}
        >
          Next
          {theme.direction === 'rtl' ? (
            <KeyboardArrowLeft />
          ) : (
            <KeyboardArrowRight />
          )}
        </Button>
      }
      backButton={
        <Button
          size="small"
          onClick={handleBack}
          disabled={activeStep === 0}
          ref={backButtonRef}
        >
          {theme.direction === 'rtl' ? (
            <KeyboardArrowRight />
          ) : (
            <KeyboardArrowLeft />
          )}
          Back
        </Button>
      }
    />
  );
}

MobileStepper API

데모 (Demos)

이 React 컴포넌트의 사용 예시와 세부 사항은 컴포넌트 데모 페이지를 방문해보세요:

임포트 (Import)

import MobileStepper from '@mui/material/MobileStepper';
// or
import { MobileStepper } from '@mui/material';

Props

Name Type Default Required Description
steps integer - Yes
activeStep integer 0 No
backButton node - No
classes object - No Override or extend the styles applied to the component.
nextButton node - No
position 'bottom' | 'static' | 'top' 'bottom' No
slotProps { dot?: func | object, dots?: func | object, progress?: func | object, root?: func | object } {} No
slots { dot?: elementType, dots?: elementType, progress?: elementType, root?: elementType } {} No
sx Array<func | object | bool> | func | object - No The system prop that allows defining system overrides as well as additional CSS styles.
variant 'dots' | 'progress' | 'text' 'dots' No

Note: The ref is forwarded to the root element.

Any other props supplied will be provided to the root element (Paper).

상속 (Inheritance)

명시적으로 문서화되진 않았지만, Paper 컴포넌트의 props도 MobileStepper에서 사용할 수 있어요.

테마 기본 props (Theme default props)

MuiMobileStepper를 사용하면 테마로 이 컴포넌트의 기본 props를 바꿀 수 있어요.

슬롯 (Slots)

Name Default Class Description
root Paper .MuiMobileStepper-root The component that renders the root slot.
progress LinearProgress .MuiMobileStepper-progress The component that renders the progress slot.
dots 'div' .MuiMobileStepper-dots The component that renders the dots slot.
dot 'div' .MuiMobileStepper-dot The component that renders the dot slot.

CSS 규칙 이름 (Rule name)

Global class Rule name Description
- dotActive Styles applied to a dot if variant="dots" and this is the active step.
- positionBottom Styles applied to the root element if position="bottom".
- positionStatic Styles applied to the root element if position="static".
- positionTop Styles applied to the root element if position="top".

소스 코드 (Source code)

이 페이지에서 원하는 정보를 찾지 못했다면, 더 자세한 내용을 위해 컴포넌트의 구현을 살펴보는 것을 고려해보세요.

Step API

데모 (Demos)

임포트 (Import)

import Step from '@mui/material/Step';
// or
import { Step } from '@mui/material';

Props

Name Type Default Required Description
active bool - No
children node - No
classes object - No Override or extend the styles applied to the component.
completed bool - No
component elementType - No
disabled bool - No
expanded bool false No
index integer - No
last bool - No
sx Array<func | object | bool> | func | object - No The system prop that allows defining system overrides as well as additional CSS styles.

Note: The ref is forwarded to the root element (HTMLLIElement).

Any other props supplied will be provided to the root element (native element).

테마 기본 props (Theme default props)

MuiStep를 사용하면 테마로 이 컴포넌트의 기본 props를 바꿀 수 있어요.

CSS 규칙 이름 (Rule name)

Global class Rule name Description
- alternativeLabel Styles applied to the root element if alternativeLabel={true}.
.Mui-completed - State class applied to the root element if completed={true}.
- horizontal Styles applied to the root element if orientation="horizontal".
- root Styles applied to the root element.
- vertical Styles applied to the root element if orientation="vertical".

소스 코드 (Source code)

StepButton API

데모 (Demos)

임포트 (Import)

import StepButton from '@mui/material/StepButton';
// or
import { StepButton } from '@mui/material';

Props

Name Type Default Required Description
children node - No
classes object - No Override or extend the styles applied to the component.
icon node - No
optional node - No
sx Array<func | object | bool> | func | object - No The system prop that allows defining system overrides as well as additional CSS styles.

Note: The ref is forwarded to the root element (HTMLButtonElement).

Any other props supplied will be provided to the root element (ButtonBase).

상속 (Inheritance)

ButtonBase 컴포넌트의 props도 StepButton에서 사용할 수 있어요.

테마 기본 props (Theme default props)

MuiStepButton를 사용하면 테마로 이 컴포넌트의 기본 props를 바꿀 수 있어요.

CSS 규칙 이름 (Rule name)

Global class Rule name Description
- horizontal Styles applied to the root element if orientation="horizontal".
- root Styles applied to the root element.
- touchRipple Styles applied to the ButtonBase touch-ripple.
- vertical Styles applied to the root element if orientation="vertical".

소스 코드 (Source code)

StepConnector API

데모 (Demos)

임포트 (Import)

import StepConnector from '@mui/material/StepConnector';
// or
import { StepConnector } from '@mui/material';

Props

Name Type Default Required Description
classes object - No Override or extend the styles applied to the component.
sx Array<func | object | bool> | func | object - No The system prop that allows defining system overrides as well as additional CSS styles.

Note: The ref is forwarded to the root element (HTMLDivElement).

Any other props supplied will be provided to the root element (native element).

테마 기본 props (Theme default props)

MuiStepConnector를 사용하면 테마로 이 컴포넌트의 기본 props를 바꿀 수 있어요.

CSS 규칙 이름 (Rule name)

Global class Rule name Description
.Mui-active - State class applied to the root element if active={true}.
- alternativeLabel Styles applied to the root element if alternativeLabel={true}.
.Mui-completed - State class applied to the root element if completed={true}.
.Mui-disabled - State class applied to the root element if disabled={true}.
- horizontal Styles applied to the root element if orientation="horizontal".
- line Styles applied to the line element.
- root Styles applied to the root element.
- vertical Styles applied to the root element if orientation="vertical".

소스 코드 (Source code)

StepContent API

데모 (Demos)

임포트 (Import)

import StepContent from '@mui/material/StepContent';
// or
import { StepContent } from '@mui/material';

Props

Name Type Default Required Description
children node - No
classes object - No Override or extend the styles applied to the component.
slotProps { transition?: func | object } {} No
slots { transition?: elementType } {} No
sx Array<func | object | bool> | func | object - No The system prop that allows defining system overrides as well as additional CSS styles.
transitionDuration 'auto' | number | { appear?: number, enter?: number, exit?: number } 'auto' No

Note: The ref is forwarded to the root element (HTMLDivElement).

Any other props supplied will be provided to the root element (native element).

테마 기본 props (Theme default props)

MuiStepContent를 사용하면 테마로 이 컴포넌트의 기본 props를 바꿀 수 있어요.

슬롯 (Slots)

Name Default Class Description
transition Collapse .MuiStepContent-transition The component that renders the transition slot.
Follow this guide to learn more about the requirements for this component.

CSS 규칙 이름 (Rule name)

Global class Rule name Description
- last Styles applied to the root element if last={true} (controlled by Step).
- root Styles applied to the root element.

소스 코드 (Source code)

StepIcon API

데모 (Demos)

임포트 (Import)

import StepIcon from '@mui/material/StepIcon';
// or
import { StepIcon } from '@mui/material';

Props

Name Type Default Required Description
active bool false No
classes object - No Override or extend the styles applied to the component.
completed bool false No
error bool false No
icon node - No
sx Array<func | object | bool> | func | object - No The system prop that allows defining system overrides as well as additional CSS styles.

Note: The ref is forwarded to the root element (SVGSVGElement).

Any other props supplied will be provided to the root element (SvgIcon).

상속 (Inheritance)

SvgIcon 컴포넌트의 props도 StepIcon에서 사용할 수 있어요.

테마 기본 props (Theme default props)

MuiStepIcon를 사용하면 테마로 이 컴포넌트의 기본 props를 바꿀 수 있어요.

CSS 규칙 이름 (Rule name)

Global class Rule name Description
.Mui-active - State class applied to the root element if active={true}.
.Mui-completed - State class applied to the root element if completed={true}.
.Mui-error - State class applied to the root element if error={true}.
- root Styles applied to the root element.
- text Styles applied to the SVG text element.

소스 코드 (Source code)

StepLabel API

데모 (Demos)

임포트 (Import)

import StepLabel from '@mui/material/StepLabel';
// or
import { StepLabel } from '@mui/material';

Props

Name Type Default Required Description
children node - No
classes object - No Override or extend the styles applied to the component.
error bool false No
icon node - No
optional node - No
slotProps { label?: func | object, root?: func | object, stepIcon?: func | object } {} No
slots { label?: elementType, root?: elementType, stepIcon?: elementType } {} No
sx Array<func | object | bool> | func | object - No The system prop that allows defining system overrides as well as additional CSS styles.

Note: The ref is forwarded to the root element (HTMLSpanElement).

Any other props supplied will be provided to the root element (native element).

테마 기본 props (Theme default props)

MuiStepLabel를 사용하면 테마로 이 컴포넌트의 기본 props를 바꿀 수 있어요.

슬롯 (Slots)

Name Default Class Description
root span .MuiStepLabel-root The component that renders the root.
label span .MuiStepLabel-label The component that renders the label.
stepIcon undefined - The component to render in place of the StepIcon.

CSS 규칙 이름 (Rule name)

Global class Rule name Description
.Mui-active - State class applied to the label element if active={true}.
- alternativeLabel State class applied to the root and icon container and label if alternativeLabel={true}.
.Mui-completed - State class applied to the label element if completed={true}.
.Mui-disabled - State class applied to the root and label elements if disabled={true}.
.Mui-error - State class applied to the root and label elements if error={true}.
- horizontal Styles applied to the root element if orientation="horizontal".
- iconContainer Styles applied to the icon container element.
- labelContainer Styles applied to the container element which wraps label and optional.
- vertical Styles applied to the root element if orientation="vertical".

소스 코드 (Source code)

Stepper API

데모 (Demos)

임포트 (Import)

import Stepper from '@mui/material/Stepper';
// or
import { Stepper } from '@mui/material';

Props

Name Type Default Required Description
activeStep integer 0 No
alternativeLabel bool false No
children node - No
classes object - No Override or extend the styles applied to the component.
component elementType - No
connector element <StepConnector /> No
nonLinear bool false No
orientation 'horizontal' | 'vertical' 'horizontal' No
sx Array<func | object | bool> | func | object - No The system prop that allows defining system overrides as well as additional CSS styles.

Note: The ref is forwarded to the root element (HTMLOListElement).

Any other props supplied will be provided to the root element (native element).

테마 기본 props (Theme default props)

MuiStepper를 사용하면 테마로 이 컴포넌트의 기본 props를 바꿀 수 있어요.

CSS 규칙 이름 (Rule name)

Global class Rule name Description
- alternativeLabel Styles applied to the root element if alternativeLabel={true}.
- horizontal Styles applied to the root element if orientation="horizontal".
- nonLinear Styles applied to the root element if nonLinear={true}.
- root Styles applied to the root element.
- vertical Styles applied to the root element if orientation="vertical".

소스 코드 (Source code)

더 알아보기 (Learn more)