<div id="todo" class="container">
  <section class="todo">
    <header class="todo__header">
      <h1 @keydown.enter.prevent="updateTitle" @keyup="updateTitle" @blur="updateTitle" @paste="updateTitle" @delete="updateTitle" contentEditable class="todo__title">{{ title }}</h1>
    </header>

    <transition-group name="todo__item" tag="ul" class="todo__list">
      <li v-for="(todo, index) in todos" v-if="todo.active" :key="todo.id" :class="{ 'is-done': todo.done }" class="todo__item">
        <label class="checkbox todo__checkbox">
          <input type="checkbox" :checked="todo.done" v-model="todo.done" class="checkbox__input">
          <span class="checkbox__icon"></span>
        </label>
        <span @keydown.enter.prevent="editItem(todo, $event)" @keyup="editItem(todo, $event)" @blur="editItem(todo, $event)" @paste="editItem(todo, $event)" @delete="editItem(todo, $event)" contentEditable class="todo__text">{{ todo.text }}</span>
        <button @click="deactivateItem(todo)" class="todo__delete">x</button>
      </li>
    </transition-group>

    <form @submit.prevent="addItem" class="todo__form">
      <input type="text" name="text" id="text" autocomplete="off" placeholder="New Entry" class="todo__input">
    </form>
  </section>
  
  <section class="recycle-bin">
    <header class="recycle-bin__header">
      <h2>Recycle Bin</h2> <button @click="resetList">Delete List</button>
    </header>

    <transition-group name="todo__item" tag="ul" class="todo__list">
      <li v-for="(todo, index) in todos" v-if="!todo.active" :key="todo.id" :class="{ 'is-done': todo.done }" class="todo__item">
        <label class="checkbox todo__checkbox">
          <input type="checkbox" :checked="todo.done" v-model="todo.done" class="checkbox__input">
          <span class="checkbox__icon"></span>
        </label>
        <span @keydown.enter.prevent="editItem(todo, $event)" contentEditable class="todo__text">{{ todo.text }}</span>
        <div class="todo__button-group">
          <button @click="activateItem(todo)">^</button>
          <button @click="deleteItem(index)">X</button>
        </div>
      </li>
    </transition-group>
  </section>
</div>
// SVG URL
// Source:
// https://codepen.io/jakob-e/pen/doMoML
// =============================================================================

@function svg-url($svg){
  // Chunk up string in order to avoid
  // "stack level too deep" error
  $encoded: '';
  $slice: 2000;
  $index: 0;
  $loops: ceil(str-length($svg)/$slice);
  @for $i from 1 through $loops {
    $chunk: str-slice($svg, $index, $index + $slice - 1);

    // Encode (may need a few extra replacements)
    $chunk: str-replace($chunk,'"','\'');
    $chunk: str-replace($chunk,'<','%3C');
    $chunk: str-replace($chunk,'>','%3E');
    $chunk: str-replace($chunk,'&','%26');
    $chunk: str-replace($chunk,'#','%23');
    $encoded: #{$encoded}#{$chunk};
    $index: $index + $slice;
  }
  @return url("data:image/svg+xml;charset=utf8,#{$encoded}");
}

@function str-replace($string, $search, $replace: '') {
  $index: str-index($string, $search);
  @if $index {
    @return str-slice($string, 1, $index - 1) + $replace +
      str-replace(str-slice($string, $index +
      str-length($search)), $search, $replace);
  }
  @return $string;
}



// SCREEN READER ONLY
// Source:
// https://medium.com/@jessebeach/beware-smushed-off-screen-accessible-text-5952a4c2cbfe
// =============================================================================



@mixin sr-only {
  position: absolute;
  width: 1px;
  height: 1px;

  overflow: hidden;

  clip: rect(1px 1px 1px 1px); /* IE 6/7 */
  clip: rect(1px, 1px, 1px, 1px);

  white-space: nowrap;
}


