89

Is there a good technique to make a resizable split pane in HTML?

May it be done using CSS / jQuery / JavaScript or is there a good JavaScript library that have been used?

(An example of a split pane is the favorites bar in Internet Explorer which you may have docked to the left of your main browser window.)

Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
mortb
  • 9,361
  • 3
  • 26
  • 44
  • 5
    no not frames. good methord is shown on http://www.alistapart.com/articles/holygrail – GajendraSinghParihar Aug 30 '12 at 10:19
  • https://stackoverflow.com/a/53731196/ has a great example of pure-CSS resizable split panes. (Note that the resize-handle is in the bottom-right corner, the divider itself is not draggable.) – Peter Jun 07 '22 at 11:40

14 Answers14

112

I wanted a vanilla, lightweight (jQuery UI Layout weighs in at 185 KB), no dependency option (all existing libraries require jQuery), so I wrote Split.js.

It weights less than 2 KB and does not require any special markup. It supports older browsers back to Internet Explorer 9 (or Internet Explorer 8 with polyfills). For modern browsers, you can use it with Flexbox and grid layouts.

Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
nathancahill
  • 10,452
  • 9
  • 51
  • 91
  • 4
    PS: Weight isn't only a question of file size, but of performances. JQuery and JQuery-UI are very heavy in that regard. So thank you very much :) – Domino Nov 24 '15 at 17:23
  • 2
    I'm playing around with split.js now, and when you create a Split instance you can supply a sizes optional parameter. If you have two panes, and you supply [70,30], it becomes 70% for pane 1 and 30% for pane 2. I'm looking at panes for a vertical split I'm doing and I'd really like to be able to have the bottom pane at a fixed size (e.g. 25px) and the top pane take up the remainder of the space. Is that possible? – JustLooking Sep 06 '17 at 19:00
  • Thanks for the wonderful work, but does it support click the gutter to collapse? –  Jan 13 '18 at 23:50
  • I understand how to use the api methods now, but it doesn't appear to provide a way out of the box to add any extra offset in the size calculations (you ***can*** define the elementStyle method as [mentioned in the Usage Examples here](https://nathancahill.github.io/Split.js/), but you're only able to define an extra offset for all split panes, and not individual offsets for each one). So I've added a sizeOffsets option and created a [pull request](https://github.com/nathancahill/Split.js/pull/148). – jbyrd Sep 14 '18 at 20:49
  • Tried it in Chrome; it splits the `
    ` elements, but the gutter is invisible and dragging it does nothing. The question was about a draggable splitter.
    – personal_cloud Mar 16 '19 at 22:13
  • 2
    @personal_cloud There's a [demo of it here](https://split.js.org/). When I tried that using Chrome I can see and drag the splitter, can't you? – ChrisW Apr 25 '19 at 13:08
  • @ChrisW That's funny, I read the text and didn't realize the page itself was a demo. Anyway, when I got one step ahead and tried the code, it had the problem I mentioned. Seems difficult to integrate. But if you have time to get it working, go for it. – personal_cloud Apr 25 '19 at 18:39
  • @nathancahill, does your library support nesting? – Pete Alvin Jun 13 '19 at 12:00
  • @PeteAlvin Yes. – nathancahill Jun 17 '19 at 19:46
  • @vettipayyan please open an issue or a PR if we can improve the docs. Thanks! – nathancahill Aug 15 '19 at 20:03
48

Simplest HTML + CSS accordion, with just CSS resize.

div {
  resize: vertical;
  overflow: auto;
  border: 1px solid
}
.menu {
  display: grid
  /* Try height: 100% or height: 100vh */
}
<div class="menu">
  <div>
    Hello, World!
  </div>
  <div>
    Hello, World!
  </div>
  <div>
    Hello, World!
  </div>
</div>

Simplest HTML + CSS vertical resizable panes:

div {
  resize: horizontal;
  overflow: auto;
  border: 1px solid;
  display: inline-flex;
  height: 90vh
}
<div>
  Hello, World!
</div>
<div>
  Hello, World!
</div>

The plain HTML, details element!.

<details>
  <summary>Morning</summary>
  <p>Hello, World!</p>
</details>
<details>
  <summary>Evening</summary>
  <p>How sweat?</p>
</details>

Simplest HTML + CSS topbar foldable menu

