I am trying to create a grid of tiles, which sounds easy enough.
The problem I run into is that I want to have each row overflow in the X axis and each tile to expand on hover.
When the tile is expanded on hover it should only push aside its siblings, not the row above/below.
I have made a version that works, but the code is not exactly elegant and the only reason in works is using negative bottom-margins to push the lines lines together as well as setting the aspect ratio on the background image. The way I understand it this happens because Flex is trying to fit all items into it's container. I also don't quite understand how setting the aspect ratio of the image can override flexbox's need to fit everything inside the frame.
I've made a code pen of it demonstrating the code, but I would love for someone to help me coming up with a better solution since this is quite messy!
If this could be achieved in HTML and CSS only that would be great.
So here's the requirements I want to have:
several rows with 11 tiles that overflows in the X direction without creating scrollbars so that the user can horizontal scroll on each line
when hovering over a tile it should push the items in the same row to the side, but not the line above/below
when hovering the tile size should increase by X%, but the content (text) should not increase in size
the box-shadow should not be cut off by the line below when hovering
Link to codepen
HTML:
<div class="grid">
<div class="row">
<div class="tile">
<div class="text">
<p>Text</p>
</div>
<img class="background-image" src="">
</div>
</div>
</div>
CSS
grid {
display: flex;
flex-direction: column;
width: 100%;
height: 100%;
/* firefox */
scrollbar-width: none;
overflow-x: auto;
}
/* chrome */
.grid::-webkit-scrollbar {
display: none;
}
.row {
position: relative;
display: flex;
align-items: center;
gap: 1vw;
height: 12vw;
margin-bottom: -4vw;
overflow-y: hidden;
/* FireFox */
scrollbar-width: none;
}
/* chrome */
.row::-webkit-scrollbar {
display: none;
}
.tile {
position: relative;
height: 6vw;
width: 10vw;
transition: 350ms all;
}
.tile:hover {
height: 8vw;
width: 13vw;
z-index: 2;
box-shadow: 1vw 0.5vw 1vw #00000075;
}
.text {
position: absolute;
bottom: 0;
font-size: 1.5vw;
height: 30%;
transition: all 250ms;
}
.tile:hover .text {
font-size: 2.5vw;
height: 100%;
z-index: 3;
}
.background-image {
object-fit: cover;
object-position: 50% 10%;
aspect-ratio: 1.62/1;
height: 100%;
}
I have a screen with 3 horizontally aligned main areas:
The sidebar (1) can be collapsed/hidden (to the left). When hidden, the freed-up space shall be given to the visualization (3) whilst the menus width (2) stays the same.
ATM I'm aligning the content like this:
Parent container of (1), (2) & (3): display: flex
(1): width: 260px;
(2): width: 293px;
(3): flex: 1;
When collapsed, I simply set width: 0 for the sidebar.
I know that I could simply animate/transition the width change or go about it by changing and animating/transition the left property or play with margins etc. but all those solutions I can think of trigger the browsers layout step (changing width, left, margin, ...) which I'd like to prevent since that leads to poor animation performance as explained here.
Ideally I'd like to stay with CSS transitions of properties which only trigger the browsers compose step like translate etc.
Unfortunately I can't think of a way to only use those "good" CSS properties and also meet my goal of redistributing the freed-up space from the hidden sidebar to the visualization (3).
Is it even possible to hide the sidebar animated to the left without triggering a re-layout but still redistribute the freed-up space? If not, how can this still be done performantly?
I imagine that this is a pretty common use case in web development, so links to according literature, blogs etc. are welcome as well!
I found a lot of examples on the web regarding animated hiding of sidebars but they either animate the width property or don't redistribute the freed up space to the remaining visible content (e.g. sidebars which are simply shown "above" the main content etc.), so none of the examples I found so far actually meet my described goals...
Please check the following HTML,JS,CSS snippet. You could change the actual widths of the elements to exactly match the ones you need.
Manipulating the width of the sidebar is not a problem. Performance is, as explained in the link you provided. On the finishing lines of this article it is written:
Performance matters to users. Web developers need to build apps that
react quickly and render smoothly. Google performance guru Paul Lewis
is here to help you destroy jank and create web apps that maintain 60
frames per second performance. You'll leave this course with the tools
you need to profile apps and identify the causes of jank. You'll
explore the browser's rendering pipeline and uncover patterns that
make it easy to build performant apps.
So you could re-create the logic of the below example by using Javascript requestAnimationFrame as shown here.
const side = document.querySelector('.side');
const sideToggle = document.querySelector('.sideToggle');
const main = document.querySelector('.main');
sideToggle.addEventListener('click', () => {
if (!sideToggle.classList.contains('active')) {
sideToggle.classList.add('active');
} else {
sideToggle.classList.remove('active');
}
if (!main.classList.contains('full')) {
main.classList.add('full');
} else {
main.classList.remove('full');
}
if (!side.classList.contains('hidden')) {
side.classList.add('hidden');
} else {
side.classList.remove('hidden');
}
});
.container {
display: inline-flex;
flex-direction: row;
align-items: stretch;
align-content: space-evenly;
justify-content: space-evenly;
width: 100%;
}
.side {
display: flex;
flex-direction: column;
align-items: stretch;
align-content: space-evenly;
justify-content: space-evenly;
background: #000;
padding: 0;
width: 20%;
height: 100vh;
transition: width 1s linear;
}
.side a {
color: #fff;
text-decoration: none;
line-height: 1;
height: 1.5rem;
padding: 1rem;
}
.side a:hover {
color: #000;
background: #fff;
}
.side.hidden {
width: 0;
transition: width 1s linear;
}
.sideToggle {
background: #000;
color: #fff;
width: 2rem;
height: 2rem;
position: fixed;
right: .75rem;
bottom: .75rem;
border-radius: 50%;
text-align: center;
cursor: pointer;
z-index: 1001;
}
.sideToggle:after {
content: "\2630";
font-size: 1.25rem;
vertical-align: top;
}
.sideToggle.active:after {
content: "\00D7";
vertical-align: top;
font-size: 1.75rem;
}
.main {
background: red;
width: 80%;
height: 100vh;
transition: width 1s linear;
display: inline-flex;
flex-direction: row;
align-items: stretch;
align-content: space-evenly;
justify-content: space-evenly;
width: 100%;
color: #fff;
}
.main.full {
width: 100%;
transition: width 1s linear;
}
.left {
width: 15rem;
padding: 1rem;
}
.right {
width: calc(100% - 15rem);
background: indigo;
padding: 1rem;
}
<div class="container">
<div class="side">
Home
Page 1
Page 2
Page 3
</div>
<span class="sideToggle active"></span>
<div class="main">
<div class="left">
this width is ment to be static
</div>
<div class="right">
this width is ment to be dynamic
</div>
</div>
</div>
This is another possible way by using mostly translateX and without changing the width of the sidebar.
const side = document.querySelector('.side');
const sideToggle = document.querySelector('.sideToggle');
const main = document.querySelector('.main');
sideToggle.addEventListener('click', () => {
if (!sideToggle.classList.contains('active')) {
sideToggle.classList.add('active');
} else {
sideToggle.classList.remove('active');
}
if (!main.classList.contains('full')) {
main.classList.add('full');
} else {
main.classList.remove('full');
}
if (!side.classList.contains('hidden')) {
side.classList.add('hidden');
} else {
side.classList.remove('hidden');
}
});
.container {
display: inline-flex;
flex-direction: row;
align-items: stretch;
align-content: space-evenly;
justify-content: space-evenly;
width: 100%;
}
.side {
display: flex;
flex-direction: column;
align-items: stretch;
align-content: space-evenly;
justify-content: space-evenly;
background: #000;
padding: 0;
width: 7rem;
transform: translateX(0);
height: 100vh;
transition: transform 1s linear, z-index 1s linear;
z-index: 9999;
position: fixed;
left: 0;
top: 0;
will-change: transform, z-index;
}
.side a {
color: #fff;
text-decoration: none;
line-height: 1;
height: 1.5rem;
padding: 1rem;
}
.side a:hover {
color: #000;
background: #fff;
}
.side.hidden {
transform: translateX(-100%);
transition: transform 1s linear, z-index 1s linear;
z-index: -1;
}
.sideToggle {
background: #000;
color: #fff;
width: 2rem;
height: 2rem;
position: fixed;
right: .75rem;
bottom: .75rem;
border-radius: 50%;
text-align: center;
cursor: pointer;
z-index: 1001;
}
.sideToggle:after {
content: "\2630";
font-size: 1.25rem;
vertical-align: top;
}
.sideToggle.active:after {
content: "\00D7";
vertical-align: top;
font-size: 1.75rem;
}
.main {
background: red;
width: calc(100% - 7rem);
height: 100vh;
transition: transform 1s linear, width 1s linear;
display: inline-flex;
flex-direction: row;
align-items: stretch;
align-content: space-evenly;
justify-content: space-evenly;
color: #fff;
left: 0;
top: 0;
transform: translateX(7rem);
position: absolute;
will-change: transform, width;
}
.main.full {
transform: translateX(0);
width: 100%;
transition: transform 1s linear, width 1s linear;
}
.main .left,
.main.full .left {
flex-grow: 1;
flex-shrink: 1;
width: 15rem;
padding: 1rem;
}
.right {
flex-grow: 2;
flex-shrink: 2;
width: calc(100% - 15rem);
background: indigo;
padding: 1rem;
}
<div class="container">
<div class="side">
Home
Page 1
Page 2
Page 3
</div>
<span class="sideToggle active"></span>
<div class="main">
<div class="left">
this width is ment to be static
</div>
<div class="right">
this width is ment to be dynamic
</div>
</div>
</div>
Althought the solution of user2560539 is nice, it did not suffice my requirements because with a lot of elements in the content area, the manipulation of the sidebar width leads definitely to a performance problem.
The issue is that as soon as you start transforming geometrical or positional properties (width, height, margin, padding, top, left, bottom, right, etc.) the browser starts to recalculate the layout again and again for every frame during the animation. You can see this in the performance tab of Chrome or Firefox as "Layout Shift". This is super laggy as soon as your DOM includes more than a few nodes.
Transformations (translations, rotations, skews, etc.) are much faster because the browser does not need to calculate all values regarding geometry and position over and over again. The browser calculates everything on a per pixel basis.
So I came up with another solution. What I did is to use the fast
transform: scaleX(1.1); //e.g. 1920px / 1745px
CSS property to enlarge the content area. The factor 1.1 is calculated by the width of the full content area (no sidebar visible) divided by the compressed content area (sidebar visible).
With this in mind you can use a simple translation animation to translate the sidebar and to scale the content area. Here is a codepen [slightly stolen from user2560539, thank you :) ]:
https://codepen.io/enne87/pen/MWBaOrp
Of course the content is distorted since scaleX does not only scale the content wrapper, but also all its child elements. If this is a problem for you, you can add a CSS class to the distorted elements which holds the inverse of the scaleX operation:
.make-thinner-initally {
transform: scaleX(0.9088); //e.g. 1745px / 1920px
}
Add this class to the elements as soon as the animation begins and remove it after the animation ends.
By the way, Google Calendar uses a very similar approach when you expand / collapse the left side bar.
I have an element with border-radius applied that appears to leave a sort of trail (visual bug) behind when returning to its normal width after being shrunk to accommodate other enlarged elements. This seems to only happen when the border-radius property is used and the glitch level is proportional to the value of border-radius.
Basically, there are two elements inside a container with display: flex. The second element increases in width on hover and so the other element needs to shrink in order to not overflow. When I stop hovering, on the second element, the first one returns to its normal width, but it leaves a strange visual trail of its edge (pun unintended).
Before hover:
During hover:
After hover (the bug):
#container {
display: flex;
align-items: center;
width: 50%;
margin: 0 auto;
}
#reduce {
background: #eee;
width: 100%;
height: 50px;
border-radius: 30px;
}
#hoverexpand {
transition: all 0.5s ease;
width: 20%;
}
#hoverexpand:hover {
width: 50%;
}
<div id="container">
<div id="reduce">
</div>
<div id="hoverexpand">
<span>Hover this</span>
</div>
</div>
Again, this only happens on Microsoft Edge and I'm baffled as to what might be causing it. Is this a known bug? Is there any workaround?
There is a workaround. You can force Edge to repaint the affected element by promoting it to a composite layer with translateZ.
You only have to set the following rule to your #reduce element:
transform: translateZ(0);
Here is the working example:
#container {
display: flex;
align-items: center;
width: 50%;
margin: 0 auto;
}
#reduce {
flex: 2 0;
background: #eee;
height: 50px;
border-radius: 30px;
transform: translateZ(0);
}
#hoverexpand {
flex: 1 0;
transition: flex 0.5s ease;
}
#hoverexpand:hover {
flex: 2 0;
}
<div id="container">
<div id="reduce">
</div>
<div id="hoverexpand">
<span>Hover this</span>
</div>
</div>
So I was playing around with a layout and I kind of stumbled across a cool trick, but I don't know why it works? Link to example.
http://codepen.io/TaylorHuston/pen/NNjZqZ
<div id="container">
<div id="left">
</div>
<div id="right">
<button id="expand">Expand</button>
</div>
</div>
.....
body {
background-color: green;
}
#container {
display: flex;
background-color: white;
width: 1300px;
height: 500px;
margin: 0px auto;
}
#left {
background-color: grey;
width: 0;
transition: all .2s ease;
}
#left.expanded {
width: 300px;
}
#right {
flex: 0 0 100%;
margin-right: -400px;
}
....
$('#expand').click(function() {
$('#left').toggleClass('expanded');
});
With that code, when you click on the 'expand' button, the left column expands out to 300px wide, and the right column is pushed over 300px. And it is actually pushed over, it doesn't just shrink to accordingly. If no negative margin is set though, nothing happens, since #right is 100% width. Why does a negative margin allow the #right to be pushed when there is something to the left of it, but doesn't actually move or affect it when there isn't?
I'm trying to create a tile layout / grid box layout with dynamic sizing images. The Idea is to create this: http://i.imgur.com/ypmk6yR.jpg
But the size of the box needs to change depending on the width or height of the browser. Even better would be when the boxes also get less per row if the browser is too short in width. A full row of boxes/images should always be the full width of the page. And each image is square.
Someone created this
http://codepen.io/davidkpiano/pen/EaxjBj
With SASS, but I have no clue how to work with SASS but thought it could be achieved without SASS.
This is what I was playing around with but I never got It really working
.img_left {
float: left;
padding-bottom: 500px;
}
.img_left img {
width: 19.82vw;
height: 19.82vw;
}
.img_work img {
width: 19.82vw;
height: 19.82vw;
float: left;
}
.img_left is my div for the very first picture.
Is there a good solution to my problem?
The codepen script you posted is realy simple. Let me "decode" Sass for you, it might be what you looking for:
* {
box-sizing: border-box;
position: relative;
}
.tile {
float:left;
width: 25%;
padding: 25% 0 0 0;
height: 0;
overflow: hidden;
transition: 0.3s all ease-in-out;
}
.tile > img {
position: absolute;
top: 0;
left: 0;
width: 100%;
}
// Make sure rows are flush
.tile:nth-child(4n + 1) {
clear: left;
}
// Small screens
#media (max-width: 768px) {
.tile {
width: 50%;
padding: 50% 0 0 0;
}
.tile:nth-child(2n + 1) {
clear: left;
}
}
Here is a good SASS to CSS converter for you to use in the future. At least until you become more familar with SASS.
http://sassmeister.com/