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

Auto Save

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

              
                <div id="app" v-on:mousemove="handleMousemove" v-on:mouseup="handleMouseup">
  <div id="toolbar">
    <div class="toolbar">
      <div class="control-group" v-on:mouseover="showMax = false">
        <h3>Minimum</h3>

        <div class="control">
          <h4>Width</h4>
          <scrubber :min='320' :max='480' :steps='10' :value.sync='settings.minWidth' :change='onMinWidthChange'></scrubber>
        </div>

        <div class="control">
          <h4>Base font size</h4>
          <scrubber :min='13' :max='22' :steps='1' :value.sync='settings.minBase'></scrubber>
        </div>

        <div class="control">
          <h4>Contrast</h4>
          <scrubber :min='1' :max='2' :steps='0.01' :value.sync='settings.minContrast'></scrubber>
        </div>
      </div>

      <hr />

      <div class="control-group" v-on:mouseover="showMax = true">
        <h3>Maximum</h3>
        
        <div class="control">
          <h4>Width</h4>
          <scrubber :min='480' :max='1800' :steps='10' :value.sync='settings.maxWidth' :change='onMaxWidthChange'></scrubber>
        </div>

        <div class="control">
          <h4>Base font size</h4>
          <scrubber :min='settings.minBase' :max='32' :steps='1' :value.sync='settings.maxBase'></scrubber>
        </div>

        <div class="control">
          <h4>Contrast</h4>
          <scrubber :min='settings.minContrast' :max='2' :steps='0.01' :value.sync='settings.maxContrast'></scrubber>
        </div>

      </div>
    </div>

    <hr /> More information: <a href="https://medium.com/@getflourish/designing-with-intent-be6664b10ac#.ihuhmfasg">Designing with intent</a> by Florian Schulz

  </div>

  <div class="side-by-side" v-if="lock">
    <div class="with-controls" v-if="!showMax || splitView">
      <div class="handle" v-on:mousedown="handleMinMousedown" v-bind:class="{'js-active': mousedown && willChange == 'min' }"></div>
      <div class="demo" v-bind:style="{'width': minWidth + 'px', 'padding': scaleMin[3] + 'px', 'transitionDuration': settings.transitionDuration }">
        <div v-for="item in content">
          <div class="item" v-bind:class="item.class" v-bind:style="{'font-size': scaleMin[item.size] + 'px', 'transitionDuration': settings.transitionDuration }">
            {{ item.text }}
            <input class="size-input" type="number" max="9" min="0" v-model="item.size" />
          </div>
        </div>
      </div>
    </div>

    <div class="with-controls" v-if="showMax || splitView">
      <div class="handle" v-on:mousedown="handleMousedown" v-bind:class="{'js-active': mousedown && willChange == 'max' }"></div>
      <div class="demo demo--big" v-bind:style="{'width': maxWidth + 'px', 'padding': scale[3] + 'px', 'transitionDuration': settings.transitionDuration }">
        <div v-for="item in content">
          <div class="item" v-bind:class="item.class" v-bind:style="{'font-size': scale[item.size] + 'px', 'transitionDuration': settings.transitionDuration}">
            {{ item.text }}
            <input class="size-input" type="number" max="9" min="0" v-model="item.size" />
          </div>
        </div>
      </div>
    </div>
  </div>
              
            
!

CSS

              
                $sans: "soleil",
"Helvetica Neue",
sans;
$serif: "ff-meta-serif-web-pro",
Georgia,
serif;
* {
  box-sizing: border-box;
}

body {
  padding: 2rem;
  margin: 0 auto;
  font-family: "Helvetica Neue", sans;
  background: #EEF0F4;
  user-select: none;
}

#app {
  display: flex;
}

.demo {
  background: white;
  position: relative;
  transition: width 0s;
}

div {
  position: relative;
}

.title {
  margin: 0;
  line-height: 1.1;
  font-weight: bold;
  font-family: $sans;
}

.subtitle {
  margin: 1em 0 2em 0;
  line-height: 1.3;
}

.paragraph {
  line-height: 1.5;
  max-width: 800px;
}

.heading {
  margin: 1.5em 0 0.5em;
  font-weight: bold;
  font-family: $sans;
}

.toolbar {}

#toolbar {
  padding: 0 2rem;
  z-index: 10;
  width: 300px;
}

.control {
  max-width: 180px;
}