div{
 display: flex
}
summary,p{
 margin: 0px 0 -1px 0px;
 padding: 0 0 0 0.5rem;
 border: 1px black solid
}
summary {
  padding: 0 1rem 0 0.5rem
}
<div>
  <details>
    <summary>FILE</summary>
    <p>Save</p>
    <p>Save as</p>
  </details>
  <details>
    <summary>EDIT</summary>
    <p>Pump</p>
    <p>Transfer</p>
    <p>Review</p>
    <p>Compile</p>
  </details>
  <details>
    <summary>PREFERENCES</summary>
    <p>How sweat?</p>
    <p>Powered by HTML</p>
  </details>
</div>

Fixed bottom menu bar, unfolding upward.

div{
 display: flex;
 position: fixed;
 bottom: 0;
 transform: rotate(180deg)
}
summary,p{
 margin: 0px 0 -1px 0px;
 padding: 0 0 0 0.5rem;
 border: 1px black solid;
 transform: rotate(180deg)
}
summary {
  padding: 0 1rem 0 0.5rem;
}
<div>
  <details>
    <summary>FILE</summary>
    <p>Save</p>
    <p>Save as</p>
  </details>
  <details>
    <summary>EDIT</summary>
    <p>Pump</p>
    <p>Transfer</p>
    <p>Review</p>
    <p>Compile</p>
  </details>
  <details>
    <summary>PREF</summary>
    <p>How?</p>
    <p>Power</p>
  </details>
</div>

Simplest HTML full-screen modal popup

.popup > p {
    padding: 1rem;
    margin: 0;
    display: flex;
    flex-direction: column;
    width: 25vw
}
.popup summary {
    padding: 1rem 0.5rem;
    cursor: pointer;
    max-height: 90vh;
    overflow: auto
}
.popup[open] summary {
    background: black;
    color: white;
    padding: 0.5rem;
}

.popup[open] {
    position: fixed;
    /* top: calc(50% - 25vw); */
    left: calc(50% - 15vw);
    outline: 5000px #00000090 solid;
    border: 5px red solid;
    border-radius: 0.5rem;
    z-index: 1;
    max-height: 90vh;
    overflow-y: auto;
    overflow-x: hidden
}

.popup[open] summary::after {
    content: '❌';
    float: right;
}
<details class="popup">
  <summary>HTML popup</summary>
    <p>
      <span>Name</span>
      <input value="HTML" />
      <br>
      <span>Difficulty</span>
      <input type="number" value="3" />
      <br>
      <span>Coolness</span>
      <input type="number" value="100" />
      <br>
      <p><span>Powered by HTML</span></p>
    </p>
</details>

Simplest resizable pane, using JavaScript.

let ismdwn = 0
rpanrResize.addEventListener('mousedown', mD)

function mD(event) {
  ismdwn = 1
  document.body.addEventListener('mousemove', mV)
  document.body.addEventListener('mouseup', end)
}

function mV(event) {
  if (ismdwn === 1) {
    pan1.style.flexBasis = event.clientX + "px"
  } else {
    end()
  }
}
const end = (e) => {
  ismdwn = 0
  document.body.removeEventListener('mouseup', end)
  rpanrResize.removeEventListener('mousemove', mV)
}
div {
  display: flex;
  border: 1px black solid;
  width: 100%;
  height: 200px;
}

#pan1 {
  flex-grow: 1;
  flex-shrink: 0;
  flex-basis: 50%; /* initial status */
}

#pan2 {
  flex-grow: 0;
  flex-shrink: 1;
  overflow-x: auto;
}

#rpanrResize {
  flex-grow: 0;
  flex-shrink: 0;
  background: #1b1b51;
  width: 0.2rem;
  cursor: col-resize;
  margin: 0 0 0 auto;
}
<div>
  <div id="pan1">MENU</div>
  <div id="rpanrResize">&nbsp;</div>
  <div id="pan2">BODY</div>
</div>
NVRM
  • 11,480
  • 1
  • 88
  • 87
  • 3
    I like this answer. One suggestion is to use `panel.style.flexBasis = event.clientX + "px";` with `flex-grow` and `flex-shrink` in the JavaScript version. – Junle Li Jul 29 '20 at 17:26
  • This javascript answer is exactly what I need! Well, I think the last statement in the `end()` function may be `document.body.removeEventListener('mousemove', mV)` . Also, in case the `#pan1` is not the most left of the page, I also use `getBoundingClientRect()` to get the left of the element and calculate the value of the `flex-basis` . Well, actually I am working on a vertical version, so I am using `height` and `min-height` instead of `flex-basis`. – Programus Oct 26 '21 at 05:33
40

