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
ecomplete
. - 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:

Confira aqui o CodePen para ver um exemplo funcional.
Caso tenha gostado do artigo, considere se tornar um membro do Patreon do autor. 🙂