javascriptreactjsmaterial-uireact-functional-componentuistepper

Material UI StepIcon to have actual icon but keep background circle


I've tried searching stack overflow and haven't found a specific question with this attempt. I am using the styled way to style a Material UI Stepper component. All the examples I see use withStyles, makeStyles, everything but styled for changing colors. But, I also want to have a real icon, not text, in the step labels like below.

Stepper with image

Every time I add an icon component to the icon properties, it just shows the icon and chucks the circle that would normally encompass the text. I'd like to keep the circle and add the icon along with add text to the top. The active/completed would be the gold color and grey for disabled/inactive.

Here's my coding attempts, I'd tried various combinations, stuck the examples from the demos on Material UI website and still haven't had much luck getting icon to show inside a circle. Do I need to just give up on the StepIcon component and wrap the icon in an Avatar component or something?

  const TimelineIcon = styled(StepIcon)(({ theme }) => ({
        root: {
          color: theme.palette.primary.light,
          display: "flex",
          height: 22,
          alignItems: "center",
          "&$active": {
            color: theme.palette.success.main,
          },
          "&$completed": {
            backgroundColor: theme.palette.primary.light,
            color: theme.palette.success.main,
            zIndex: 1,
            fontSize: 18,
          },
        },
      }));

      const stepIconComponent = () => {
        return (
          <div>
            <TimelineIcon icon={<Check />} />
          </div>
        );
      };

    <Stepper
      orientation={"horizontal"}
      alternativeLabel
      style={{ width: "100%" }}
    >
      {stepper.steps.map((step: TimelineStepProps) => {
        return (
          <Step key={step.title}>
            <StepLabel StepIconComponent={stepIconComponent}>
              {step.title}
            </StepLabel>
          </Step>
        );
      })}
    </Stepper>

Solution

  • Due to the design of the StepIcon, it does not apply classes if an actual icon is passed rather than text or number. This forces it to only show the icon and not the circle background with it. Here is the component: implementation of StepIcon

    The code that forces the StepIcon to apply classes only if the "icon" is a number or string is this:

    if (typeof icon === 'number' || typeof icon === 'string') {
        const className = clsx(classes.root, {
          [classes.active]: active,
          [classes.error]: error,
          [classes.completed]: completed,
        });
    

    So what I had to do was recreate the icon with the circle and have two different looks for active or disabled. I also had to style the connector to push it down a little if there was text up top (in the example a number above the circle) or not (since that could be optional in this case)

    const ActiveSvgIcon = styled(SvgIcon)(({ theme }) => ({
        color: theme.palette.success.main,
        "&.MuiSvgIcon-root": {
          width: "1.2em",
          height: "1.2em",
        },
      }));
    
      const DisabledSvgIcon = styled(SvgIcon)(({ theme }) => ({
        color: theme.palette.primary.light,
        "&.MuiSvgIcon-root": {
          width: "1.2em",
          height: "1.2em",
        },
      }));
    
      const getIcon = (step: TimelineStepProps) => {
        return step.active ? (
          <ActiveSvgIcon shapeRendering="circle">
            <circle cx="12" cy="12" r="12" />
            {step.icon}
          </ActiveSvgIcon>
        ) : (
          <DisabledSvgIcon shapeRendering="circle">
            <circle cx="12" cy="12" r="12" />
            {step.icon}
          </DisabledSvgIcon>
        );
      };
    
      const iconStepComponent = (step: TimelineStepProps) => {
        return (
          <div style={{ marginTop: step.superScript === "" ? "20px" : 0 }}>
            <StepperText>{step.superScript}</StepperText>
            {getIcon(step)}
            <StepperTitle>{step.title}</StepperTitle>
            <StepperText>{step.subTitle}</StepperText>
          </div>
        );
      };