Improving on Reza's answer:

  • prevent the browser from interfering with a drag
  • prevent setting an element to a negative size
  • prevent drag getting out of sync with the mouse due to incremental delta interaction with element width saturation

<html><head><style>

.splitter {
    width: 100%;
    height: 100px;
    display: flex;
}

#separator {
    cursor: col-resize;
    background-color: #aaa;
    background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='10' height='30'><path d='M2 0 v30 M5 0 v30 M8 0 v30' fill='none' stroke='black'/></svg>");
    background-repeat: no-repeat;
    background-position: center;
    width: 10px;
    height: 100%;

    /* Prevent the browser's built-in drag from interfering */
    -moz-user-select: none;
    -ms-user-select: none;
    user-select: none;
}

#first {
    background-color: #dde;
    width: 20%;
    height: 100%;
    min-width: 10px;
}

#second {
    background-color: #eee;
    width: 80%;
    height: 100%;
    min-width: 10px;
}

</style></head><body>

<div class="splitter">
    <div id="first"></div>
    <div id="separator" ></div>
    <div id="second" ></div>
</div>

<script>

// A function is used for dragging and moving
function dragElement(element, direction)
{
    var   md; // remember mouse down info
    const first  = document.getElementById("first");
    const second = document.getElementById("second");

    element.onmousedown = onMouseDown;

    function onMouseDown(e)
    {
        //console.log("mouse down: " + e.clientX);
        md = {e,
              offsetLeft:  element.offsetLeft,
              offsetTop:   element.offsetTop,
              firstWidth:  first.offsetWidth,
              secondWidth: second.offsetWidth
             };

        document.onmousemove = onMouseMove;
        document.onmouseup = () => {
            //console.log("mouse up");
            document.onmousemove = document.onmouseup = null;
        }
    }

    function onMouseMove(e)
    {
        //console.log("mouse move: " + e.clientX);
        var delta = {x: e.clientX - md.e.clientX,
                     y: e.clientY - md.e.clientY};

        if (direction === "H" ) // Horizontal
        {
            // Prevent negative-sized elements
            delta.x = Math.min(Math.max(delta.x, -md.firstWidth),
                       md.secondWidth);

            element.style.left = md.offsetLeft + delta.x + "px";
            first.style.width = (md.firstWidth + delta.x) + "px";
            second.style.width = (md.secondWidth - delta.x) + "px";
        }
    }
}


dragElement( document.getElementById("separator"), "H" );

</script></body></html>
Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
personal_cloud
  • 3,943
  • 3
  • 28
  • 38
  • For some reason my mouse-down event doesn't have e.x only e.screenX ? – Didier A. May 17 '20 at 06:26
  • 1
    @Didier A. Good catch. While x/y is [documented](https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent), there's a note there not to use it in production code. I guess you've found the real reason (out of curiosity, what browser was missing x/y?) So I've updated my answer to use clientX/clientY. It also seems more consistent between different event types. Reza's original code used clientX/clientY for all events. – personal_cloud Jun 28 '20 at 08:03
  • I think, using flexbox is not necesary the line "element.style.left = [...]", the separator will move automatically ; Also the line to "prevent negative-sized elements" is not necesary for the split to work. – fender0ne Aug 29 '20 at 00:04
  • For me the really useful bit was the `background-image` inline SVG drag-handle which I was able to paste into the [split-grid](https://github.com/nathancahill/split/tree/master/packages/split-grid) (specifically the [vue-split-grid](https://github.com/stijlbreuk/vue-split-grid) wrapper) gutter CSS without modification. Useful if you choose to use a canned splitter. – Robin Macharg Jan 18 '21 at 15:49
  • @fenderOne "The line to prevent negative-sized elements is not necesary for the split to work." Sure, if you're not bothered by the glitches that occur when this line is not there and you drag the mouse outside of the window. Personally those glitches make me feel like I'm fighting it when I try to make one side very small. I wouldn't intentionally deliver software with that kind of glitch. – personal_cloud Dec 12 '22 at 05:12
19

I wrote simple code for it without any third-party library. This code is only for a horizontal splitter (vertical is the same).

function onload()
{
    dragElement( document.getElementById("separator"), "H" );
}

// This function is used for dragging and moving
function dragElement( element, direction, handler )
{
  // Two variables for tracking positions of the cursor
  const drag = { x : 0, y : 0 };
  const delta = { x : 0, y : 0 };
  /* If present, the handler is where you move the DIV from
     otherwise, move the DIV from anywhere inside the DIV */
  handler ? ( handler.onmousedown = dragMouseDown ): ( element.onmousedown = dragMouseDown );

  // A function that will be called whenever the down event of the mouse is raised
  function dragMouseDown( e )
  {
    drag.x = e.clientX;
    drag.y = e.clientY;
    document.onmousemove = onMouseMove;
    document.onmouseup = () => { document.onmousemove = document.onmouseup = null; }
  }

  // A function that will be called whenever the up event of the mouse is raised
  function onMouseMove( e )
  {
    const currentX = e.clientX;
    const currentY = e.clientY;

    delta.x = currentX - drag.x;
    delta.y = currentY - drag.y;

    const offsetLeft = element.offsetLeft;
    const offsetTop = element.offsetTop;


    const first = document.getElementById("first");
    const second = document.getElementById("second");
    let firstWidth = first.offsetWidth;
    let secondWidth = second.offsetWidth;
    if (direction === "H" ) // Horizontal
    {
        element.style.left = offsetLeft + delta.x + "px";
        firstWidth += delta.x;
        secondWidth -= delta.x;
    }
    drag.x = currentX;
    drag.y = currentY;
    first.style.width = firstWidth + "px";
    second.style.width = secondWidth + "px";
  }
}
.splitter {
    width: 500px;
    height: 100px;
    display: flex;
}

#separator {
    cursor: col-resize;
    background: url(https://raw.githubusercontent.com/RickStrahl/jquery-resizable/master/assets/vsizegrip.png) center center no-repeat #535353;
    width: 10px;
    height: 100px;
    min-width: 10px;
}

#first {
    background-color: green;
    width: 100px;
    height: 100px;
    min-width: 10px;
}

