Skip to content Skip to sidebar Skip to footer

Scaling A Centered Div In A Scrollable Container

I'm trying to do a scale transformation on a div that is centered on a scrollable container div. The trick i'm using to reflect the new div size after transformation, is using a wr

Solution 1:

Is this what you are after?

I used CSS transitions for the scaling animation.

#centered {
    background-image: linear-gradient(to bottom,yellow,red);
    height: 200px;
    transform: scale(1);
    transition: transform 1s ease-out;
    width: 200px;
}

#scrollable {
    align-items: center;
    border: 1px solid red;
    display: flex;
    height: 300px;
    justify-content: center;
    overflow: auto;
    width: 300px;
}

Update #1

How about this one?

  • Using old school absolute centering (position absolute)
  • I see your point about flexbox. It seems that flexbox centering has some limitations when the centered element is larger than its container.

Solution 2:

I am assuming that parts of the problem you are having can be summarized as:

  1. You have a complex structure in your inner div which you want to scale as a group. (If it were a single element, this would've been easy with a matrix).
  2. When the inner div is scaled beyond the bounds of the container, you don't get scrollbars without controlling the width/height.
  3. You want the inner div to remain centered and at the same time, scrollbars should reflect the correct position.

With this, there are two (three actually) easy options.

Option 1:

Using the same markup in the question, you could keep the div centered when the scale factor is below 1. And for scale factors above 1, you change it to top-left. the overflow: auto on the container will take care of the scroll on its own because the div being scaled (via transform) is wrapped inside of another div. You don't really need Javascript for this.

This solves your problems 1 and 2.

Fiddle 1: http://jsfiddle.net/abhitalks/c0okhznc/

Snippet 1:

* { box-sizing: border-box; }
#container {
    position: relative;
    border: 1px solid red; background-color: blue;
    width: 400px; height: 300px;
    overflow: auto;
}
.wrapper {
    position: absolute; 
    background-color: transparent; border: 2px solid black;
    width: 300px; height: 200px;
    top: 50%; left: 50%;
    transform-origin: top left;
    transform: translate(-50%, -50%); 
    transition: all 1s;
}
.box {
    width: 300px; height: 200px;
    background: linear-gradient(to right, red, white);
    border: 2px solid yellow;
}
.inner { width:50px; height: 50px; background-color: green; }

#s0:checked ~ div#container.wrapper { transform: scale(0.5) translate(-50%, -50%); }
#s1:checked ~ div#container.wrapper { transform: scale(0.75) translate(-50%, -50%); }
#s2:checked ~ div#container.wrapper { transform: scale(1) translate(-50%, -50%); }
#s3:checked ~ div#container.wrapper { top: 0%; left: 0%; transform-origin: top left; transform: scale(1.5) translate(0%, 0%); }
#s4:checked ~ div#container.wrapper { top: 0%; left: 0%; transform-origin: top left; transform: scale(2) ; }
#s5:checked ~ div#container.wrapper { top: 0%; left: 0%; transform-origin: top left; transform: scale(3) ; }
<inputid="s0"name="scale"data-scale="0.5"type="radio" />Scale 0.5
<inputid="s1"name="scale"data-scale="0.75"type="radio" />Scale 0.75
<inputid="s2"name="scale"data-scale="1"type="radio"checked />Scale 1
<inputid="s3"name="scale"data-scale="1.5"type="radio" />Scale 1.5
<inputid="s4"name="scale"data-scale="2"type="radio" />Scale 2
<inputid="s5"name="scale"data-scale="3"type="radio" />Scale 3
<hr><divid="container"><divid="wrapper"class="wrapper"><divid="box"class="box"><divclass="inner"></div></div></div></div>

But this creates another problem. The overflow:auto will cause a jump/flicker when the div is scaled beyond the container. This can be easily solved by making it overflow:scroll to show the scrollbars at all times (the way you are doing it already). Although, it works like a charm in Firefox, Chrome falters here and doesn't update the scrollbar position. The trick here is to use Javascript to force a reflow by changing the overflow to auto once your scaling completes. So you need to delay it a bit.