$color-text: #333;
$color-text-disabled: rgba($color-text, 0.4);



*, *::before, *::after {
  box-sizing: border-box;
}

body {
  padding: 1em;
  font-family: 'Roboto', sans-serif;
  font-size: 1em;
  color: $color-text;
}

h1, h2 {
  margin: 0;
  font-weight: 300;
}

h1 {
  font-size: 1.5em;
}

h2 {
  font-size: 1.25em;
}

button {
  padding: 0.25em 0.5em;
  font-family: 'Roboto', sans-serif;
  font-weight: 300;
  font-size: 0.9em;
  text-transform: uppercase;
  line-height: 1;
  color: #fff;
  background-color: rgba(0, 0, 0, 0.4);
  border: 0.0625rem solid rgba(0, 0, 0, 0.5);
  cursor: pointer;
  
  &:hover,
  &:focus {
    background-color: rgba(0, 0, 0, 0.6);
    outline: none;
  }
}

.checkbox {
  position: absolute;
  left: 1em;
  z-index: 1;
}

.checkbox__input {
  @include sr-only;
}

.checkbox__icon {
  display: block;
  width: 0.9em;
  height: 0.9em;
  cursor: pointer;
  
  .checkbox__input + & {
    background: svg-url('<svg fill="#{$color-text}" height="24" viewBox="0 0 24 24" width="24" xmlns="http://www.w3.org/2000/svg"><path d="M19 5v14H5V5h14m0-2H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2z"/></svg>') left / 100% no-repeat;
  }
  
  .checkbox__input:checked + & {
    background-image: svg-url('<svg fill="#{$color-text}" height="24" viewBox="0 0 24 24" width="24" xmlns="http://www.w3.org/2000/svg"><path d="M19 3H5c-1.11 0-2 .9-2 2v14c0 1.1.89 2 2 2h14c1.11 0 2-.9 2-2V5c0-1.1-.89-2-2-2zm-9 14l-5-5 1.41-1.41L10 14.17l7.59-7.59L19 8l-9 9z"/></svg>');
  }
  
  .checkbox__input:focus + & {
    box-shadow: 0 0 0 0.0625rem rgba(0, 0, 0, 0.5);
    border-radius: 0.1em;
  }
}



.container {
  max-width: 25em;
}



.todo {
  margin-bottom: 1em;
  background-color: #fffb93;
}

.todo__header {
  padding: 1em;
}

.todo__title {
  &:focus {
    outline: none;
  }
}

.todo__list {
  list-style: none;
  margin: 0;
  padding: 0;
  font-family: 'Roboto Slab', serif;
}

.todo__item {
  display: flex;
  align-items: center;
  position: relative;
  height: 2em;
  transition: opacity 0.2s, height 0.2s;
}

.todo__item-enter, .todo__item-leave-active {
  height: 0;
  opacity: 0;
}

.todo__checkbox {
  transition: opacity 0.2s;
  
  .is-done & {
    opacity: 0.4;
  }
}

.todo__text {
  display: flex;
  flex-grow: 1;
  align-items: center;
  height: 100%;
  padding-left: 2.5em;
  transition: opacity 0.2s;
  
  &:focus {
    outline: none;
    border-top:    0.0625rem solid rgba(0, 0, 0, 0.2);
    border-bottom: 0.0625rem solid rgba(0, 0, 0, 0.2);
  }

  .is-done & {
    color: $color-text-disabled;
  }
}

.todo__delete {
  position: absolute;
  right: 1em;
  width: 1.25em;
  height: 1.25em;
  padding: 0;
  text-transform: none;
  border-radius: 50%;
  opacity: 0;
  transition: opacity 0.2s;
  
  &:focus,
  .todo__item:hover & {
    opacity: 1;
  }
}

.todo__form {
  display: flex;
  flex-wrap: wrap;
  padding-bottom: 1em;
}