#second {
    background-color: red;
    width: 390px;
    height: 100px;
    min-width: 10px;
}
<html>

    <head>
        <link rel="stylesheet" href="T10-Splitter.css">
        <script src="T10-Splitter.js"></script>
    </head>

    <body onload="onload()">
        <div class="splitter">
            <div id="first"></div>
            <div id="separator"></div>
            <div id="second"></div>
        </div>
    </body>

</html>
Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
Reza
  • 3,473
  • 4
  • 35
  • 54
  • 3
    Great solution! By far the best approach so far. This is the first answer that works for me in both Mozilla and Chrome. I have posted a new answer based on your answer, but incorporating two small (but important) fixes to prevent the separator from getting "stuck to the mouse" or out of sync with the mouse due to negative div widths. – personal_cloud Mar 17 '19 at 00:20
14

Here is my lightweight vanilla JavaScript approach, using Flexbox:

http://codepen.io/lingtalfi/pen/zoNeJp

It was tested successfully in Google Chrome 54, Firefox 50, Safari 10, don't know about other browsers.

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
    <script src="https://cdn.rawgit.com/lingtalfi/simpledrag/master/simpledrag.js"></script>

    <style type="text/css">

        html, body {
            height: 100%;
        }

        .panes-container {
            display: flex;
            width: 100%;
            overflow: hidden;
        }

        .left-pane {
            width: 18%;
            background: #ccc;
        }

        .panes-separator {
            width: 2%;
            background: red;
            position: relative;
            cursor: col-resize;
        }

        .right-pane {
            flex: auto;
            background: #eee;
        }

        .panes-container,
        .panes-separator,
        .left-pane,
        .right-pane {
            margin: 0;
            padding: 0;
            height: 100%;
        }

    </style>

</head>

<body>

<div class="panes-container">
    <div class="left-pane" id="left-pane">
        <p>I'm the left pane</p>
        <ul>
            <li><a href="#">Item 1</a></li>
            <li><a href="#">Item 2</a></li>
            <li><a href="#">Item 3</a></li>
        </ul>
    </div>
    <div class="panes-separator" id="panes-separator"></div>
    <div class="right-pane" id="right-pane">
        <p>And I'm the right pane</p>
        <p>
            Lorem ipsum dolor sit amet, consectetur adipisicing elit. A accusantium at cum cupiditate dolorum, eius eum
            eveniet facilis illum maiores molestiae necessitatibus optio possimus sequi sunt, vel voluptate. Asperiores,
            voluptate!
        </p>
    </div>
</div>


