Pen Settings

HTML

CSS

CSS Base

Vendor Prefixing

Add External Stylesheets/Pens

Any URLs added here will be added as <link>s in order, and before the CSS in the editor. You can use the CSS from another Pen by using its URL and the proper URL extension.

+ add another resource

JavaScript

Babel includes JSX processing.

Add External Scripts/Pens

Any URL's added here will be added as <script>s in order, and run before the JavaScript in the editor. You can use the URL of any other Pen and it will include the JavaScript from that Pen.

+ add another resource

Packages

Add Packages

Search for and use JavaScript packages from npm here. By selecting a package, an import statement will be added to the top of the JavaScript editor for this package.

Behavior

Save Automatically?

If active, Pens will autosave every 30 seconds after being saved once.

Auto-Updating Preview

If enabled, the preview panel updates automatically as you code. If disabled, use the "Run" button to update.

Format on Save

If enabled, your code will be formatted when you actively save your Pen. Note: your code becomes un-folded during formatting.

Editor Settings

Code Indentation

Want to change your Syntax Highlighting theme, Fonts and more?

Visit your global Editor Settings.

HTML

              
                script#battery(type='x-template')
    svg.battery( viewBox='0 0 250 700')
      defs
        path#border(d="M0 50c62.5-25 187.5-25 250 0v25c-62.5-25-187.5-25-250 0")
        path#subBorder(d="M0 73c62.5-25 187.5-25 250 0v6c-62.5-25-187.5-25-250 0")
        linearGradient#mainGr
          stop(offset="0" stop-color="#fff")
          stop(offset="10%" stop-color="#000")
          stop(offset="13%" stop-color="#fff")
          stop(offset="20%" stop-color="#fff")
          stop(offset="60%" stop-color="#000")
          stop(offset="63%" stop-color="#ddd")
          stop(offset="80%" stop-color="#ddd")
          stop(offset="83%" stop-color="#fff")
          stop(offset="90%" stop-color="#fff")
          stop(offset="99%" stop-color="#000")
          stop(offset="1" stop-color="#fff")
        linearGradient#borderGr //#1c1c1c
          stop(offset="0" stop-color="#ccc")
          stop(offset="10%" stop-color="#1c1c1c")
          stop(offset="12%" stop-color="#ccc")
          stop(offset="40%" stop-color="#1c1c1c")
          stop(offset="80%" stop-color="#1c1c1c")
          stop(offset="90%" stop-color="#ccc")
          stop(offset="99%" stop-color="#1c1c1c")
          stop(offset="1" stop-color="#ccc")          
        linearGradient#borderOppositeGr(href="#borderGr" x1="100%" y1="0" x2="0" y2="0")
          
        filter#dim
          feColorMatrix(type="saturate" values=".95")
        
      path#knob(d="M75 23q50-20 100 0v20q-50-20-100 0" fill="url(#borderGr)")
      
      rect#bottom(x="0" y="50" width="249" height="40" rx="125" ry="18" fill="#111")

      use(href="#bottom" y="550" v-show="Math.round(fromTop) == bottom")
  
      path.battery-inner-section(:d='sectionPath', :fill="innerColor" filter="url(#dim)" v-show="Math.round(fromTop) !== bottom")
      path#inner(:d='innerPath', :fill="innerColor")
      
      path#body.battery-body(d="M0 70c62.5-25 187.5-25 250 0v550c-62.5 25-187.5 25-250 0" fill="url(#mainGr)")
      
      use(href="#border" fill="url(#borderGr)")
      use(href="#subBorder")
      use(href="#border" y="550" transform="rotate(180, 125, 620)" fill="url(#borderOppositeGr)")
      use(href="#subBorder" y="548" transform="rotate(180, 125, 620)")
      
script#time(type="x-template")
  .time-box
    p.label {{label}}
    .input
      .controls
        button.plus(@click="increaseTime", :disabled="disabled") +
        button.minus(@click="decreaseTime", :disabled="disabled") -
      input(type="number", :min="min", :max="max", :value="value", :disabled="disabled", @keydown.e.prevent='' @change="onChange($event.target.value)")
      span min.
      