.control input[type="range"] {
  width: 100px;
}

.control h3 {
  display: block;
}

.control + .control {
  margin-top: 2rem;
}

.item:hover .size-input {
  opacity: 1;
}

.size-input {
  opacity: 0;
  position: absolute;
  left: 0px;
  top: 0px;
  width: 50px;
  font-size: 16px;
  padding: 0.25rem;
  z-index: 202;
  text-align: center;
}

[contenteditable="true"]:active,
[contenteditable="true"]:focus {
  border: none;
  outline: none;
}

[contenteditable="true"]:hover {
  background: #f2f2f2;
}

table {
  display: inline-block;
}

.disabled {
  opacity: 0.4;
  pointer-events: none;
}

td {
  padding: 0.25rem;
}

thead td {
  font-weight: bold;
}

.side-by-side {
  width: 100%;
  overflow-y: scroll;
  white-space: nowrap;
}

.with-controls {
  display: inline-block;
  position: relative;
  vertical-align: top;
}

.with-controls + .with-controls {
  margin-left: 2rem;
}

.demo {
  display: block;
  white-space: normal;
  vertical-align: top;
  transition: all 0s;
  position: relative;
  height: 90vh;
  overflow-y: scroll;
  background: white;
}

.item {
  transition: all 0s;
}

.handle {
  position: absolute;
  right: -16px;
  top: 0px;
  width: 16px;
  height: 100%;
  background: #FFE5ED;
  z-index: 200;
}

.handle:after {
  content: "||";
  width: 10px;
  height: 10px;
  opacity: 0.3;
  display: block;
  position: absolute;
  top: 50%;
  left: 4px;
  z-index: 201;
}

.handle:hover {
  background: #FFCCDC;
  cursor: ew-resize;
}

.js-active {
  background: #FFCCDC;
}

.vue-scrubber {
  font-size: 15px;
  padding: 0.5em;
  border: 0;
  font-family: monospace;
  cursor: ew-resize;
  background: rgba(0, 0, 0, 0.05);
  color: #242424;
  -webkit-user-select: none;
  font-feature-settings: "lnum" 1;
}

.vue-scrubber:hover,
.vue-scrubber:focus {
  outline: 0;
  box-shadow: none;
}

hr {
  margin: 2rem 0;
  opacity: 0.2;
}

h3 {
  font-weight: 300;
  letter-spacing: 1px;
}

p {
  line-height: 1.3;
  opacity: 0.8;
}

p:first-child {
  margin-top: 0;
}

#toolbar {
  min-width: 300px;
  height: 90vh;
  overflow-y: scroll;
  margin-right: 1rem;
}
              
            
!

JS

              
                // Scrubber component
// Lets the user change an input field value by dragging the mouse left/right.
// Manual text input is still possible.