<script>


    var leftPane = document.getElementById('left-pane');
    var rightPane = document.getElementById('right-pane');
    var paneSep = document.getElementById('panes-separator');

    // The script below constrains the target to move horizontally between a left and a right virtual boundaries.
    // - the left limit is positioned at 10% of the screen width
    // - the right limit is positioned at 90% of the screen width
    var leftLimit = 10;
    var rightLimit = 90;


    paneSep.sdrag(function (el, pageX, startX, pageY, startY, fix) {

        fix.skipX = true;

        if (pageX < window.innerWidth * leftLimit / 100) {
            pageX = window.innerWidth * leftLimit / 100;
            fix.pageX = pageX;
        }
        if (pageX > window.innerWidth * rightLimit / 100) {
            pageX = window.innerWidth * rightLimit / 100;
            fix.pageX = pageX;
        }

        var cur = pageX / window.innerWidth * 100;
        if (cur < 0) {
            cur = 0;
        }
        if (cur > window.innerWidth) {
            cur = window.innerWidth;
        }


        var right = (100-cur-2);
        leftPane.style.width = cur + '%';
        rightPane.style.width = right + '%';

    }, null, 'horizontal');


</script>

</body>
</html>

This HTML code depends on the simpledrag vanilla JavaScript lightweight library (less than 60 lines of code).

Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
ling
  • 9,545
  • 4
  • 52
  • 49
  • Good effort. But careful, this solution uses a fixed percentage gutter width, so you will get unexpected behavior on narrow or wide screens. – nathancahill May 23 '17 at 18:05
  • I really like this solution and added some ui gravy to make it pretty. Among other things, like @nathancahill noted I made the gutter an absolute value. Hope it helps someone. https://codepen.io/theHeadlessSourceMan/pen/MWpaKGB – TheHeadlessSourceMan May 10 '21 at 19:27
8

Hmm, I came across this property in CSS 3. This might be easier to use.

CSS resize Property

Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
ColacX
  • 3,928
  • 6
  • 34
  • 36
7

In the old days, you would use frames to achieve this. There are several reasons why this approach is not so good. See Reece's response to Why are HTML frames bad?. See also Jakob Nielson's Why Frames Suck (Most of the Time).

A somewhat newer approach is to use inline frames. This has pluses and minuses as well: Are iframes considered 'bad practice'?

An even better approach is to use fixed positioning. By placing the navigation content (e.g. the favorites links in your example) in a block element (like a div) then applying position:fixed to that element and setting the left, top and bottom properties like this:

#myNav {
    position: fixed;
    left: 0px;
    top: 0px;
    bottom: 0px;
    width: 200px;
}

... you will achieve a vertical column down the left side of the page that will not move when the user scrolls the page.

The rest of the content on the page will not "feel" the presence of this nav element, so it must take into account the 200px of space it occupies. You can do this by placing the rest for the content in another div and setting margin-left:200px;.

Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
Faust
  • 15,130
  • 9
  • 54
  • 111
  • Good thorough answer. ...I remember reading JN's article "Why frames suck" 1998... I'm sorry I missed to point out in my original question that I wanted to be able to resize the split. – mortb Aug 30 '12 at 10:39
5

Many missed this post from Barguast on Feb 27 '15 where shows a interesting generic flexbox vertical and horizontal resizer.

Take a look: Flexbox Resizing

Barguast note that "... it only handles items sized with flex-grow. If flex-shrink or flex-basis is defined, then the calculations simply don't work.", and he is looking for a better solution, so do I.

Here is his code for reference:

function manageResize(md, sizeProp, posProp)
{
    var r = md.target;

    var prev = r.previousElementSibling;
    var next = r.nextElementSibling;
    if (!prev || !next) {
        return;
    }

    md.preventDefault();

    var prevSize = prev[sizeProp];
    var nextSize = next[sizeProp];
    var sumSize = prevSize + nextSize;
    var prevGrow = Number(prev.style.flexGrow);
    var nextGrow = Number(next.style.flexGrow);
    var sumGrow = prevGrow + nextGrow;
    var lastPos = md[posProp];

    function onMouseMove(mm)
    {
        var pos = mm[posProp];
        var d = pos - lastPos;
        prevSize += d;
        nextSize -= d;
        if (prevSize < 0) {
            nextSize += prevSize;
            pos -= prevSize;
            prevSize = 0;
        }
        if (nextSize < 0) {
            prevSize += nextSize;
            pos += nextSize;
            nextSize = 0;
        }

        var prevGrowNew = sumGrow * (prevSize / sumSize);
        var nextGrowNew = sumGrow * (nextSize / sumSize);

        prev.style.flexGrow = prevGrowNew;
        next.style.flexGrow = nextGrowNew;

        lastPos = pos;
    }

    function onMouseUp(mu)
    {
        window.removeEventListener("mousemove", onMouseMove);
        window.removeEventListener("mouseup", onMouseUp);
    }

    window.addEventListener("mousemove", onMouseMove);
    window.addEventListener("mouseup", onMouseUp);
}