Fiddle 2: http://jsfiddle.net/abhitalks/u7sfef0b/

Snippet 2:

$("input").on("click", function() {
    var scroll = 'scroll', 
        scale = +($(this).data("scale"));
    if (scale > 1) { scroll = 'auto'; }
    setTimeout(fixScroll, 300, scroll);
});

functionfixScroll(scroll) { $("#container").css({ overflow: scroll }); }
* { box-sizing: border-box; }
#container {
    position: relative;
    border: 1px solid red; background-color: blue;
    width: 400px; height: 300px;
    overflow: scroll;
}
.wrapper {
    position: absolute; 
    background-color: transparent; border: 2px solid black;
    width: 300px; height: 200px;
    top: 50%; left: 50%;
    transform-origin: top left;
    transform: translate(-50%, -50%); 
    transition: all 1s;
}
.box {
    width: 300px; height: 200px;
    background: linear-gradient(to right, red, white);
    border: 2px solid yellow;
}
.inner { width:50px; height: 50px; background-color: green; }

#s0:checked ~ div#container.wrapper { transform: scale(0.5) translate(-50%, -50%); }
#s1:checked ~ div#container.wrapper { transform: scale(0.75) translate(-50%, -50%); }
#s2:checked ~ div#container.wrapper { transform: scale(1) translate(-50%, -50%); }
#s3:checked ~ div#container.wrapper { top: 0%; left: 0%;transform-origin: top left; transform: scale(1.5) translate(0%, 0%); }
#s4:checked ~ div#container.wrapper { top: 0%; left: 0%;transform-origin: top left; transform: scale(2) ; }
#s5:checked ~ div#container.wrapper { top: 0%; left: 0%;transform-origin: top left; transform: scale(3) ; }
<scriptsrc="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script><inputid="s0"name="scale"data-scale="0.5"type="radio" />Scale 0.5
<inputid="s1"name="scale"data-scale="0.75"type="radio" />Scale 0.75
<inputid="s2"name="scale"data-scale="1"type="radio"checked />Scale 1
<inputid="s3"name="scale"data-scale="1.5"type="radio" />Scale 1.5
<inputid="s4"name="scale"data-scale="2"type="radio" />Scale 2
<inputid="s5"name="scale"data-scale="3"type="radio" />Scale 3
<hr><divid="container"><divid="wrapper"class="wrapper"><divid="box"class="box"><divclass="inner"></div></div></div></div>

Option 2:

In order to solve problem 3 of keeping the div centered and at the same time maintaining the correct scrollbar position, you have to fallback on Javascript.

The principle remains the same that for scale factors above 1 you need to reset the top-left and translate positions. You would also need to recalc the scaled width/height and then re-assign that to your wrapper div. Then setting the scrollTop and scrollLeft on the container will be as easy as just getting the difference of the wrapper div and the container div.

This solves your problems 1, 2, and 3.

Fiddle 3: http://jsfiddle.net/abhitalks/rheo6o7p/1/

Snippet 3:

var $container = $("#container"), 
    $wrap = $("#wrapper"), 
    $elem = $("#box"), 
    originalWidth = 300, originalHeight = 200;

$("input").on("click", function() {
    var factor = +(this.value);
    scaler(factor);
});

functionscaler(factor) {
    var newWidth = originalWidth * factor,
        newHeight = originalHeight * factor;
    $wrap.width(newWidth); $wrap.height(newHeight);
    if (factor > 1) {
        $wrap.css({ left: 0, top: 0, transform: 'translate(0,0)' }); 
        $elem.css({ transform: 'scale(' + factor + ')' }); 
        setTimeout(setScroll, 400);
    } else {
        $elem.css({ transform: 'scale(' + factor + ') ' });        
        $wrap.css({ left: '50%', top: '50%', transform: 'translate(-50%, -50%)' });
    }
}

