Artigo original: How to Build a Responsive and Dynamic Progress Bar with HTML, CSS, and JavaScript

Há alguns anos, escrevi um artigo curto (em inglês) sobre a criação de uma barra de progresso responsiva. Minha técnica evoluiu com o tempo. Por isso, está na hora de uma atualização.

A maior mudança está no fato de que, agora, os pseudoelementos (before e after) não são mais necessários. O CSS está mais direto ao ponto, o DOM está mais fácil de ler e o projeto ficou mais dinâmico.

Vamos de novo, então.

Nosso objetivo é criar uma barra de progresso responsiva, simples e eficaz, que faça o seguinte:

  • Tenha quatro etapas até a conclusão.
  • Cada etapa tenha um estado default, active e complete.
  • Seja possível avançar de uma etapa para a próxima até a conclusão.

Confira aqui o CodePen para ver um exemplo funcional.

HTML

Para reduzir a redundância e aumentar a possibilidade de reutilização, rastreamos todo o state em um componente do Vue. No DOM, isso gera dinamicamente um número qualquer de etapas exigidas.

Observação: É possível fazer isso em JavaScript (ECMAScript) puro ou com qualquer outro framework de front-end. O uso do Vue é para fins de demonstração.

A barra de progresso usa uma marcação em HTML básica. Nela, temos:

  • um contêiner com as classes computadas na etapa atual: progressClasses
  • uma faixa estática em segundo plano: progress__bg
  • um laço que percorre cada etapa e aplica stepClasses com base na etapa atual.

Cada etapa tem:

  • um progress__indicator, que contém um ícone de verificação o qual fica visível quando a etapa foi concluída.
  • um progress__label, que contém o texto de rótulo daquela etapa.
<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, progress__actions, que controla a direção na qual a barra progride, é aninhada dentro da própria barra de progresso.

CSS (SCSS)

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

Primeiro, vamos selecionar algumas cores com as quais trabalharemos (dois tons de cinza, azul, verde e branco, nessa ordem):

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

Em seguida, definimos a classe .progress: o contêiner que une todo o conteúdo da barra de progresso.

.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 nossas etapas de progresso percorrerão como se fosse uma trilha. A trilha será cinza, mudando de cor (sendo coberta por essa outra cor) à medida que avançamos para uma próxima etapa.

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

Cada .progress__step contém o espaço circular da etapa que ficará em destaque e que se preencherá quando a barra de progresso for avançando.

.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);
    }
  }
}

Elas também contém o .progress__indicator circular e o texto do rótulo, .progress__label. O estilo padrão é definido fora de .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 continuar o aninhamento dentro de .progress__step e definir a etapa em seu estado active (ativo).

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

A seguir, definimos a etapa em seu estado complete (concluído). Observação: os estilos padrão para .progress__indicator e para .progress__label são sobrescritos no estado concluído.

&.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;
  }
}

JavaScript

Como mencionamos antes, esta parte será diferente com base no modo como você implementará a lógica das etapas – dependendo do tamanho do contexto em que ela for implementada, dos frameworks e dos padrões que serão utilizados e assim por diante.

Este exemplo usa um componente do Vue para demonstrar:

  • o cálculo de classes para a barra de progresso com base no estado atual.
  • o 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)
      
      // lidando com o retorno
      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" }   
      }
      
      // lidando com o avanço
      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

Ao final, o que você verá se assemelhará a isto:

progress-1

Confira aqui o CodePen para ver um exemplo funcional.

Caso tenha gostado do artigo, considere se tornar um membro do Patreon do autor. 🙂