.todo__input {
  flex-grow: 1;
  height: 2em;
  margin-right: -0.0625rem;
  padding: 0 0 0 2.5em;
  font-family: 'Roboto Slab', serif;
  background: transparent svg-url('<svg fill="#{$color-text-disabled}" height="24" viewBox="0 0 24 24" width="24" xmlns="http://www.w3.org/2000/svg"><path d="M19 13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z"/></svg>') left 0.9em center / 1em no-repeat;
  border: none;
  
  &:focus {
    border-top:    0.0625rem solid rgba(0, 0, 0, 0.2);
    border-bottom: 0.0625rem solid rgba(0, 0, 0, 0.2);
    outline: none;
  }
  
  &::placeholder {
    opacity: 1;
    color: $color-text-disabled;
  }
}

.todo__delete-list {
  margin-bottom: 1em;
}

.todo__button-group {
  position: absolute;
  right: 1em;
}



.recycle-bin {
  background-color: #ffbaba;
  padding-bottom: 1em;
}

.recycle-bin__header {
  display: flex;
  justify-content: space-between;
  align-items: flex-start;
  padding: 1em;
}
View Compiled
'use strict';

let createUUID = function() {
  return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
    var r = Math.random()*16|0, v = c == 'x' ? r : (r&0x3|0x8);
    return v.toString(16);
  });
};

let updateLocalStorage = function(key, value) {
  localStorage.setItem(key, JSON.stringify(value));
};

let getLocalStorage = function(key) {
  return JSON.parse(localStorage.getItem(key));
};

let todo = new Vue({
  el: '#todo',
  
  data: {
    title: 'Simple Todo List',
    todos: [
      {
        id: createUUID(),
        active: true,
        done: true,
        text: 'Create todo list with Vue.js'
      },
      {
        id: createUUID(),
        active: true,
        done: false,
        text: 'Make todo list more awesome'
      },
      {
        id: createUUID(),
        active: false,
        done: false,
        text: 'Secret hidden todo list entry'
      }
    ]
  },
  
  created: function() {
    let todoLocal  = getLocalStorage('todos');
    let titleLocal = getLocalStorage('title');

    if (todoLocal && todoLocal.length > 0) {
      this.todos = todoLocal;
    }
    
    if (titleLocal) {
      this.title = titleLocal;
    }
  },
  
  methods: {
    updateTitle: _.debounce(function(event) {
      let title = event.target.innerText;
      
      if (title) {
        this.title = title;
        updateLocalStorage('title', title);
      }
    }, 500),
    addItem: function(event) {
      let formData = new FormData(event.target);
      let text = formData.get('text');

      if (text) {
        this.todos.push({
          id: createUUID(),
          active: true,
          done: false,
          text: text
        });
      }
      
      updateLocalStorage('todos', this.todos);
      
      event.target.reset();
    },
    editItem: _.debounce(function(todo, event) {
      let text = event.target.innerText;
      
      if (text) {
        todo.text = event.target.innerText;
        updateLocalStorage('todos', this.todos);
      }
    }, 500),
    deactivateItem: function(todo) {
      todo.active = false;
      updateLocalStorage('todos', this.todos);
    },
    activateItem: function(todo) {
      todo.active = true;
      updateLocalStorage('todos', this.todos);
    },
    deleteItem: function(index) {
      this.todos.splice(index, 1);
    },
    resetList: function() {
      if (confirm('This deletes the whole list including unfinished entries!')) {
        this.title = 'Simple Todo List';
        this.todos = [];
        localStorage.removeItem('todos');
        localStorage.removeItem('title');
      }
    }
  }
});
View Compiled

External CSS

  1. https://fonts.googleapis.com/css?family=Roboto+Slab|Roboto:300,400,700

External JavaScript

  1. https://unpkg.com/vue@2/dist/vue.js
  2. https://cdn.jsdelivr.net/lodash/4.16.6/lodash.min.js