functionsetScroll() {
    var horizontal, vertical;
    horizontal = ($wrap.width() - $container.width()) / 2;
    vertical = ($wrap.height() - $container.height()) / 2;
    $container.stop().animate({scrollTop: vertical, scrollLeft: horizontal}, 500);
}
* { box-sizing: border-box; }
#container {
    position: relative;
    border: 1px solid red; background-color: blue;
    width: 400px; height: 300px;
    overflow: scroll;
}
.wrapper {
    position: absolute; 
    background-color: transparent; border: 2px solid black;
    width: 300px; height: 200px;
    top: 50%; left: 50%;
    transform: translate(-50%, -50%); 
    transition: all 0.5s;
}
.box {
    width: 300px; height: 200px;
    background: linear-gradient(to right, red, white);
    border: 2px solid yellow;
    transform-origin: top left;
    transition: all 0.5s;
}
.inner { width:50px; height: 50px; background-color: green; }
<scriptsrc="https://ajax.googleapis.com/ajax/libs/jquery/2.1.0/jquery.min.js"></script><labelfor="slider1">Scale: </label><inputid="slider1"type="range"min="0.5"max="3"value="1"step="0.25"list="datalist"onchange="scaleValue.value=value" /><outputfor="slider1"id="scaleValue">1</output><datalistid="datalist"><option>0.5</option><option>0.75</option><option>1</option><option>1.5</option><option>2</option><option>3</option></datalist><hr><divid="container"><divid="wrapper"class="wrapper"><divid="box"class="box"><divclass="inner"></div></div></div></div>

All scripts tested with IE-11, GC-43, and FF-38

.

Solution 3:

The hard part of this problem is that, in the middle of the zoom in process, you need to change from a model (where you have still margins) to another model where you need to expand the size of the container.

Since this happens in the middle of the transform, it's difficult to handle with a single property transform

The way that I have found to solve this is to change the min-height and min-width properties. This will give the posibility to auto detect this point and handle it gracefully

I am keeping in JS only the basic functionality, everything else is done in CSS

functionsetZoom3() {
    var ele = document.getElementById("base");
    ele.className = "zoom3";
}

functionsetZoom1() {
    var ele = document.getElementById("base");
    ele.className = "";
}

functionsetZoom05() {
    var ele = document.getElementById("base");
    ele.className = "zoom05";
}
.container {
    width: 300px;
    height: 300px;
    overflow: scroll;
    position: relative;
}

#base {
    width: 300px;
    height: 300px;
    top: 0px;
    left: 0px;
    position: absolute;
    min-width: 300px;
    min-height: 300px;
    transition: min-width 5s, min-height 5s;
}

.inner {
    width: 200px;
    height: 200px;
    background-color: lightgreen;
    background-image: linear-gradient(-45deg, red 5%, yellow 5%, green 95%, blue 95%);
    top: 50%;
    left: 50%;
    position: absolute;
    transform: translate(-50%, -50%); 
    transform-origin: top left;
    transition: transform 5s;
}

#base.zoom3 {
    min-width: 600px;
    min-height: 600px;
}
.zoom3.inner {
    transform: scale(3) translate(-50%, -50%);
}

.zoom05.inner {
    transform: scale(0.5) translate(-50%, -50%);
}

.trick {
    width: 20px;
    height: 20px;
    background-color: red;
    position: absolute;
    
    transform: translate(0px);
}
<divclass="container"><divid="base"><divclass="inner"></div></div></div><buttononclick="setZoom3();">zoom 3</button><buttononclick="setZoom1();">zoom 1</button><buttononclick="setZoom05();">zoom 0.5</button>

The only issue left would be to set the scroll position, just in case you want it to keep in the center

Solution 4:

I've managed to solve it with a touch of logic. I've used GSAP for the animation and updates, but you could easily get it working with vanilla JS:

http://codepen.io/theprojectsomething/pen/JdZWLV

... Because of the need to scroll the parent div to keep the element centred (and no way to animate the scroll) you're not going to be able to get a smooth transition with CSS alone.