Vue.component('scrubber', {
  data: function() {
    return {
      isMouseDown: false,
      initialMouse: null
    }
  },
  computed: {

    // returns the number of decimals based on the step value
    // e.g. "0.25" returns "2"
    decimals: function() {
      return this.steps.toString().substr((this.steps).toString().indexOf(".")).length - 1;
    },

    // every time the value changes, we need to make sure it stays inside the min/max
    constrainedValue: function() {
      return this.constrain(this.value, this.min, this.max, this.decimals);
    },

    style: function() {
      var stop = translate(this.value, this.min, this.max, 0, 100);
      return {
        background: this.color,
        width: this.width + "px",
        backgroundImage: "linear-gradient(to right, rgba(0, 0, 0, 0.04) 0%, rgba(0, 0, 0, 0.04) " + (stop - 1) + "%, rgba(0, 0, 0, 0) " + stop + "%, rgba(0, 0, 0, 0) 100%)"
      }
    }
  },

  // props that the scrubber can receive
  // value: initial value
  // min: minimum value
  // max: maximum value
  // steps: increments for each pixel the mouse is moved
  props: ["value", "min", "max", "steps", {
    "name": "change",
    "default": function() {}
  }, {
    "name": "width",
    "default": 200
  }, {
    "name": "color",
    "default": "rgba(0, 0, 0, 0.05)"
  }],

  // the template
  template: "<input class='vue-scrubber' v-model='constrainedValue' v-on:mousedown='handleMouseDown' v-on:input='handleInput' v-on:keydown.up='handleKeyCodeUp' v-on:keydown.down='handleKeyCodeDown' v-on:change='handleChange' v-bind:style='style' />",

  methods: {

    // constrains a number to not exceed the min/max
    // decimals: rounding precision
    constrain: function(value, min, max, decimals) {
      decimals = typeof decimals !== 'undefined' ? decimals : 0;

      if (min != undefined && max != undefined) {
        return this.round(Math.min(Math.max(parseFloat(value), min), max), decimals);
      } else {
        return value;
      }
    },

    // method to round a number to given decimals
    round: function(value, decimals) {
      return Number(Math.round(value + 'e' + decimals) + 'e-' + decimals);
    },
    handleInput: function(event) {
      console.log(event.keyCode)
        // only allow numeric keys
      if (event.keyCode != 46 && event.keyCode < 48 || event.keyCode > 57) {
        event.preventDefault();
      } else {
        //this.value = parseFloat(event.target.value)
        if (event.keyCode != 8) {
          var value = isNaN(parseFloat(event.target.value)) ? 0 : parseFloat(event.target.value);
          this.value = Math.min(Math.max(value, this.min), this.max);
        } else {

        }

      }
    },

    handleChange: function(event) {

      this.value = isNaN(parseFloat(event.target.value)) ? 0 : parseFloat(event.target.value);

    },

    handleKeyCodeUp: function(event) {
      event.preventDefault();
      this.value += parseFloat(this.steps);
    },

    handleKeyCodeDown: function(event) {
      event.preventDefault();
      this.value -= parseFloat(this.steps);
    },

    // mouse handler
    handleMouseDown: function(event) {

      // enable scrubbing
      this.mouseDown = true;

      // remember the initial mouse position when the scubbing started
      this.initialMouse = {
        x: event.clientX,
        y: event.clientY
      }

      // remember the initial value
      this.initialValue = this.value;

      // register global event handlers because now we are not bound to the component anymore
      document.addEventListener("mousemove", this.handleMouseMove)

      // global mouse up listener
      document.addEventListener("mouseup", this.handleMouseUp)

    },
    handleMouseUp: function($event) {

      // disable scrubbing
      this.mouseDown = false;

      document.removeEventListener("mousemove", this.handleMouseMove)
      document.removeEventListener("mouseup", this.handleMouseUp)

    },

    // the actual translation of mouse movement to value change…
    handleMouseMove: function(event) {

      // scrub if the mouse is being pressed
      if (this.mouseDown) {
        var damping = (this.max - this.min) / this.width;
        var newValue = this.initialValue + ((event.clientX - this.initialMouse.x) * damping)

        // constrain the value to the min/max
        this.value = this.constrain(newValue, this.min, this.max, this.decimals);

        // call change handler
        if (typeof this.change == "function") this.change(event)
      }
    }
  }
})

function translate(value, low1, high1, low2, high2) {
  var value = parseFloat(value)
  var low1 = parseFloat(low1)
  var high1 = parseFloat(high1)
  var low2 = parseFloat(low2)
  var hight2 = parseFloat(high2)
  return low2 + (high2 - low2) * ((value - low1) / (high1 - low1));
}

var content = [{
    text: "Fluid Modular Scale",
    size: 3,
    class: "title"
  },

  {
    text: "A sane approach to responsive typography",
    size: 2,
    class: "subtitle"
  },

  {
    text: "Parameters, not values",
    size: 1,
    class: "heading"
  },

  {
    text: "Lorem ipsum dolor sit amet, consectetur adipisicing elit. Aperiam illo vero vitae placeat cupiditate, blanditiis. Possimus, quidem, ea. Beatae perspiciatis officia obcaecati ipsum incidunt, adipisci illo inventore ab architecto eum. Lorem ipsum dolor sit amet, consectetur adipisicing elit. Aperiam illo vero vitae placeat cupiditate, blanditiis. Possimus, quidem, ea. Beatae perspiciatis officia obcaecati ipsum incidunt, adipisci illo inventore ab architecto eum.",
    size: 0,
    class: "paragraph"
  },

  {
    text: "Math to the rescue",
    size: 0,
    class: "heading"
  },

  {
    text: "Lorem ipsum dolor sit amet, consectetur adipisicing elit. Aperiam illo vero vitae placeat cupiditate, blanditiis. Possimus, quidem, ea. Beatae perspiciatis officia obcaecati ipsum incidunt, adipisci illo inventore ab architecto eum.",
    size: 0,
    class: "paragraph"
  }

]