script#play-btn(type="text/x-template")
  button.play-btn(@click="onClick")
    svg(viewBox="-1 -1 6 6")
      polygon(ref="polygon" points="0 0 4 2 4 2 0 4")
      
#pomodoro.app 
  p.title Pomodoro Timer
  .state
    transition(name="fade")
      div.state-text(v-show="isRunning")
        transition(name="switch" mode="out-in")
          p(v-if="recess" key="break") Break
          p(v-else key="session") Session
          
        time {{time}}
    clock-battery(:class="{running: isRunning}", :progress="progress")
    
  .timers
    clock-time(v-model="sessionTime", :min="1", :max="59" label="Session", :disabled="isRunning")
    clock-time(v-model="breakTime", :min="1", :max="59" label="Break", :disabled="isRunning")
  play-btn(@click="!isRunning ? run() : stop()", :running="isRunning")
  

              
            
!

CSS

              
                @import url('https://fonts.googleapis.com/css?family=Share+Tech+Mono');

:root {
    --main-bg: rgb(156, 181, 239);
    --time-box-clr: rgb(47, 43, 78);
    --time-box-text-clr: rgb(194, 154, 255);
    --accent: #fff551;
    --shadow: 0 .1em .3em rgb(0, 0, 0);
    --time-clr: rgb(240, 248, 255);
}

$play-btn-width: 2em;
$pad: .5em;

body {
  background-color: var(--main-bg);
  text-align: center;
  min-height: 100vh;
  overflow: hidden;
}

body, button, input {
  font-family: 'Share Tech Mono', monospace;
}

.app {
  font-size: 6vmin;
  display: inline-block;
  background-image: linear-gradient(
    135deg, 
    rgb(31, 34, 72) 0, 
    rgb(90, 77, 142) 100%
  );
  border-radius: .1em;
  box-shadow: var(--shadow);
  padding: .3em;
  
  &::after {
    $add-size: .3;
    
    content: '';
    position: absolute;
    width: $play-btn-width + $add-size;
    height: $play-btn-width + $add-size;
    background-color: #514783;
    box-shadow: var(--shadow);
    transform: translate(-50%, -20%);
    border-radius: 50%;
    
    // negative values are for the shadow to be fully visible
    clip-path: polygon(-10% 33%, 110% 33%, 110% 115%, -10% 115%);
  }
} 

.battery {
  height: 9em;
  transition: transform .3s;
  position: absolute;
  
  &.running {
    transform: translateX(-2.2em);
  }
  
  text {
    fill: var(--time-box-clr);
  }
  
  &-body {
    opacity: .3;
  }
}

.state {
  display: flex;
  justify-content: center;
  min-height: 9em;
  
  time {
    color: var(--time-clr);
  }
  
  p {
    color: var(--main-bg); 
    margin-top: 0;
  }
  
  &-text {
    align-self: center;
    flex-basis: 4em;
    transform: translateX(2.2em);
  }
}

input[type=number] {
  -moz-appearance: textfield;
  -webkit-appearance: textfield;
  text-align: center;
  font-size: 1.5em;
  border: none;
  background-color: var(--time-box-clr);
  color: var(--time-clr);
  width: 1.5em;
  
  &::-webkit-inner-spin-button,
  &::-webkit-outer-spin-button {
  -webkit-appearance: none;
  }
}

.time-box {
  font-size: .6em;
  background-color: var(--time-box-clr);
  padding: $pad;
  color: var(--time-box-text-clr);
  box-shadow: var(--shadow);
  border-radius: .1em;
  
  &:first-child {
    margin-right: .5em;
  }
  
  span {
    align-self: center;
    margin-left: $pad;
  }
}

.title {
  color: var(--main-bg);
  margin: .5em 0 0;
}

.timers {
  display: flex;
}