function setupResizerEvents()
{
    document.body.addEventListener("mousedown", function (md) {
        var target = md.target;
        if (target.nodeType !== 1 || target.tagName !== "FLEX-RESIZER") {
            return;
        }
        var parent = target.parentNode;
        var h = parent.classList.contains("h");
        var v = parent.classList.contains("v");
        if (h && v) {
            return;
        } else if (h) {
            manageResize(md, "scrollWidth", "pageX");
        } else if (v) {
            manageResize(md, "scrollHeight", "pageY");
        }
    });
}

setupResizerEvents();
flex {
    display: flex;
}

flex-item > flex {
    position: absolute;
    width: 100%;
    height: 100%;
}

flex.h {
    -ms-flex-direction: row;
    flex-direction: row;
}

flex.v {
    -ms-flex-direction: column;
    flex-direction: column;
}

flex-item {
    display: flex;
    position: relative;
    overflow: hidden;
}

flex > flex-resizer {
    -ms-flex: 0 0 8px;
    flex: 0 0 8px;
    background: white;
}

flex.h > flex-resizer {
    cursor: ew-resize;
}

flex.v > flex-resizer {
    cursor: ns-resize;
}
<body>
    <flex class="v" style="height: 500px">
        <flex-item style="flex: 1; background: red">Flex 1</flex-item>
        <flex-resizer></flex-resizer>
        <flex-item style="flex: 1; background: blue">
            <flex class="h">
                <flex-item style="flex: 1">Flex 2</flex-item>
                <flex-resizer></flex-resizer>
                <flex-item style="flex: 2; background: green">
                    <flex class="v">
                        <flex-item style="flex: 1; background: pink;">Flex 3</flex-item>
                        <flex-resizer></flex-resizer>
                        <flex-item style="flex: 1">
                            <flex class="h">
                                <flex-item style="flex: 1">Flex 4</flex-item>
                                <flex-resizer></flex-resizer>
                                <flex-item style="flex: 2; background: yellow">Flex 5</flex-item>
                                <flex-item style="flex: 2; background: yellow">Flex 6</flex-item>
                            </flex>
                        </flex-item>
                    </flex>
                </flex-item>
            </flex>
        </flex-item>
    </flex>
</body>

And here is my improved version:

function manageResize(md, sizeProp, posProp) {
    var r = md.target;

    var prev = r.previousElementSibling;
    var next = r.nextElementSibling;
    if (!prev || !next) {
        return;
    }

    md.preventDefault();

    var prevSize = prev[sizeProp];
    var nextSize = next[sizeProp];
    var sumSize = prevSize + nextSize;
    var prevGrow = Number(prev.style.flexGrow);
    var nextGrow = Number(next.style.flexGrow);
    var sumGrow = prevGrow + nextGrow;
    var lastPos = md[posProp];

    function onMouseMove(mm) {
        var pos = mm[posProp];
        var d = pos - lastPos;
        prevSize += d;
        nextSize -= d;
        if (prevSize < 0) {
            nextSize += prevSize;
            pos -= prevSize;
            prevSize = 0;
        }
        if (nextSize < 0) {
            prevSize += nextSize;
            pos += nextSize;
            nextSize = 0;
        }

        var prevGrowNew = sumGrow * (prevSize / sumSize);
        var nextGrowNew = sumGrow * (nextSize / sumSize);

        prev.style.flexGrow = prevGrowNew;
        next.style.flexGrow = nextGrowNew;

        lastPos = pos;
    }

    function onMouseUp(mu) {
        // Change cursor to signal a state's change: stop resizing.
        const html = document.querySelector('html');
        html.style.cursor = 'default';

        if (posProp === 'pageX') {
            r.style.cursor = 'ew-resize'; 
        } else {
            r.style.cursor = 'ns-resize';
        }
        
        window.removeEventListener("mousemove", onMouseMove);
        window.removeEventListener("mouseup", onMouseUp);
    }

    window.addEventListener("mousemove", onMouseMove);
    window.addEventListener("mouseup", onMouseUp);
}

