13

I have a list which is rendering images (horizontally with scroll):

<div id="my-cool-wrapper">
  ...
  // My cool wrapper has more elements (apart from list)

  <ul id="the-list" style="display: flex; flex-direction: row; overflow-x: scroll;">
    <li>
      <img src="https://image.shutterstock.com/image-vector/sample-stamp-grunge-texture-vector-260nw-1389188336.jpg" />
    </li>

    <li>
      <img src="https://image.shutterstock.com/image-vector/sample-stamp-grunge-texture-vector-260nw-1389188336.jpg" />
    </li>

    <li>
      <img src="https://image.shutterstock.com/image-vector/sample-stamp-grunge-texture-vector-260nw-1389188336.jpg" />
    </li>
  </ul>
</div>

I would like to transform: scale(1.5) the images on user interaction (e.g. click, hover, etc).

The problem:

  • The images do not overflow outside the parent. I want the images to be fully visible when growing (even if it's outside the list's height).

I thought I could achieve this by setting overflow-y: visible to #the-list. However, according to the CSS overflow-x: visible; and overflow-y: hidden; causing scrollbar issue thread this is not possible.

Is there any alternative to achieving what I want?

Update: A JSFiddle is available to demonstrate the issue: http://jsfiddle.net/f7vdebt2/

I would like for the content of the <ul> to be able to scale beyond its parent boundaries.

