Pen Settings

HTML

CSS

CSS Base

Vendor Prefixing

Add External Stylesheets/Pens

Any URL's added here will be added as <link>s in order, and before the CSS in the editor. If you link to another Pen, it will include the CSS from that Pen. If the preprocessor matches, it will attempt to combine them before processing.

+ 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

              
                <!--Window splitter pattern  from https://www.w3.org/TR/wai-aria-practices-1.1/#windowsplitter-->
<header id="primary">
<h1 id="title">Accessible splitter example</h1>
<p >This example is composed of three panes. The first primary pane is this paragraph separated by an horizontal splitter from the container of the two other panes. These last two beeing separated by a vertical splitter.</p>
<p>Note that the first splitter is fixed.</p>
</header>
<div id="splitterH" class="splitter" data-pane1="primary" data-pane2="secondary" data-orientation="horizontal" data-unit="rem" data-position="fixed" aria-labelled-by="title">
</div>
<section id="secondary" class="container" aria-label="This Window splitter component usage">
  <nav id="pane1" class="pane1">
    <h2 id="toc">Table of contents</h2>
    <ol>
      <li><a href="#def">Definition and usage</a>
      <li><a href="#attr">Attributes</a>
      <li><a href="#key">Keyboard support</a>
      <li><a href="#mouse">Mouse support</a>
      <li><a href="#touch">Touch support</a>
	</ol>
  </nav>
  <div id="splitterV" class="splitter" data-pane1="pane1" data-pane2="pane2" aria-labelled-by="toc" ></div>
  <main id="pane2" class="pane2">
    <h2 id="def">Definition and usage</h2>
    <p>This <dfn>splitter</dfn> defines a separator between two panes. it redefines the size (width or height) of the primary pane. If fixed, the primary pane can only be toggle.</p>
    <p>It's possible to define a minimum size with the css min/max-width/height attributes on the primary pane.<br>
     0&lt;=&mldr;valueMin&mldr;valueNow&mldr;valueMax&mldr;&lt;=100(dimTotal = pane1.dim + pane2.dim)</p>
    
<h2 id="attr">Attributes</h2>
<dt>data-orientation</dt><dd>splitter "vertical" (by default) or "horizontal" orientation; defines the width/height and keys used to move the element</dd>
      <dt>data-pane1</dt><dd>primary pame controled by the splitter</dd>
      <dt>data-pane2</dt><dd>secondary pane needed to calculate maximum size</dd>
      <dt>data-unit</dt><dd>"rem"/"px" unit to define size. "px" by default</dd>
      <dt>data-position</dt><dd>"fixed"/"variable" value. "variable" by default. A fixed size splitter omits implementation of the arrow keys.</dd>
    </dl>
    <h2 id="key">Keyboard support</h2>
	<table>
	<tr>
	<th scope="col">Key
	<th scope="col">Function
	<tr>
	<td><kbd>Left</kbd> arrows
	<td>Moves a vertical splitter to the left (if not fixed).
	<tr>
	<td><kbd>Right</kbd> arrows
	<td>Moves a vertical splitter to the right (if not fixed).
	<tr>
	<td><kbd>Up</kbd> arrows
	<td>Moves a horizontal splitter up (if not fixed).
	<tr>
	<td><kbd>Down</kbd> arrows
	<td>Moves a horizontal splitter down (if not fixed).
	<tr>
	<td><kbd>Enter</kbd>
	<td>if the primary pane is not collapsed, collapses the pane. If the pane is collapsed, restores the splitter to its previous position.
	<tr>
	<td><kbd>Home</kbd>
	<td>Moves splitter to the position that gives the primary pane its smallest allowed size. This may completely collapse the primary pane.
	<tr>
	<td><kbd>End</kbd>
	<td>Moves splitter to the position that gives the primary pane its largest allowed size. This may completely collapse the secondary pane.
	</table>
    <h2 id="mouse">Mouse support</h2>
    <p>Mouse movements have the same effects as arrow keys. On a fixed panel, you can toggle if you exceed half the panel.</p>
    <h2 id="touch">Touch support</h2>
    <p>Touch movements have the same effects as arrow keys. On a fixed panel, you can toggle if you exceed half the panel.</p><p>Swipe gestures have the same effect as <kbd>end</kbd> and <kbd>home</kbd> keys.</p>
  </main>
</section>

</body>
</html>

              
            
!

CSS

              
                *{
  box-sizing: border-box;
  margin: 0;
  
}

html {
	height:100%;
}