function setupResizerEvents() {
    document.body.addEventListener("mousedown", function (md) {

        // Used to avoid cursor's flickering
        const html = document.querySelector('html');
        
        var target = md.target;
        if (target.nodeType !== 1 || target.tagName !== "FLEX-RESIZER") {
            return;
        }
        var parent = target.parentNode;
        var h = parent.classList.contains("h");
        var v = parent.classList.contains("v");
        if (h && v) {
            return;
        } else if (h) {
            // Change cursor to signal a state's change: begin resizing on H.
            target.style.cursor = 'col-resize';
            html.style.cursor = 'col-resize'; // avoid cursor's flickering

            // use offsetWidth versus scrollWidth (and clientWidth) to avoid splitter's jump on resize when a flex-item content overflow (overflow: auto).
            manageResize(md, "offsetWidth", "pageX");
            
        } else if (v) {
            // Change cursor to signal a state's change: begin resizing on V.
            target.style.cursor = 'row-resize';
            html.style.cursor = 'row-resize'; // avoid cursor's flickering

            manageResize(md, "offsetHeight", "pageY");
        }
    });
}

setupResizerEvents();
body {
    /* margin:0; */
    border: 10px solid #aaa;
}

flex {
    display: flex;
    overflow: hidden;
}

/* flex-item > flex {
    position: absolute;
    width: 100%;
    height: 100%;
} */

flex.h {
    flex-direction: row;
}

flex.v {
    flex-direction: column;
}

flex-item {
    /* display: flex; */
    /* position: relative; */
    /* overflow: hidden; */
    overflow: auto;
}

flex > flex-resizer {
    flex: 0 0 10px;
    /* background: white; */
    background-color: #aaa;
    background-repeat: no-repeat;
    background-position: center;
}

flex.h > flex-resizer {
    cursor: ew-resize;
    background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='10' height='30'><path d='M2 0 v30 M5 0 v30 M8 0 v30' fill='none' stroke='black'/></svg>");
}

flex.v > flex-resizer {
    cursor: ns-resize;
    background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='30' height='10'><path d='M0 2 h30 M0 5 h30 M0 8 h30' fill='none' stroke='black'/></svg>");
}
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>flex-splitter</title>
    <link rel="stylesheet" href="./src/styles.css">
    <script src="./src/index.js" defer></script>
</head>

<body>
    <flex class="v" style="flex: 1; height: 500px;">
        <flex-item style="flex: 1;">Flex 1</flex-item>
        <flex-resizer></flex-resizer>
        <flex class="h" style="flex: 1;">
            <flex-item style="flex: 1; background-color: aqua;">
      
      <!-- 
        The next section is an example to test the splitter when there is content inside a flex-item
      -->
        <section>
                    <div>
                        <label for="CursorCoor" style="display: block;">showCursorCoor: </label>
                        <textarea id="CursorCoor" rows="6" cols="50" wrap="soft" readonly></textarea>
                    </div>
                
                    <br />
                
                    <div>
                        <label for="boxInfo" style="display: block;">showBoxInfo: </label>
                        <textarea id="boxInfo" rows="6" cols="50" wrap="soft" readonly></textarea>
                    </div>
                </section>
        
      </flex-item>
            <flex-resizer></flex-resizer>
            <flex class="v" style="flex: 2; ">
                <flex-item style="flex: 1; background: pink;">Flex 3</flex-item>
                <flex-resizer></flex-resizer>
                <flex class="h" style="flex: 1">
                    <flex-item style="flex: 1; background: green;">Flex 4</flex-item>
                    <flex-resizer></flex-resizer>
                    <flex-item style="flex: 2;">Flex 5</flex-item>
                    <!-- <flex-resizer></flex-resizer> -->
                    <flex-item style="flex: 3; background: darkorange;">Flex 6</flex-item>
                </flex>
            </flex>
        </flex>
    </flex>
    
</body>
</html>

Or see it on Codesandbox:

Edit sad-butterfly-1fwo4

fender0ne
  • 281
  • 4
  • 13
4

You can do it with jQuery UI without another JavaScript library. Just add a function to the .resizable resize event to adjust the width of the other div.

$("#left_pane").resizable({
  handles: 'e', // 'East' side of div draggable
  resize: function() {
    $("#right_pane").outerWidth( $("#container").innerWidth() - $("#left_pane").outerWidth() );
  }
});

Here's the complete JSFiddle.

Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
Colin R. Turner
  • 1,323
  • 15
  • 24
3

One totally different approach is to put things in a grid, such as ui-grid or Kendo's grid, and have the columns be resizable. A downside is that users would not be able to resize the rows, though the row size could be set programmatically.