.label {
  font-size: 1.2em;
  margin: 0 0 $pad;
  line-height: 1;
  color: var(--time-box-text-clr);
}

.input {
  display: flex;
}

.controls {
  display: flex;
  flex-direction: column;
  
  button {
    color: inherit;
    border: none;
    background-color: var(--time-box-clr);
    font-size: 1em;
  }
}

.play-btn {
  position: absolute;
  font-size: 1em;
  left: 50%;
  width: $play-btn-width;
  height: $play-btn-width;
  border-radius: 50%;
  box-shadow: 0 .2em .2em -.1em #000;
  border: none;
  
  // translateZ(0) to move the element to a separate layer
  transform: translate(-50%, -20%) translateZ(0);
  
  background-color: var(--time-box-clr);
  z-index: 2;
  
  svg {
    position: absolute;
    width: 60%;
    top: 50%;
    left: 50%;
    transform: translate(-50%,-50%);
    
    polygon {
      fill: none;
      stroke: rgb(75, 226, 255); 
      stroke-width: .4;
    }
  }
}

button:not([disabled]) {
  @media (hover:hover) {
    &:hover {
      filter: brightness(1.1);
    }  
  }
  &:active {
    filter: brightness(.85);
  }
  &:focus {
    outline: none;
  }
}

input:not([disabled]) {
  &:hover,
  &:focus {
    filter: brightness(1.1);  
  }
  &:focus {
    outline: none;
  }
}

button[disabled],
input[disabled] {
  color: grey;
}

/*
======================================================
                      Animation
======================================================
*/

.fade-enter-active,
.fade-leave-active,
.switch-enter-active,
.switch-leave-active {
  transition: transform .3s, opacity .3s;
}

.fade-enter, 
.fade-leave-to {
  transform: translateX(0);
  opacity: 0;
}

.switch-enter,
.switch-leave-to {
  transform: translateX(-100%);
  opacity: 0;
}
              
            
!

JS

              
                Vue.config.keyCodes.e = 69

const clockBattery = {
  template: '#battery',
  data () {
    return {
      top: 70,
      bottom: 620,
      yHandlerMax: 31,

      halfDistance: null,
      middle: null
    }
  },
  props: ['running', 'progress'],
  created () {
    const top = this.top

    this.middle = (this.bottom - top) / 2 + top
    this.halfDistance = this.middle - top
  },
  computed: {
    fromTop () {
        const pathHeight = this.bottom - this.top

        return this.top + pathHeight * this.progress
      },
      curveCoeff () {
        const middle = this.middle

        return (this.fromTop - middle) / middle
      },
      innerPath () {
        const yHandler = this.yHandlerMax * this.curveCoeff

        return `M0 ${this.fromTop}` +
               `c62.5 ${yHandler} 187.5 ${yHandler} 250 0` +
               'V620' +
               'c-62.5 25-187.5 25-250 0'
      },
      innerColor () {
        const getColor = (from, to, point) => {
          const getChanell = (from, to) => ~~(from + (to - from) * topCoeff)

          const relativeFromTop = this.fromTop - point,
            topCoeff = relativeFromTop / this.halfDistance

          return Object.keys(from).map(ch => getChanell(from[ch], to[ch]))
        }

        // battery colors
        const good = { r: 0,   g: 128, b: 0 },
               mid = { r: 255, g: 165, b: 0 },
               low = { r: 255, g: 0,   b: 0 }

        const args = this.fromTop <= this.middle
          ? [good, mid, this.top]
          : [mid, low, this.middle]
        
        return `rgb(${getColor(...args)})`
      },
      sectionPath () {
        const curveCoeff = this.curveCoeff,
          yHandlerMax = this.yHandlerMax,
          bottomYHandler = curveCoeff * yHandlerMax,
          topYHandler = -curveCoeff * yHandlerMax

        return `M0 ${this.fromTop}` +
               `c62.5 ${topYHandler} 187.5 ${topYHandler} 250 0` +
               `-62.5 ${bottomYHandler}-187.5 ${bottomYHandler}-250 0`
      }
  }
}

