Vue.js checkout stepper I have made this small checkout stepper with Vue (v 2.x.x):


var app = new Vue({
  el: "#cart",
  data: {
    stepCounter: 1,
    steps: [{
        step: 1,
        completed: false,
        text: "Cart"
      },
      {
        step: 2,
        completed: false,
        text: "Shipping"
      },
      {
        step: 3,
        completed: false,
        text: "Payment"
      },
      {
        step: 4,
        completed: false,
        text: "Confirmation"
      }
    ]
  },
  methods: {
    doPrev: function() {
      if (this.stepCounter &gt; 1) {
        this.stepCounter--;
        this.doCompleted();
      }
    },
    doNext: function() {
      if (this.stepCounter &lt;= this.steps.length) {
        this.stepCounter++;
        this.doCompleted();
      }
    },
    doCompleted: function() {
      this.steps.forEach(item =&gt; {
        item.completed = item.step &lt; this.stepCounter;
      });
    }
  }
});
* {
  margin: 0;
  padding: 0;
  font-family: "Poppins", sans-serif;
}

.progressbar {
  display: flex;
  list-style-type: none;
  counter-reset: steps;
  padding-top: 50px;
  justify-content: space-between;
}

.progressbar li {
  font-size: 13px;
  text-align: center;
  position: relative;
  flex-grow: 1;
  flex-basis: 0;
  color: rgba(0, 0, 0, 0.5);
  font-weight: 600;
}

.progressbar li.completed {
  color: #ccc;
}

.progressbar li.active {
  color: #4caf50;
}

.progressbar li::after {
  counter-increment: steps;
  content: counter(steps, decimal);
  display: block;
  width: 30px;
  height: 30px;
  line-height: 30px;
  border: 2px solid rgba(0, 0, 0, 0.5);
  background: #fff;
  border-radius: 50%;
  position: absolute;
  left: 50%;
  margin-left: -15px;
  margin-top: -60px;
}

.progressbar li.active::after,
.progressbar li.completed::after {
  background: #4caf50;
  border-color: rgba(0, 0, 0, 0.15);
  color: #fff;
}

.progressbar li.completed::after {
  content: '\2713';
}

.progressbar li::before {
  content: "";
  position: absolute;
  top: -26px;
  left: -50%;
  width: 100%;
  height: 2px;
  background: rgba(0, 0, 0, 0.5);
  z-index: -1;
}

.progressbar li.active::before,
.progressbar li.completed::before,
.progressbar li.active+li::before {
  background: #4caf50;
}

.progressbar li:first-child::before {
  display: none;
}
&lt;script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.6.11/vue.min.js"&gt;&lt;/script&gt;
&lt;link href="https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/4.4.1/css/bootstrap.min.css" rel="stylesheet" /&gt;

&lt;div id="cart" class="mt-2"&gt;
  &lt;div class="container"&gt;
    &lt;ul class="progressbar"&gt;
      &lt;li v-for="(step, index) in steps" v-bind:class="{ active: index + 1 === stepCounter, completed: step.completed === true }"&gt;{{step.text}}&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/div&gt;

  &lt;div class="container px-4 mt-5 text-center"&gt;
    &lt;div class="d-flex justify-content-between"&gt;
      &lt;button type="button" class="btn btn-sm btn-success" v-bind:class="{ disabled : stepCounter === 1}" @click="doPrev()"&gt;Prev&lt;/button&gt;
      &lt;button type="button" class="btn btn-sm btn-success" v-bind:class="{ disabled : stepCounter &gt; steps.length}" @click="doNext()"&gt;Next&lt;/button&gt;
    &lt;/div&gt;
  &lt;/div&gt;
&lt;/div&gt;



Questions

Is there any room for &quot;shortening&quot; the code?
Anything inconsistent at the logic level?
Is there any room for &quot;shortening&quot; the code?

Shorthands
Shorthands can simplify the markup. The click handlers already use the shorthand @click instead of v-on:click. The bindings can be simplified as well. For example - instead of:

v-bind:class=&quot;...&quot;


the v-bind can be omitted:
:class=&quot;...&quot;

Watch out!

Yesterday there was an answer to your TODO app post which suggested using computed properties. With the current architecture using a computed property doesn't seem feasible, but a watcher method could be employed. Instead of setting up a method like doCompleted() that must be called manually, a watcher method can be used to adjust the values whenever that value changes.
watch: {
  stepCounter: function(newValue, oldValue) {
    this.steps.forEach(item =&gt; {
      item.completed = item.step &lt; this.stepCounter;
    });
  }
},

Note that newValue could be used instead of this.stepCounter.
While it would occupy the same number of lines as the current code, it takes the burden off the methods doPrev and doNext of having to call the method doCompleted. See the demo below for an illustration of this.


const makeStep = (text, index) =&gt; {return {text, step: ++index, completed: false}};
const steps = ['Cart', 'Shipping', 'Payment', 'Confirmation'].map(makeStep);
var app = new Vue({
  el: "#cart",
  data: {
    stepCounter: 1,
    steps
  },
  watch: {
    stepCounter: function(newValue, oldValue) {
      this.steps.forEach(item =&gt; {
        item.completed = item.step &lt;  this.stepCounter;
      });
    }
  },
  methods: {
    doPrev: function() {
      if (this.stepCounter &gt; 1) {
        this.stepCounter--;
      }
    },
    doNext: function() {
      if (this.stepCounter &lt;= this.steps.length) {
        this.stepCounter++;
      }
    }
  }
});
* {
  margin: 0;
  padding: 0;
  font-family: "Poppins", sans-serif;
}

.progressbar {
  display: flex;
  list-style-type: none;
  counter-reset: steps;
  padding-top: 50px;
  justify-content: space-between;
}

.progressbar li {
  font-size: 13px;
  text-align: center;
  position: relative;
  flex-grow: 1;
  flex-basis: 0;
  color: rgba(0, 0, 0, 0.5);
  font-weight: 600;
}

.progressbar li.completed {
  color: #ccc;
}

.progressbar li.active {
  color: #4caf50;
}

.progressbar li::after {
  counter-increment: steps;
  content: counter(steps, decimal);
  display: block;
  width: 30px;
  height: 30px;
  line-height: 30px;
  border: 2px solid rgba(0, 0, 0, 0.5);
  background: #fff;
  border-radius: 50%;
  position: absolute;
  left: 50%;
  margin: -60px 0 0 -15px;
}

.progressbar li.active::after,
.progressbar li.completed::after {
  background: #4caf50;
  border-color: rgba(0, 0, 0, 0.15);
  color: #fff;
}

.progressbar li.completed::after {
  content: '\2713';
}

.progressbar li::before {
  content: "";
  position: absolute;
  top: -26px;
  left: -50%;
  width: 100%;
  height: 2px;
  background: rgba(0, 0, 0, 0.5);
  z-index: -1;
}

.progressbar li.active::before,
.progressbar li.completed::before,
.progressbar li.active+li::before {
  background: #4caf50;
}

.progressbar li:first-child::before {
  display: none;
}
&lt;script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.6.11/vue.min.js"&gt;&lt;/script&gt;
&lt;link href="https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/4.4.1/css/bootstrap.min.css" rel="stylesheet" /&gt;

&lt;div id="cart" class="mt-2"&gt;
  &lt;div class="container"&gt;
    &lt;ul class="progressbar"&gt;
      &lt;li v-for="(step, index) in steps" :class="{ active: index + 1 === stepCounter, completed: step.completed }"&gt;{{step.text}}&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/div&gt;

  &lt;div class="container px-4 mt-5 text-center"&gt;
    &lt;div class="d-flex justify-content-between"&gt;
      &lt;button type="button" class="btn btn-sm btn-success" :class="{ disabled : stepCounter === 1}" @click="doPrev()"&gt;Prev&lt;/button&gt;
      &lt;button type="button" class="btn btn-sm btn-success" :class="{ disabled : stepCounter &gt; steps.length}" @click="doNext()"&gt;Next&lt;/button&gt;
    &lt;/div&gt;
  &lt;/div&gt;
