Barra de progresso responsiva simples e eficaz

<div
  id="app"
  :class="progressClasses"
>
  <div class="progress__bg"></div>
  
  <template v-for="(step, index) in steps">
    <div :class="stepClasses(index)">
      <div class="progress__indicator">
        <i class="fa fa-check"></i>
      </div>
      <div class="progress__label">
        {{step.label}}
      </div>
    </div>
  </template>
  
  <div class="progress__actions">
    <div
      class="btn"
      v-on:click="nextStep(false)"
    >
      Back
    </div>
    <div
      class="btn"
      v-on:click="nextStep"
    >
      Next
    </div>
    <div>
      Step:
      {{currentStep ? currentStep.label : "Start"}}
    </div>
  </div>
</div>

Para simplificar, o progress__actions que controlam a direção da viagem estão aninhados na própria barra de progresso.

O CSS (SCSS)

É aqui que fazemos o trabalho pesado. As classes definidas aqui serão aplicadas dinamicamente pelo JS com base na etapa atual.

Primeiro, vamos selecionar algumas cores para trabalhar:

$gray:  #E5E5E5;
$gray2: #808080;
$blue:  #2183DD;
$green: #009900;
$white: #FFFFFF;

Agora defina o .progress classe: o contêiner que mantém o conteúdo da barra de progresso junto.

.progress {
  position: absolute;
  top: 15vh;
  width: 0%;
  height: 10px;
  background-color: $blue;
  transition: width .2s;
}

Nossa barra de progresso precisa de um .progress__bg que as etapas de progresso correrão como uma trilha. Ele ficará cinza, coberto pela barra colorida conforme avança para a próxima etapa.

.progress__bg {
  position: absolute;
  width: 100vw;
  height: 10px;
  background-color: $gray;
  z-index: -1;
}

Cada .progress__step contém a etapa circular que será destacada e preenchida conforme a barra de progresso avança.

.progress__step {
  position: absolute;
  top: -8px;
  left: 0;
  display: flex;
  flex-direction: column;
  align-items: center;
  text-align: center;
  
  @for $i from 1 through 5 {
    &.progress__step--#{$i} {
      left: calc(#{$i * 20}vw - 9px);
    }
  }
}

Ele também contém a rodada .progress__indicator e texto do rótulo .progress__label. Seus estilos padrão são definidos fora do .progress__step.

.progress__indicator {
  width: 25px;
  height: 25px;
  border: 2px solid $gray2;
  border-radius: 50%;
  background-color: $white;
  margin-bottom: 10px;
  
  .fa {
    display: none;
    font-size: 16px;
    color: $white;
  }
}

.progress__label {
  position: absolute;
  top: 40px;
}

Vamos agora continuar a aninhar dentro .progress__step novamente e definir a etapa em seu ativo Estado.

&.progress__step--active {
  color: $blue;
  font-weight: 600;
}

Em seguida, defina a etapa em seu completo Estado. Nota: os estilos padrão para .progress__indicator e .progress__label são substituídos quando no estado completo.

&.progress__step--complete {
  .progress__indicator {
    background-color: $green;
    border-color: $blue;
    color: $white;
    display: flex;
    align-items: center;
    justify-content: center;
  }
    
  .progress__indicator .fa {
    display: block;
  }
  
  .progress__label {
    font-weight: 600;
    color: $green;
  }
}

O JavaScript

Conforme mencionado anteriormente, isso será diferente com base em como você implementa a lógica de etapas, o contexto mais amplo em que é implementado, quais estruturas e padrões você usa e assim por diante.

Este exemplo usa um componente Vue para demonstrar:

  • cálculo de classes para a barra de progresso com base no estado atual.
  • cálculo de classes para cada etapa com base no estado atual.

var app = new Vue({
  el: '#app',
  
  data: {
    currentStep: null,
    steps: [
      {"label": "one"},
      {"label": "two"},
      {"label": "three"},
      {"label": "complete"}
    ]
  },
  
  methods: {
    nextStep(next=true) {
      const steps = this.steps
      const currentStep = this.currentStep
      const currentIndex = steps.indexOf(currentStep)
      
      // handle back
      if (!next) {
        if (currentStep && currentStep.label === 'complete') {
          return this.currentStep = steps[steps.length - 1]           
        }

        if (steps[currentIndex - 1]) {
          return this.currentStep = steps[currentIndex - 1] 
        }

        return this.currentStep = { "label": "start" }   
      }
      
      // handle next
      if (this.currentStep && this.currentStep.label === 'complete') {
        return this.currentStep = { "label": "start" }
      }
      
      if (steps[currentIndex + 1]) {
        return this.currentStep = steps[currentIndex + 1]
      }

      this.currentStep = { "label": "complete" }   
    },
    
    stepClasses(index) {
      let result = `progress__step progress__step--${index + 1} `
      if (this.currentStep && this.currentStep.label === 'complete' ||
          index < this.steps.indexOf(this.currentStep)) {
        return result += 'progress__step--complete'
      }
      if (index === this.steps.indexOf(this.currentStep)) {
        return result += 'progress__step--active'
      }
      return result
    }
  },
  
  computed: {
     progressClasses() {
      let result="progress "
      if (this.currentStep && this.currentStep.label === 'complete') {
        return result += 'progress--complete'
      }
      return result += `progress--${this.steps.indexOf(this.currentStep) + 1}`
    }
  }
})

Conclusão

No final de tudo, você tem isso:

Confira o CodePen para um exemplo ao vivo.

Se você achar meus artigos úteis, por favor, considere se tornar um membro de meu patreon 🙂

Ou se você só quer me comprar café (eu amo café):