new Vue({
  el: "#app",
  data: function() {
    return {
      factor: 1.38,
      amount: 10,
      content: content,
      lock: true,
      ratio: 0,
      minWidth: 320,
      maxWidth: 960,
      width: 960,
      timeout: null,
      mouse: {
        x: 0,
        y: 0
      },
      willChange: "none",
      initialWidth: 0,
      showMax: false,
      splitView: true,
      settings: {
        minBase: 16,
        maxBase: 18,
        minWidth: 320,
        maxWidth: 960,
        minContrast: 1.2,
        maxContrast: 1.6,
        transitionDuration: "0s"
      },
      mousedown: false,
      initialMouse: {
        x: 0,
        y: 0
      }
    }
  },
  methods: {
    onLock: function() {
      this.ratio = this.factor / this.base;
    },
    onMinWidthChange: function() {
      this.minWidth = this.settings.minWidth;
    },
    onMaxWidthChange: function() {
      this.maxWidth = this.settings.maxWidth;
    },
    onWidthUp: function() {
      if (this.lock) this.width = this.settings.maxWidth;
    },
    handleMinMousedown: function(event) {

      this.willChange = "min";
      this.mousedown = true;

      this.initialWidth = this.minWidth;

      this.initialMouse = {
        x: event.clientX,
        y: event.clientY
      }
    },
    handleMousedown: function(event) {

      this.willChange = "max";
      this.mousedown = true;

      this.initialWidth = this.maxWidth;

      this.initialMouse = {
        x: event.clientX,
        y: event.clientY
      }
    },
    handleMouseup: function(event) {
      this.mousedown = false;

      switch (this.willChange) {
        case "min":
          this.minWidth = this.settings.minWidth;
          break;
        case "max":
          this.maxWidth = this.settings.maxWidth;
          break;
      }

      var that = this;
      this.settings.transitionDuration = "0.25s";
      this.timeout = setTimeout(function() {
        that.settings.transitionDuration = "0s";
      }, 250)
    },

    handleMousemove: function(event) {

      if (this.mousedown) {
        if (this.willChange == "min") {

          this.minWidth = Math.min(Math.max(this.initialWidth - (this.initialMouse.x - event.clientX), this.settings.minWidth), this.settings.maxWidth)
          this.mouse = {
            x: event.clientX,
            y: event.clientY
          }
        }

        if (this.willChange == "max") {

          this.maxWidth = Math.min(Math.max(this.initialWidth - (this.initialMouse.x - event.clientX), this.settings.minWidth), this.settings.maxWidth)
          this.mouse = {
            x: event.clientX,
            y: event.clientY
          }
        }
        this.settings.transitionDuration = "0s";
      }

    }
  },
  computed: {
    base: function() {
      return translate(this.maxWidth, this.settings.minWidth, this.settings.maxWidth, this.settings.minBase, this.settings.maxBase).toFixed(2)
    },

    baseMin: function() {
      return translate(this.minWidth, this.settings.minWidth, this.settings.maxWidth, this.settings.minBase, this.settings.maxBase).toFixed(2)
    },
    css: function() {
      var diffBase = this.base - this.baseMin;
      var diffWidth = this.maxWidth - this.minWidth;

      return "font-size: calc(" + this.baseMin + "px + (" + diffBase + " * (100vw - " + this.minWidth + "px) /  " + diffWidth + "))"

    },

    scale: function() {
      var result = [];
      var start = 0;

      for (var i = start; i < this.amount; i++) {
        result.push(this.base * Math.pow(this.contrast, i))
      }
      return result;
    },
    scaleMin: function() {
      var result = [];
      var start = 0;

      for (var i = start; i < this.amount; i++) {
        result.push(this.baseMin * Math.pow(this.contrastMin, i))
      }
      return result;
    },

    contrast: function() {
      return translate(this.maxWidth, this.settings.minWidth, this.settings.maxWidth, this.settings.minContrast, this.settings.maxContrast).toFixed(2)
    },
    contrastMin: function() {
      return translate(this.minWidth, this.settings.minWidth, this.settings.maxWidth, this.settings.minContrast, this.settings.maxContrast).toFixed(2)
    }
  },
  ready: function() {
    document.addEventListener("mouseup", this.handleMouseup)
  }
})
              
            
!
999px

Console