body {
	display: flex;
	flex-direction: column;
	height:100%;
	font-size: 1.3rem;
  background: floralwhite;
}	

h1 {
  text-align: center;
  padding:1rem 0;
  border-bottom: solid 1px black
}		

h2, p {
	margin: 1rem 0
}

dt {
	font-weight: bold;
}

dd {
	margin-left: 4rem;
}

*:focus {
	border: 2px blue dotted;
}

.container {
	display: flex;
	flex-direction: row;
	flex-wrap: nowrap;
}


#primary, #pane1 {
	flex: 0 1 auto;
	overflow: auto;
}
#secondary, #pane2 { 
	flex: 1 1 0;
	overflow: auto;
}
#secondary {
	height:auto;
}
.splitter {
  display:block;
  width: 10px;
  height:auto;
  background: linear-gradient(0.25turn,  lightgray,gray,lightgray);
  cursor: col-resize;
  margin: 0 3px;
}
.splitter:focus {
  background: linear-gradient(0.25turn,  lightgray,lightSkyBlue,lightgray);
}
.splitter[data-orientation="horizontal"] {
  cursor: row-resize;
  width: auto;
  height: 10px;
}
table {
	border-collapse:collapse;
}
td, th {
	border:1px solid black;
	padding: 5px;
}

th {
	background: lightgray;
}
              
            
!

JS

              
                console.clear();

class Splitter{
  static pxToRem(pixelValue) {
    //document.documentElement is the reference to rem and not document.body
    // test with html {font-size: 18px} and body {font-size: 14px}, 1rem = 18px
    const fontSize = 1 / parseFloat(window.getComputedStyle(document.documentElement).getPropertyValue('font-size'));
    const newValue = pixelValue * fontSize;
    return newValue;
  }

  static dimToPx(elem, property) { //cf https://www.w3.org/TR/css-cascade-3/#computed
    const prop = window.getComputedStyle(elem, null).getPropertyValue(property);
    if (prop.slice(-2) === "px") {
      return parseInt(prop);
    } else if (prop.slice(-1) === "%") {
      let size = this._getDimension(elem.parentElement);
          
      return size * parseFloat(prop) / 100;
    } else {
      return 0; // -> none, auto, ...
    }
  }

  constructor(splitterId) {
    this.$elem = document.getElementById(splitterId);
    this.$pane1 = document.getElementById(this.$elem.dataset.pane1);
    this.$pane2 = document.getElementById(this.$elem.dataset.pane2);

    if (!this.$elem || !this.$pane1 || !this.$pane2) return;

    // instance variables
    // instance variables
    this.$isPane1Before = true;
    this.$isMoving = false;
    this.$drag = 0;
    this.$oldSize = 0; // used for toggle
    this.$maxSize = 0;  //limited by css max-xxx
    this.$minSize = 0; //limited by css min-xxx
    this.$totalSize = 0; // pane1+pane2 size
    //keyboard attributes
    this.$speedUp = 1;
    this.$maxSpeed = 20;
    //touch attributes
    this.$touchOrigin = 0;
    this.$threshold = 60; //required min distance traveled to be considered swipe
    this.$allowedTime = 300; // maximum time allowed to travel that distance
    this.$startTime = 0;
    
    // attributes
    this.$fixedPosition = this.$elem.dataset.position === "fixed";
    
    if (this.$elem.dataset.orientation !== "horizontal") {
      this.$orientation = "vertical";
      this.$cssOrient = "width";
      this.$mouseOrient = "clientX";
    } else {
      this.$orientation = "horizontal";
      this.$cssOrient = "height";
      this.$mouseOrient = "clientY";
    }

    if (this.$elem.dataset.unit !== "rem") 
      this.$unit = "px";
    else
      this.$unit = "rem";
    
    //initialisation
    // bind events
    this._onMouseDown = this._onMouseDown.bind(this);
    this._onMouseMove = this._onMouseMove.bind(this);
    this._onMouseUp = this._onMouseUp.bind(this);      
    this._onKeyDown = this._onKeyDown.bind(this);      
    this._onKeyUp = this._onKeyUp.bind(this);      
	this._onFocus = this._onFocus.bind(this);     
    if ('ontouchstart' in window) {
      this._onTouchStart = this._onTouchStart.bind(this);
      this._onTouchEnd = this._onTouchEnd.bind(this);
      this._onTouchMove = this._onTouchMove.bind(this);
    }  

    this._addEvents();
      
    // init var for dimension calcul
    this._initDim(this.$pane1, this.$pane2);

    // init attributes
    this._setAttribute("role","separator");

    if (!this.$elem.hasAttribute('aria-labelled-by') && !this.$elem.hasAttribute("aria-label"))
      this._setAttribute("aria-label","Window splitter");

    this._setAttribute('tabindex', '0');
    this._setAttribute("aria-orientation",this.$orientation);
    this._setAttribute("aria-controls",this.$pane1.id);
    this._setAttribute("aria-valuemin",this._calcValue(this.$minSize));
    this._setAttribute("aria-valuenow",this._calcValue(this._getDimension(this.$pane1)));
    this._setAttribute("aria-valuemax",this._calcValue(this.$maxSize));
  }  
	
