.container
  .view(
    v-for='s in strings' 
    v-responsive-text='s' 
    responsive-text-offset='24'
  ) 
  //- .view(
  //-   v-for='s in strings' 
  //-   v-responsive-text='s' 
  //-   responsive-text-offset='12'
  //-   responsive-text-max='64'
  //- )
View Compiled
@import url(https://fonts.googleapis.com/css?family=Open+Sans);

.container {
  width: 50%;
  max-width: 720px;
  margin: 0 auto;
  display: flex;
  justify-content: center;
  flex-wrap: wrap;
  align-items: baseline;
  font-family: 'Open Sans';
}

.view {
  width: 275px;
  font-size: 22px;
  margin: 4px;
  overflow: hidden;
  white-space: nowrap;
  text-align: center;
  
  // debug...
  // border: 1px solid rgba(blue, 0.1);
}
View Compiled
Vue.directive('responsive-text', {

  params: [
    'responsive-text-min',
    'responsive-text-max',
    'responsive-text-offset'
  ],

  bind() {
    this.__getFontSize = (el) => {
      const fontSize = window.getComputedStyle(el, null)
        .getPropertyValue('font-size')
      return parseFloat(fontSize.replace(/[^0-9.]/g, ''))
    }

    this.__addPx = (element, n) => {
      const x = this.__getFontSize(element) + n
      return `${x}px`
    }

    this.__getRandomId = () => {
      return `responsive-text__${Math.random().toString(36).slice(2, 15)}`
    }
  },

  update(value) {
    if (!value) return

    const id = this.__getRandomId()
    const offset = this.params.responsiveTextOffset || 0
    const max = this.params.responsiveTextMax
    const min = this.params.responsiveTextMin || 4

    // create a virtual offscreen clone of this.el with styles
    // that will enable us to calculate the actual text width
    const dummyEl = document.createElement(this.el.tagName)
    dummyEl.className = this.el.className
    dummyEl.setAttribute('id', id)
    dummyEl.textContent = value

    const dummyStyles = {
      position: 'absolute',
      top: '-9999px',
      height: '0 !important',
      opacity: 0
    }
    Object.keys(dummyStyles).forEach(key => {
      dummyEl.style[key] = dummyStyles[key]
    })

    // css priorities cannot be set directly
    dummyEl.style.setProperty('width', 'auto', 'important')

    // adjacent for easier debugging
    this.el.parentNode.insertBefore(dummyEl, this.el)


    Vue.nextTick(() => {
      // safe guard just in case
      let maxIterations = 200

      if (dummyEl.scrollWidth === this.el.scrollWidth) {
        this.el.textContent = value
        this.el.parentNode.removeChild(dummyEl)

        return

      } else if (dummyEl.scrollWidth < this.el.scrollWidth - offset) {

        // enlarge
        while(--maxIterations &&
            dummyEl.scrollWidth < this.el.scrollWidth - offset) {

          if (max && this.__getFontSize(dummyEl) >= max) {
            dummyEl.style.setProperty('font-size', max + 'px')
            break
          }

          dummyEl.style.setProperty('font-size', this.__addPx(dummyEl, 1))
        }

      } else {

        // shrink
        while (--maxIterations &&
            dummyEl.scrollWidth > this.el.scrollWidth - offset) {

          if (min && this.__getFontSize(dummyEl) <= min) {
            dummyEl.style.setProperty('font-size', min + 'px')
            break
          }

          dummyEl.style.setProperty('font-size', this.__addPx(dummyEl, -1))
        }
      }

      this.el.textContent = value
      this.el.style.setProperty('font-size', this.__getFontSize(dummyEl) + 'px')
      this.el.parentNode.removeChild(dummyEl)
    })
  }
})

//    +-----------+
//    | DEMO CODE |
//    +-----------+

new Vue({
  el: '.container',
  data: {
    strings: [
      'abc',
      '1 2 3',
      'foobarbaz',
      'foo bar baz',
      'a beautiful day for a neighbor',
      'gargantua',
      'Marry Poppins',
      'I Care Because You Do'
    ],
    test: 'foo'
  }
})
View Compiled
Run Pen

External CSS

This Pen doesn't use any external CSS resources.

External JavaScript

  1. https://cdnjs.cloudflare.com/ajax/libs/vue/1.0.13/vue.min.js
  2. https://cdnjs.cloudflare.com/ajax/libs/jquery/2.1.3/jquery.min.js