const clockTime = {
  template: '#time',
  props: ['min', 'max', 'value', 'label', 'disabled'],
  methods: {
    increaseTime () {
        if (this.value < this.max) this.value++

        this.$emit('input', this.value)
      },
      decreaseTime () {
        if (this.value > this.min) this.value--

        this.$emit('input', this.value)
      },
      onChange (value) {
        value = Number(value)

        if (!value || value < this.min) value = this.min
        else if (value > this.max) value = this.max

        this.value = value
        this.$emit('input', value)
      }
  }
}

const playBtn = {
  template: '#play-btn',
  props: ['running', 'duration'],
  methods: {
    // Custom shape morphing
    onClick () {
      const triangle = '0 0 4 2 4 2 0 4', 
          rect = '0 0 4 0 4 4 0 4',
          polygon = this.$refs.polygon,
          backwards = this.running,
          duration = 200,
          d = {}, r = {}           // diff (f - s), result
      
      let lastTime = 0, 
        s = parsePoints(triangle), // start shape
        f = parsePoints(rect)      // final shape
      
      if (backwards) {
        const temp = s
        s = f
        f = temp
      }

      for (const point in s) 
        d[point] = f[point] - s[point]
      
      const animate = time => {
        if (!lastTime) lastTime = time

        const progress = time - lastTime
        
        for (const point in s)
          r[point] = s[point] + d[point] * (progress / duration)
        
        polygon.setAttribute(
          'points', 
          Object.keys(r).reduce((str, prop, i) => (
            str + (i ? ' ' : '') + r[prop]
          ), '')
        )

        if (progress < duration) 
          requestAnimationFrame(animate)
        else 
          polygon.setAttribute('points', (backwards ? triangle : rect))
      }

      requestAnimationFrame(animate)
      
      this.$emit('click')
    }
  }
}

new Vue({
  el: '#pomodoro',
  data: {
    sessionTime: 25,
    breakTime: 5,
    timer: 0,
    progress: 0,
    time: null,
    recess: false,
    isRunning: false
  },
  components: {
    clockBattery,
    clockTime,
    playBtn
  },
  methods: {
    run () {
        let time = this.sessionTime
        let deadline = addMinutes(new Date(), time)
        let initialMs = time * 60000

        const countdown = () => {
          let { minutes, seconds, remainingMs } = getTimeRemaining(deadline)
          
          if (!minutes && !seconds) {
            if (!this.recess) {
              this.recess = true
              time = this.breakTime
            } else {
              this.recess = false
              time = this.sessionTime
            }

            initialMs = time * 60000

            deadline = addMinutes(new Date(), time);

            ({ minutes, seconds, remainingMs } = getTimeRemaining(deadline))
          }
          
          this.progress = (this.recess 
                             ? remainingMs 
                             : initialMs - remainingMs) / initialMs
          
          if (minutes < 10) minutes = '0' + minutes
          if (seconds < 10) seconds = '0' + seconds

          this.time = `${minutes}:${seconds}`
          this.timer = setTimeout(countdown, 100)
        }

        this.isRunning = true

        countdown()
      },
      stop () {
        this.recess = this.isRunning = false
        this.progress = 0

        clearTimeout(this.timer)
      }
  }
})

const addMinutes = (date, minutes) =>
  new Date(date.getTime() + minutes * 60000)

function getTimeRemaining (deadline) {
  const t = Date.parse(deadline) - Date.now()
  const seconds = Math.floor(t / 1000 % 60)
  const minutes = Math.floor(t / 1000 / 60 % 60)
          
  return { minutes, seconds, remainingMs: t }
}

function parsePoints (str) {
  let x = 1, y = 1
        
  return str.split(' ').map(Number).reduce((o, p, i) => {
    const prop = ((i + 1) % 2 === 0) ? `y${y++}` : `x${x++}`
    o[prop] = p
    return o
  }, {})
}

              
            
!
999px

Console