  // methods
  _setAttribute(attr,value) {
    this.$elem.setAttribute(attr, value);
  }
  
  _calcValue(value) {
    const total = this.$totalSize;
    if (total > 0) 
      return Math.round(value * 10000 / total)/100;
    else
      return 0;
  }

  _getDimension(elem) {
    return elem.getBoundingClientRect()[this.$cssOrient];
  }

  _setDimension(elem, dimension) {
    if(this.$unit === "rem")
      elem.style[this.$cssOrient] = Math.round(Splitter.pxToRem(dimension)*1000)/1000 + "rem"; // 3 significative digits max
    else
      elem.style[this.$cssOrient] = Math.round(dimension) + "px";
  }

  _setspeedUp() {
    if (this.$speedUp < this.$maxSpeed)
      this.$speedUp++;
  }

  _initDim(pane1, pane2) {
    this.$isPane1Before = (pane1.compareDocumentPosition(this.$elem) & Node.DOCUMENT_POSITION_FOLLOWING) ===  Node.DOCUMENT_POSITION_FOLLOWING;
    this.$totalSize = this._getDimension(pane1) + this._getDimension(pane2);
    this.$minSize = Splitter.dimToPx(pane1,`min-${this.$cssOrient}`);
    
    if (this.$fixedPosition) {
      this.$maxSize = this._getDimension(pane1);
    } else {
      const maxStyle = Splitter.dimToPx(pane1,`max-${this.$cssOrient}`);
      if (maxStyle === 0 || this.$totalSize <= maxStyle) {
        this.$maxSize = this.$totalSize;
      } else {
        this.$maxSize = maxStyle;
      }
    }
  }

  _setPane(delta) {
    let pane1 = this._getDimension(this.$pane1);

    if (this.$isPane1Before) {
		pane1 += delta;
	} else {
		pane1 -= delta;
	}

    if (pane1 >= this.$maxSize) {
      pane1 = this.$maxSize;
    } else if (pane1 <= this.$minSize) {
      pane1 = this.$minSize;
    }
	
    this._setDimension(this.$pane1, pane1);
      
    this._setAttribute("aria-valuenow",this._calcValue(pane1));
  }
    
  _keyArrow(increase) {
	this._setPane(increase * this.$speedUp);
    this._setspeedUp();
  }

  _removeEvents() {
    this.$elem.removeEventListener('mousedown',this._onMouseDown);
    window.removeEventListener('mouseup', this._onMouseUp);
    document.removeEventListener('keydown', this._onKeyDown);
    document.removeEventListener('keyup', this._onKeyUp);
    this.$elem.removeEventListener('focus', this._onFocus);
	
    if ('ontouchstart' in window) {
      this.$elem.removeEventListener('touchstart',this._onTouchStart,false);
      this.$elem.removeEventListener('touchend',this._onTouchEnd,false);
      this.$elem.removeEventListener('touchcancel',this._onTouchEnd,false);
    }      
  }
    
  _addEvents() {
    this.$elem.addEventListener('mousedown',this._onMouseDown);
    window.addEventListener('mouseup', this._onMouseUp);
    document.addEventListener('keydown', this._onKeyDown);
    document.addEventListener('keyup', this._onKeyUp);
    this.$elem.addEventListener('focus',this._onFocus);
	
    if ('ontouchstart' in window) {
      this.$elem.addEventListener('touchstart',this._onTouchStart, false);
      this.$elem.addEventListener('touchend',this._onTouchEnd, false);
      this.$elem.addEventListener('touchcancel',this._onTouchEnd, false);
    }      
  }    
	
  //events
  _onTouchStart(event) {
    event.preventDefault();
    this.$isMoving = true;
    this.$drag = event.targetTouches[0][this.$mouseOrient];
    this.$touchOrigin = this.$drag;
    this.$startTime = Date.now();

    if (!this.$fixedPosition)
      this._initDim(this.$pane1, this.$pane2);
    this.$elem.addEventListener('touchmove',this._onTouchMove);
  }
  