Steve M
  • 31
  • 1
2

You can use absolute of fixed positioning. This CSS for example will dock a 2em-bar on the left side of your page:

body {
    padding-left: 2.5em;
}
body > #bar {
    position:fixed;
    top:0; left:0;
    width: 2em;
    height: 100%;
    border-right: 2px solid #55F; background: #ddd;
}

(Demo at jsfiddle.net)

Bergi
  • 630,263
  • 148
  • 957
  • 1,375
2

The Angular version with no third-party libraries (based on personal_cloud's answer):

import { Component, Renderer2, ViewChild, ElementRef, AfterViewInit, OnDestroy } from '@angular/core';

@Component({
  selector: 'my-app',
  templateUrl: './app.component.html',
  styleUrls: [ './app.component.css' ]
})

export class AppComponent implements AfterViewInit, OnDestroy {

  @ViewChild('leftPanel', {static: true})
  leftPanelElement: ElementRef;

  @ViewChild('rightPanel', {static: true})
  rightPanelElement: ElementRef;

  @ViewChild('separator', {static: true})
  separatorElement: ElementRef;

  private separatorMouseDownFunc: Function;
  private documentMouseMoveFunc: Function;
  private documentMouseUpFunc: Function;
  private documentSelectStartFunc: Function;

  private mouseDownInfo: any;

  constructor(private renderer: Renderer2) {
  }

  ngAfterViewInit() {

    // Init page separator
    this.separatorMouseDownFunc = this.renderer.listen(this.separatorElement.nativeElement, 'mousedown', e => {

      this.mouseDownInfo = {
        e: e,
        offsetLeft: this.separatorElement.nativeElement.offsetLeft,
        leftWidth: this.leftPanelElement.nativeElement.offsetWidth,
        rightWidth: this.rightPanelElement.nativeElement.offsetWidth
      };

      this.documentMouseMoveFunc = this.renderer.listen('document', 'mousemove', e => {
        let deltaX = e.clientX - this.mouseDownInfo.e.x;
        // set min and max width for left panel here
        const minLeftSize = 30;
        const maxLeftSize =  (this.mouseDownInfo.leftWidth + this.mouseDownInfo.rightWidth + 5) - 30;

        deltaX = Math.min(Math.max(deltaX, minLeftSize - this.mouseDownInfo.leftWidth), maxLeftSize - this.mouseDownInfo.leftWidth);

        this.leftPanelElement.nativeElement.style.width = this.mouseDownInfo.leftWidth + deltaX + 'px';
      });

      this.documentSelectStartFunc = this.renderer.listen('document', 'selectstart', e => {
        e.preventDefault();
      });

      this.documentMouseUpFunc = this.renderer.listen('document', 'mouseup', e => {
        this.documentMouseMoveFunc();
        this.documentSelectStartFunc();
        this.documentMouseUpFunc();
      });
    });
  }

  ngOnDestroy() {
    if (this.separatorMouseDownFunc) {
      this.separatorMouseDownFunc();
    }

    if (this.documentMouseMoveFunc) {
      this.documentMouseMoveFunc();
    }

    if (this.documentMouseUpFunc) {
      this.documentMouseUpFunc();
    }

    if (this.documentSelectStartFunc()) {
      this.documentSelectStartFunc();
    }
  }
}
.main {
  display: flex;
  height: 400px;
}

.left {
  width: calc(50% - 5px);
  background-color: rgba(0, 0, 0, 0.1);
}

.right {
  flex: auto;
  background-color: rgba(0, 0, 0, 0.2);
}

.separator {
  width: 5px;
  background-color: red;
  cursor: col-resize;
}
<div class="main">
  <div class="left" #leftPanel></div>
  <div class="separator" #separator></div>
  <div class="right" #rightPanel></div>
</div>

Running example on Stackblitz

Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
1

I found a working splitter, http://www.dreamchain.com/split-pane/, which works with jQuery v1.9. Note I had to add the following CSS code to get it working with a fixed bootstrap navigation bar.

fixed-left {
    position: absolute !important; /* to override relative */
    height: auto !important;
    top: 55px; /* Fixed navbar height */
    bottom: 0px;
}
Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
TMa
  • 723
  • 1
  • 5
  • 14
0

A good library is Shield UI - you can take a look at their flexible Splitter widget and the rest of the powerful components the framework offers.

Vladimir Georgiev
  • 1,949
  • 23
  • 26