&lt;/div&gt;



If the data structure can be modified such that the completed property isn't needed, then the markup could use the condition step.step &lt; stepCounter to determine when the completed class is added to each list item. See the snippet below for a demonstration of this.


const makeStep = (text, index) =&gt; {return {text, step: ++index}};
const steps = ['Cart', 'Shipping', 'Payment', 'Confirmation'].map(makeStep);
const app = new Vue({
  el: "#cart",
  data: {
    stepCounter: 1,
    steps
  },
  methods: {
    doPrev: function() {
      if (this.stepCounter &gt; 1) {
        this.stepCounter--;
      }
    },
    doNext: function() {
      if (this.stepCounter &lt;= this.steps.length) {
        this.stepCounter++;
      }
    }
  }
});
* {
  margin: 0;
  padding: 0;
  font-family: "Poppins", sans-serif;
}

.progressbar {
  display: flex;
  list-style-type: none;
  counter-reset: steps;
  padding-top: 50px;
  justify-content: space-between;
}

.progressbar li {
  font-size: 13px;
  text-align: center;
  position: relative;
  flex-grow: 1;
  flex-basis: 0;
  color: rgba(0, 0, 0, 0.5);
  font-weight: 600;
}

.progressbar li.completed {
  color: #ccc;
}

.progressbar li.active {
  color: #4caf50;
}

.progressbar li::after {
  counter-increment: steps;
  content: counter(steps, decimal);
  display: block;
  width: 30px;
  height: 30px;
  line-height: 30px;
  border: 2px solid rgba(0, 0, 0, 0.5);
  background: #fff;
  border-radius: 50%;
  position: absolute;
  left: 50%;
  margin: -60px 0 0 -15px;
}

.progressbar li.active::after,
.progressbar li.completed::after {
  background: #4caf50;
  border-color: rgba(0, 0, 0, 0.15);
  color: #fff;
}

.progressbar li.completed::after {
  content: '\2713';
}

.progressbar li::before {
  content: "";
  position: absolute;
  top: -26px;
  left: -50%;
  width: 100%;
  height: 2px;
  background: rgba(0, 0, 0, 0.5);
  z-index: -1;
}

.progressbar li.active::before,
.progressbar li.completed::before,
.progressbar li.active+li::before {
  background: #4caf50;
}

.progressbar li:first-child::before {
  display: none;
}
&lt;script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.6.11/vue.min.js"&gt;&lt;/script&gt;
&lt;link href="https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/4.4.1/css/bootstrap.min.css" rel="stylesheet" /&gt;

&lt;div id="cart" class="mt-2"&gt;
  &lt;div class="container"&gt;
    &lt;ul class="progressbar"&gt;
      &lt;li v-for="(step, index) in steps" v-bind:class="{ active: index + 1 === stepCounter, completed: step.step &lt; stepCounter }"&gt;{{step.text}}&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/div&gt;

  &lt;div class="container px-4 mt-5 text-center"&gt;
    &lt;div class="d-flex justify-content-between"&gt;
      &lt;button type="button" class="btn btn-sm btn-success" v-bind:class="{ disabled : stepCounter === 1}" @click="doPrev()"&gt;Prev&lt;/button&gt;
      &lt;button type="button" class="btn btn-sm btn-success" v-bind:class="{ disabled : stepCounter &gt; steps.length}" @click="doNext()"&gt;Next&lt;/button&gt;
    &lt;/div&gt;
  &lt;/div&gt;
&lt;/div&gt;



CSS: margin rules
The ruleset .progressbar li::after { contains these rules:

margin-left: -15px;
margin-top: -60px;


Presuming there is no margin inherited from other elements for the right and bottom, those can be combined into a single rule:
margin: -60px 0 0 -15px;