  _touchSwipe(event) {
    const delta = this.$drag - this.$touchOrigin;
    const elapsedTime = new Date().getTime() - this.$startTime;
    if (elapsedTime <= this.$allowedTime && Math.abs(delta) >= this.$threshold)
      return (delta < 0)? 'min' : 'max' ;
    else
      return "none";
  }  
  
  _onTouchEnd(event) {
    if (this.$isMoving === true) {
      event.preventDefault();
      let delta;
      const size = this._getDimension(this.$pane1);
      this.$elem.removeEventListener('touchmove',this._onTouchMove);
      
      const swipe = this._touchSwipe(event);
      if (swipe !== "none") {
        if (swipe === "min") { 
		      delta = this.$minSize - size;
	      } else if (swipe === "max") {
          delta = this.$maxSize - size;
	      }
        this._setPane(delta);	  
      } else {
        if (this.$fixedPosition && (size !== this.$minSize || size !== this.$maxSize)) {
          if (size <= this.$maxSize/2) {
	        delta = this.$minSize - size;
          } else {
            delta = this.$maxSize - size;
          }
          this._setPane(delta);
        }
      }
      this.$isMoving = false;
    }    
  }

  _onTouchMove(event) {
    event.preventDefault();
    const current = event.targetTouches[0][this.$mouseOrient];
    this._setPane(current - this.$drag);
    
    this.$drag = current;
  }

  _onMouseDown(event) {
    event.preventDefault();
    this.$isMoving = true;
    this.$drag = event[this.$mouseOrient];
	
    if (!this.$fixedPosition)
      this._initDim(this.$pane1, this.$pane2);
    document.addEventListener('mousemove',this._onMouseMove);
  }
  
  _onMouseUp(event) {
    if (this.$isMoving === true) {
      event.preventDefault();
      document.removeEventListener('mousemove',this._onMouseMove);
      const size = this._getDimension(this.$pane1);
      if (this.$fixedPosition && (size !== this.$minSize || size !== this.$maxSize)) {
		let delta;
        if (size <= this.$maxSize/2) {
		  delta = this.$minSize - size;
        } else {
          delta = this.$maxSize - size;
        }
        this._setPane(delta);
      }
      this.$isMoving = false;
    }
  }
  
  _onMouseMove(event) {
    event.preventDefault();
    const current = event[this.$mouseOrient];
    this._setPane(current - this.$drag);
    this.$drag = current;
  }

  _onKeyDown(event) {
    if ((event.target !== this.$elem) || event.defaultPrevented || event.altKey || event.ctrlKey || event.shiftKey || event.metaKey || event.OS)
      return; 

    switch (event.key) {
      case "Down": // IE/Edge specific value
     case "ArrowDown":
         if (!this.$fixedPosition && this.$orientation === "horizontal")
         this._keyArrow(1);
        break;
      case "Right": // IE/Edge specific value
      case "ArrowRight":
        if (!this.$fixedPosition && this.$orientation === "vertical")
          this._keyArrow(1);
        break;
      case "Up": // IE/Edge specific value
      case "ArrowUp":
        if (!this.$fixedPosition && this.$orientation === "horizontal")
          this._keyArrow(-1);
        break;
      case "Left": // IE/Edge specific value
      case "ArrowLeft":
        if (!this.$fixedPosition && this.$orientation === "vertical")
          this._keyArrow(-1);
        break;
      case "Enter":
        const size = this._getDimension(this.$pane1);
        if (Math.abs(size - this.$minSize) >= 1) { // relative unit is approximative
		  this._setPane(this.$minSize - size);
          this.$oldSize = size;
        } else if (this.$oldSize !== 0) {
		  this._setPane(this.$oldSize - this.$minSize);
          this.$oldSize=0;
        } 
        break;
      case "Home":
        this._setPane(this.$minSize - this._getDimension(this.$pane1));
        break;
      case "End":
        this._setPane(this.$maxSize - this._getDimension(this.$pane1));
    }
  }
    
  _onKeyUp(event) {
    if (event.target !== this.$elem)
      return;  
    this.$speedUp = 1;
  }
  
  _onFocus(event) {
    if (!this.$isMoving && !this.$fixedPosition)  //vs mouseDown
      this._initDim(this.$pane1, this.$pane2);
  }  
}

var splitterH = new Splitter("splitterH");
var splitterV = new Splitter("splitterV");
              
            
!
999px

Console