Note: Even without the scroll, a pure CSS transition seems to have trouble syncing (the offset, whether top/left, margin, or translate, is always catching up with the scale) ... this may be due to sub-pixel positioning? Someone else may be able to provide further insight here.

Full code from Codepen:

var $inner = document.querySelector('aside'),
    $outer = document.querySelector('main'),
    $anim = document.querySelector('[type="checkbox"]'),
    $range = document.querySelector('[type="range"]'),
    data = {
      width: $outer.clientWidth,
      value: 1
    };

$range.addEventListener('input', slide, false);

functionslide () {
  $anim.checked ? animate(this.value) : transform(this.value);
}

functionanimate (value) {
  TweenLite.to(data, 0.4, {
    value: value,
    onUpdate: transform
  });
}

functiontransform (value) {
  if( !isNaN(value) ) data.value = value;
  
  var val = Math.sqrt(data.value),
      offset = Math.max(1 - val, 0)*0.5,
      scroll = (val - 1)*data.width*0.5;

  TweenLite.set($inner, {
    scale: val,
    x: offset*100 + "%",
    y: offset*100 + "%"
  });
  
  TweenLite.set($outer, {
    scrollLeft: scroll,
    scrollTop: scroll
  });
}

window.onresize = function (){
  data.width = $outer.clientWidth;
  transform(data.value);
};
main, aside:before, footer {
  position: absolute;
  top: 50%;
  left: 50%;
  -webkit-transform: translate(-50%, -50%);
      -ms-transform: translate(-50%, -50%);
          transform: translate(-50%, -50%);
}

main {
  width: 50vh;
  height: 50vh;
  min-width: 190px;
  min-height: 190px;
  background: black;
  overflow: scroll;
  box-sizing: border-box;
}

aside {
  position: relative;
  width: 100%;
  height: 100%;
  border: 1vh solid rgba(0, 0, 0, 0.5);
  background-image: -webkit-linear-gradient(top, yellow, red);
  background-image: linear-gradient(to bottom, yellow, red);
  box-sizing: border-box;
  -webkit-transform-origin: 00;
      -ms-transform-origin: 00;
          transform-origin: 00;
}
aside:before {
  content: "";
  background: black;
  border-radius: 50%;
  width: 10%;
  height: 10%;
  display: block;
  opacity: 0.5;
}

input {
  display: block;
  margin: 3em auto;
}
input:after {
  content: attr(name);
  color: white;
  text-transform: uppercase;
  position: relative;
  left: 2em;
  display: block;
  line-height: 0.9em;
}
<scriptsrc="//cdnjs.cloudflare.com/ajax/libs/gsap/1.16.1/TweenMax.min.js"></script><main><aside></aside></main><footer><inputtype="checkbox"name="animate"checked><inputtype="range"min="0.1"step="0.005"max="3"></footer>

Solution 5:

Matthew King's fiddle was close to being correct.The only thing that needs fixing is the div's offset. If your inner div is exceeding the boundaries of the scrollable div (negative offset) you need to set the offset to 0. Else you want the div to be centered. Centering is however not that trivial. You are zooming the div's background with css, hence you are not affecting the actual width and height of your div and need to parse the scale matrix to calculate it's backgrounds dimension. This solution works for me:https://jsfiddle.net/6e2g6vzt/11/ (at least for FF on Ubuntu, did not test on other browsers yet)It is basically just one function to set the new offset, you don't even have to call it manually due to this line of code

$("#centered").bind("transitionend webkitTransitionEnd oTransitionEnd MSTransitionEnd", setNewOffset);

which calls the setNewOffset function after the transformation is completed. As you can see the image 'jumps' to it's correct position after the transformation, maybe you want to add a smooth effect to cover that but i just wanted to show you how to get the correct offset.

Have a look at jQuery's documentation to learn more about .offset()

Credits: Thanks to Jim Jeffers for the nice callback after transistions https://stackoverflow.com/a/9255507/3586288 Thanks to Lea Verou for the great regex to parse the scale matrix https://stackoverflow.com/a/5604199/3586288

Post a Comment for "Scaling A Centered Div In A Scrollable Container"