EDJ
  • 843
  • 3
  • 17
  • 37
  • Have you seen the alternative that the OP mentioned in the link you sent here? They were apparently able to achieve this by adding a wrapper on top of the `
      `.
    – M0nst3R Apr 11 '22 at 18:00
  • In the linked question, the OP was only trying to hide the scrollbar. There was no scale effect being applied. Adding it to his posted fiddle [results in the same problem](https://i.imgur.com/VSVshmX.png) as asked here. – Besworks Apr 11 '22 at 19:11
  • The problem is `transform` is only cosmetic, doesn't affect DOM sizes actually. If you switch to using `width` instead of `scale()` you would be able to do this. Although in that case, the list will expand with its content on hover. – M0nst3R Apr 11 '22 at 19:18
  • @GhassenLouhaichi @Besworks Thank you both for the insight. I've adjusted the JSFiddle from the related thread to suit my use case and provided a link to it in the question description. So is there no way to achieve a behaviour with `scale()`? If I set `overflow: visible` to both the `
      ` and `
      ` elements, the scale works perfectly for me but it just messes up the horizontal scroll which I want to preserve - which is basically what my problem is as of now.
    – EDJ Apr 11 '22 at 19:25
  • I don't think it's possible, I would suggest going the `width` route, if you transform the `width` instead, you won't get this problem. The only issue with this approach is, instead of the content overflowing the `ul`, the `ul` now expands with its contents on hover. – M0nst3R Apr 11 '22 at 19:29
  • Did you know that rather than using a **jsfiddle**, you can create working examples in a SO question or answer using a **snippet**? – Brett Donald Apr 13 '22 at 02:31
  • You might need to create it inside a canvas, or even a svg, it will be always scaled. It goes beyond CSS anyway. – NVRM Apr 18 '22 at 13:30

9 Answers9

6

This is actually a complex and long-standing problem. Solving it with CSS alone is not feasible.

The trick is to pluck the active element out from the static context and force it to be fixed to the viewport when hovered over. I tried to boil this down to a minimal reproducible example but the more I hacked away at it the more quirks I encountered. With fixed image sizes you can accomplish this with a pretty minimal amount of scripting but there are some usability issues and the more of them I fixed, the more complex the code got.

Ultimately, what I ended up doing was publishing a custom element that handles all of this automagically.

Using it is dead simple:

over-scroll {
  width: 50%;
  margin: 3rem auto;
}

pop-out img {
  height: 100px;
  width: auto;
}
<script type="module" src="https://bes.works/bits/bits/pop-out.js"></script>
<over-scroll>
  <pop-out><img src="https://picsum.photos/720/480"></pop-out>
  <pop-out><img src="https://picsum.photos/480/720"></pop-out>
  <pop-out><img src="https://picsum.photos/500/500"></pop-out>
  <pop-out><img src="https://picsum.photos/640/480"></pop-out>
</over-scroll>

Or you could import the element classes into a script and create them programatically:

import { OverScrollElement, PopOutElement } from './pop-out.js';

let overScroll = new OverScrollElement();
let popOut = new PopOutElement();
popOut.innerHTML = `<h1> Hello! </h1>`;
overScroll.append(popOut);
document.body.append(overScroll);

There's a test page included in the repository and a live demo on my website with additional examples. This should do the trick for you but let me know if there are any tweaks you would need to suit your specific use case.

Besworks
  • 4,123
  • 1
  • 18
  • 34
  • Just want to add a quick note here that I've updated the component to support touch events. – Besworks Apr 12 '22 at 16:28
  • I've updated the component again, this time adding support for [vertical scrolling](https://i.imgur.com/ki04YVIl.png). – Besworks Apr 15 '22 at 19:47
1

try to add these parameters

{
 flex-direction: row;
 flex-wrap: nowrap;
 overflow-x: auto;
}
  • 4
    Your answer could be improved with additional supporting information. Please [edit] to add further details, such as citations or documentation, so that others can confirm that your answer is correct. You can find more information on how to write good answers [in the help center](/help/how-to-answer). – Community Apr 11 '22 at 18:19
  • Thanks for suggesting. I did try this and the horizontal scroll continued to work as previously, but `overflow-y` did not take any effect either way. Looking at [overflow-y](https://developer.mozilla.org/en-US/docs/Web/CSS/overflow-y) documentation it states that *"If `overflow-x` is `hidden`, `scroll` or `auto` and this (`overflow-y`) property is `visible` (default) it will implicitly compute to `auto`"*. So I am still unable to achieve what I wanted. – EDJ Apr 11 '22 at 18:56
1

To be honest, it won't work with only CSS. The best solution in the CSS context will be to move the spacing of the scrollbar down so that it cannot be overlapped.

Another solution would be to use the hovered element only as a trigger to get a copy of the hovered image out of the background and position it with css position so that it lies exactly above the hovered image.

My preferred solution would be to simply remove the navbar and allow scrolling with an arrow on the left and right side. Just like a slider gallery. Of course, this would also be a Javascript solution.

Maik Lowrey
  • 15,957
  • 6
  • 40
  • 79
  • Your first suggestion is not a *solution* and simply implies giving up on achieving the desired effect. It's entirely possible to [break out of the hidden overflow](https://github.com/w3c/csswg-drafts/issues/4092#issuecomment-849664215) using only css but you run into positioning offset issues when scrolling unless you know the current sibling index, exact dimensions of all previous siblings and the parent position. JS simply helps with accounting for these quirks. – Besworks Apr 13 '22 at 17:00
  • Your second *solution* is on the right track but there's a major flaw. If the image has transparency the un-scaled copy would be visible through the hovered one. You would know this if you included an example of your proposed method. My answer addresses this issue plus several other usability issues. – Besworks Apr 13 '22 at 17:00
  • Your preferred *solution* is a viable **alternative** for some use-cases but not all. By the way, this can actually [be accomplished entirely with html+css](https://jsfiddle.net/hwjnaub9/1/) if you don't care about transitions. – Besworks Apr 13 '22 at 17:00
  • 1
    @Besworks Thank you so much for your answer, I really appreciate it. It was constructive and has substance! I like your solution and it is the best of all here and so far. It looks like they put a lot of work into it, so I vote for them. Your implementation falls under the theme 'web component' right? – Maik Lowrey Apr 13 '22 at 21:55
  • That's exactly right, it is built using all 3 parts of the [Web Components](https://developer.mozilla.org/en-US/docs/Web/Web_Components) spec. It's a Custom Element, that uses HTML Templates to [`slot`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/slot) elements into a Shadow DOM. – Besworks Apr 13 '22 at 22:35
1

Didn't have to do much, just added bigger padding to the ul and some hover effects. If this isn't what you're looking for please clarify your question more.

div
{
    overflow: visible;
}

ul{
  position: relative;
  display: flex;
  overflow-x: scroll;
  overflow-y: visible;
  border: 1px solid blue;
  border-radius: 4px;
  margin: 0px;
  padding: 2rem;
}

/* decorations. */
li{
    list-style: none;
    padding: 0.2rem;
    border: 1px solid cyan;
    border-radius: 2px;
    margin: 2px;
    position: relative;
        transition: .2s ease-out;
}
li:hover {
    transition: .2s ease-in;
  transform: scale(1.5);
  background-color: #03fce355;
    cursor: zoom-in;
}

li::before, li::after
{
    font-weight: bold;
    font-size: larger;
    content: "::";
}
<div>
    <ul>
        <li>1</li> <li>2</li> <li>3</li>
        <li>4</li> <li>5</li> <li>6</li>
        <li>7</li> <li>8</li> <li>9</li>
        <li>1</li> <li>2</li> <li>3</li>
        <li>4</li> <li>5</li> <li>6</li>
        <li>7</li> <li>8</li> <li>9</li>
        <li>1</li> <li>2</li> <li>3</li>
        <li>4</li> <li>5</li> <li>6</li>
        <li>7</li> <li>8</li> <li>9</li>
    </ul>
</div>
black-purple
  • 29
  • 1
  • 3
  • 1
    You've missed the mark entirely here. The question is about breaking out of the hidden overflow, not avoiding the overflow in the first place. Imagine an example where you clicked one of these items and it opened a vertical list. The list would be trapped inside the scrollable area and create a vertical scrollbar. To goal is to allow this content to be rendered outside of the scrollable area. – Besworks Apr 15 '22 at 15:35
  • I think I get the picture now. Looks like I didn't understand the question very well. – black-purple Apr 21 '22 at 01:00
0

Hope this is what you need

Let me explain what i did here.

When i fill the container with images and add a position:absolute on them to scale, i encounter a problem which is

when i scroll the scrollbar in x direction X-locations[left] of elements are started changing. 5th or 6th elements scale on far right from where they are in the viewport. When i use console.log to see what are their left location. It shows me 2 locations which is very wierd.

So i collect all the images in JS file and apply them

item.style.left = `${rect.left}px` 

when they are scaling and that solve the issue. You can change divs to li items and make them rely on your scenerio.

const images = document.querySelectorAll('.image');

let rect;

for (let i = 0; i < images.length; i++) {
    const image = images[i];

    image.addEventListener("mouseenter", function (event) {  

        rect = this.getBoundingClientRect();
        GetBigger(this); 

    },false);
}

for (let i = 0; i < images.length; i++) {
    const image = images[i];

    image.addEventListener("mouseleave", function (event) {  

        GetSmaller(this); 

    },false);
}

function GetBigger(item){
    item.style.transform = "scale(1.5)";
    item.style.position = "absolute";
    item.style.left = `${rect.left}px`
    item.style.zIndex = "999"
}

function GetSmaller(item){
    item.style.transform = "scale(1.0)";
    item.style.position = "static";
}
*,
*::before,
*::after {
  box-sizing: border-box;
}

img,
picture {
  max-width: 100%;
  display:block;
}


body{
  min-height: 100vh;
  background-color: bisque;
  overflow: hidden;
}

.wrapper{
  margin: 7.5rem 10rem;
  width: inherit;
  display: flex;
  flex-direction: row;
  overflow-x: scroll;
}

.item img{
  min-width: 520px;
  cursor: pointer;
}
<div class="wrapper">
      <div class="item">
        <img
          class="image"
          src="https://image.shutterstock.com/image-vector/sample-stamp-grunge-texture-vector-260nw-1389188336.jpg"
          alt=""
        />
      </div>
      <div class="item">
        <img
          class="image"
          src="https://image.shutterstock.com/image-vector/sample-stamp-grunge-texture-vector-260nw-1389188336.jpg"
          alt=""
        />
      </div>
      <div class="item">
        <img
          class="image"
          src="https://image.shutterstock.com/image-vector/sample-stamp-grunge-texture-vector-260nw-1389188336.jpg"
          alt=""
        />
      </div>
      <div class="item">
        <img
          class="image"
          src="https://image.shutterstock.com/image-vector/sample-stamp-grunge-texture-vector-260nw-1389188336.jpg"
          alt=""
        />
      </div>
      <div class="item">
        <img
          class="image"
          src="https://image.shutterstock.com/image-vector/sample-stamp-grunge-texture-vector-260nw-1389188336.jpg"
          alt=""
        />
      </div>
      <div class="item">
        <img
          class="image"
          src="https://image.shutterstock.com/image-vector/sample-stamp-grunge-texture-vector-260nw-1389188336.jpg"
          alt=""
        />
      </div>
      <div class="item">
        <img
          class="image"
          src="https://image.shutterstock.com/image-vector/sample-stamp-grunge-texture-vector-260nw-1389188336.jpg"
          alt=""
        />
      </div>
      <div class="item">
        <img
          class="image"
          src="https://image.shutterstock.com/image-vector/sample-stamp-grunge-texture-vector-260nw-1389188336.jpg"
          alt=""
        />
      </div>
      <div class="item">
        <img
          class="image"
          src="https://image.shutterstock.com/image-vector/sample-stamp-grunge-texture-vector-260nw-1389188336.jpg"
          alt=""
        />
      </div>
    </div>
UPinar
  • 1,067
  • 1
  • 2
  • 16
  • While close to the right idea, this method falls short in a few areas. There's an issue that's not really noticeable in the snippet because all the images are the same and there are no transitions. As an image gets raised, the layout actually collapses behind it and all the remaining images shift to the left. You can see this when [testing with a variety of images](https://i.imgur.com/9V1ndUjl.png). Also, as you can see in the linked screenshot larger images are hidden behind the overflow when not hovered. – Besworks Apr 12 '22 at 09:16
  • Also, scrolling is quite difficult as the scaled up images block the scrollbar and scroll events don't pass through to the wrapper element. – Besworks Apr 12 '22 at 09:17
0

Don't know if this work for your requirements.

Basically I

  • added margin-top: 20px; margin-bottom: 20px; to the LI to enlarge the UL enough to avoid the y overflow problem
  • nested the UL in a container DIV with the vertical size of images (approximated in the example)
  • added position: relative; top: -18px; to the UL to make images aligned with the container DIV (again, approximated in the example)
  • added overflow: hidden; to the UL to remove ugly scrollbars from the page
  • added z-index: 100; to the LI.hover to make the enlarged image fully visible
  • added some JavaScript to scroll horizontally with the mouse wheel

const list = document.getElementById('list');
const lis = document.querySelectorAll('li');

[list, ...lis].forEach(_ => _.addEventListener('wheel', event => {
  list.scrollLeft += event.deltaY / 2;
  event.preventDefault();
}));
div {
  overflow: visible;
}

ul {
  display: flex;
  overflow: hidden;
  position: relative;
  top: -18px;
}

li:hover {
  transform: scale(2);
  z-index: 100;
}


/* decorations. */

ul {
  margin: 0px;
  width: 100%;
}

li {
  background-color: #ffffff;
  list-style: none;
  padding: 2px;
  border: 1px solid cyan;
  border-radius: 2px;
  margin: 2px;
  margin-top: 20px;
  margin-bottom: 20px;
  position: relative;
}

li:before,
li:after {
  font-weight: bold;
  font-size: larger;
  content: "::";
}

.list-container {
  height: 30px;
}
<div style="width:550px">
  some other content<br /> some other content<br />
  <div class="list-container">
    <ul id="list">
      <li>0</li>
      <li>1</li>
      <li>2</li>
      <li>3</li>
      <li>4</li>
      <li>5</li>
      <li>6</li>
      <li>7</li>
      <li>8</li>
      <li>9</li>
      <li>0</li>
      <li>1</li>
      <li>2</li>
      <li>3</li>
      <li>4</li>
      <li>5</li>
      <li>6</li>
      <li>7</li>
      <li>8</li>
      <li>9</li>
      <li>0</li>
      <li>1</li>
      <li>2</li>
      <li>3</li>
      <li>4</li>
      <li>5</li>
      <li>6</li>
      <li>7</li>
      <li>8</li>
      <li>9</li>
    </ul>
  </div>
  some other content<br /> some other content<br />
</div>
Daniele Ricci
  • 15,422
  • 1
  • 27
  • 55
  • This kind of works, but if you increase the scale effect you must adjust the margins to match otherwise the excess is cut off at the overflow edge. You can do this automatically with some [calc magic and css vars](https://jsfiddle.net/w3xrmLfu/2/). Unfortunately, this method forces you to give up the scroll bar which may not always be desirable and if the content has nested elements that should expand beyond the overflow, they still can't. – Besworks Apr 15 '22 at 16:57
0

Surely the easiest solution is just to add some padding to the parent?

I also added a white background to the list items, and brought the active one to the top with a z-index style in the :hover rule.

div
{
    overflow: visible;
}

ul
{
  display: flex;
  overflow-x: scroll;
  overflow-y: visible;
}

li:hover {
  transform: scale(2);
  z-index: 99;
}



/* decorations. */
ul
{
    
    border: 1px solid blue;
    border-radius: 4px;
    margin: 0px;
    padding: 1em 1.5em;
}
li
{
    list-style: none;
    padding: 2px;
    border: 1px solid cyan;
    border-radius: 2px;
    margin: 2px;
    position: relative;
    background-color: white;
}
li:before, li:after
{
    font-weight: bold;
    font-size: larger;
    content: "::";
}
<div>
    <ul>
        <li>1</li> <li>2</li> <li>3</li>
        <li>4</li> <li>5</li> <li>6</li>
        <li>7</li> <li>8</li> <li>9</li>
        <li>1</li> <li>2</li> <li>3</li>
        <li>4</li> <li>5</li> <li>6</li>
        <li>7</li> <li>8</li> <li>9</li>
        <li>1</li> <li>2</li> <li>3</li>
        <li>4</li> <li>5</li> <li>6</li>
        <li>7</li> <li>8</li> <li>9</li>
    </ul>
</div>
Brett Donald
  • 6,745
  • 4
  • 23
  • 51
  • Thanks for trying to help but this is not a viable solution. This is altering the requirements of the problem to avoid it rather than solve it. I am trying to achieve a behaviour where the list items can grow beyond the list's height. I do not want to resize the list so that it can contain a scaled version of those. – EDJ Apr 12 '22 at 07:39
0

While not as polished as this answer by Besworks, here is a simple solution in JavaScript.

The idea is to hide the active element (set CSS opacity to zero), and overlay a new DIV with the same content and styling.

The overlay DIV is appended to the DOM as a child of the parent DIV. This way, the overlay sees the overflow: visible; property of the parent DIV, instead of the more complicated overflow property of the UL.

The added DIV is placed at the same position as the active LI, and then transformed.

Of course, we also must remember to remove this added DIV when the user's pointer moves away.

const box = document.getElementById("box");
const outofbox = document.getElementById("outofbox");
const items = Array.from(outofbox.children);
let replacement;

items.forEach(item => {
  item.addEventListener("pointerenter", () => replaceLI(item));
  item.addEventListener("pointerleave", () => replacement.remove());
});

function replaceLI(li) {
  const { x, y } = li.getBoundingClientRect();
  replacement = document.createElement("div");
  replacement.className = "li";
  replacement.style.left = x + "px";
  replacement.style.top = y + "px";
  replacement.innerHTML = li.innerHTML;
  box.appendChild(replacement);
}
div {
    overflow: visible;
}

ul {
  display: flex;
  overflow-x: scroll;
  overflow-y: visible;
}

li:hover {
  opacity: 0;
}

/* decorations. */
ul {
    border: 1px solid blue;
    border-radius: 4px;
    margin: 0px;
    padding: 0px;
}
li, div.li {
    list-style: none;
    padding: 2px;
    border: 1px solid cyan;
    border-radius: 2px;
    margin: 2px;
    display: inline-block;
}
li:before, li:after, div.li:before, div.li:after {
    font-weight: bold;
    font-size: larger;
    content: "::";
}
div.li {
  position: absolute;
  transform: scale(2);
}
<div id="box">
    <ul id="outofbox">
        <li>1</li> <li>2</li> <li>3</li>
        <li>4</li> <li>5</li> <li>6</li>
        <li>7</li> <li>8</li> <li>9</li>
        <li>1</li> <li>2</li> <li>3</li>
        <li>4</li> <li>5</li> <li>6</li>
        <li>7</li> <li>8</li> <li>9</li>
        <li>1</li> <li>2</li> <li>3</li>
        <li>4</li> <li>5</li> <li>6</li>
        <li>7</li> <li>8</li> <li>9</li>
    </ul>
</div>
Jeshurun Hembd
  • 527
  • 7
  • 9
  • 1
    This will not copy any event listeners you may have attached to the content inside of the list items. It also means your CSS rules must be scoped to cover both the original content and the copy. To avoid these issues, we **must** scale the original content and it **must not** move from it's position in the DOM. Additionally, if the content in question happens to be images scaled automatically in a flex layout, with your code as-is, the copied content will not match the flexed size but instead be displayed at it's native size. The same issue applies to any content with % based dimensions. – Besworks Apr 15 '22 at 15:22
0

The transform property cannot increase the height of the parent container, because of the scroll property.

All you need is to give the UL a fixed height greater than the max-size of what the enlarged image will look.

Here is the working example.

div
{
    overflow: visible;
}

ul
{
  align-items: center;
  height: 50px;
  display: flex;
  /* overflow-x: scroll; */
  /* overflow-y: visible; */
  overflow: visible auto;
}

li:hover {
  transform: scale(2);
  /* position: relative; */
}



/* decorations. */
ul
{
    
    border: 1px solid blue;
    border-radius: 4px;
    margin: 0px;
    padding: 0px;
}
li
{
    list-style: none;
    padding: 2px;
    border: 1px solid cyan;
    border-radius: 2px;
    margin: 2px;
    position: relative;
    height: 20px;
}
li:before, li:after
{
    font-weight: bold;
    font-size: larger;
    content: "::";
}
<div>
    <ul>
        <li>1</li> <li>2</li> <li>3</li>
        <li>4</li> <li>5</li> <li>6</li>
        <li>7</li> <li>8</li> <li>9</li>
        <li>1</li> <li>2</li> <li>3</li>
        <li>4</li> <li>5</li> <li>6</li>
        <li>7</li> <li>8</li> <li>9</li>
        <li>1</li> <li>2</li> <li>3</li>
        <li>4</li> <li>5</li> <li>6</li>
        <li>7</li> <li>8</li> <li>9</li>
    </ul>
</div>

Hope it helps.

  • Increasing the height of the element does not at all address what this question asked about. Which specifically was how to render an element **outside** of a parent with a scrolling overflow. The child element should be rendered **above the scrollbar** if necessary and the parent element's height should not need to be fixed or change in any way. You can see a proper working example of this behavior in [my answer](https://stackoverflow.com/a/71839839/6036546). – Besworks Apr 18 '22 at 19:05