css scroll-snap: focusing on the element which got snapped to - javascript

I implemented a horizontal grid with some cards in them. The grid uses CSS scroll-snap and it works nicely when navigating with mouse/touchscreen.
The problem occurs when the grid is navigated using a keyboard. Pressing tab after navigating through the grid with arrow keys causes the view to jump back to the element that got the focus, not the card which is current snapped to.
My ideal behaviour when pressing tab is, to focus on the card which is currently snapped to.
Any suggestions to make this possible?

As far as I can tell there is currently no way to handle this natively. Nils Schwebel's answer is not going to be very elegant, but it looks like the best way to go.
Here's a working example:
Note: I've added quite a bit of pure decoration to make it easier to understand, so you may need to pick out the relevant parts after some testing.
const main = document.getElementById("Main"),
sections = document.getElementsByClassName("section");
main.addEventListener('scroll', (e) => {
// Grab the position yo are scrolled to (the top of the viewport)
let pos = main.scrollTop;
for (let i = 0, l = sections.length; i < l; i++) {
// Since our stap-align is centered, get the position of the middle of the viewport relative to the current section's top (if your snap items are not full-height, it might require using half the viewport's height instead)
let relativePos = sections[i].offsetTop - pos + (sections[i].offsetHeight / 2);
// Check if the point we found falls within the section
if (relativePos >= 0 && relativePos < sections[i].offsetHeight) {
sections[i].focus();
break;
}
}
});
body {
margin: unset;
}
main {
display: flex;
flex-wrap: wrap;
align-items: stretch;
justify-content: center;
width: 100%;
height: 100vh;
background-color: #222;
overflow-y: auto;
scroll-snap-type: y mandatory;
}
section {
display: flex;
align-items: center;
justify-content: center;
width: 100%;
height: 100vh;
scroll-snap-align: center;
}
section:focus {
border: none;
outline: none;
}
#s1 {
background-color: #d72748;
}
#s2 {
background-color: #b51f7e;
}
#s3 {
background-color: #e64869;
}
#s4 {
background-color: #e79946;
}
section h2 {
color: white;
}
<main id="Main">
<section class="section" id="s1" tabindex="1" aria-labelledby="a1">
<h2 id="a1">AREA 1</h2>
</section>
<section class="section" id="s2" tabindex="1" aria-labelledby="a3">
<h2 id="a2">AREA 2</h2>
</section>
<section class="section" id="s3" tabindex="1" aria-labelledby="a2">
<h2 id="a3">AREA 3</h2>
</section>
<section class="section" id="s4" tabindex="1" aria-labelledby="a4">
<h2 id="a4">AREA 4</h2>
</section>
</main>

I would add a scroll listener and just check if the element is at the top of the scroll view. You may be able to modify one of these solutions: How to check if element is visible after scrolling?

Related

How to make the last element of every flex-wrapped row take up the remaining space? [duplicate]

My problem is that I want the flexbox with variable range width, and all works well, but not on the last row. I want the same dimension for all children even where the row is not full of children (the last row).
#products-list {
position:relative;
display: flex;
flex-flow: row wrap;
width:100%;
}
#products-list .product {
min-width:150px;
max-width:250px;
margin:10px 10px 20px 10px;
flex:1;
}
I created a dynamic situation in jsFiddle
My flex divs can shrink until 150px and grow up to 250px, but all must be with the same size (and obviously I want a CSS solution, with JS I know the way).
Unfortunately, in the current iteration of flexbox (Level 1), there is no clean way to solve the last-row alignment problem. It's a common problem.
It would be useful to have a flex property along the lines of:
last-row
last-column
only-child-in-a-row
alone-in-a-column
This problem does appear to be a high priority for Flexbox Level 2:
CSS Working Group Wiki - Specification Issues and Planning
https://lists.w3.org/Archives/Public/www-style/2015Jan/0150.html
Although this behavior is difficult to achieve in flexbox, it's simple and easy in CSS Grid Layout:
Equal width flex items even after they wrap
In case Grid is not an option, here's a list of similar questions containing various flexbox hacks:
Properly sizing and aligning the flex item(s) on the last row
Flex-box: Align last row to grid
Flexbox wrap - different alignment for last row
How can a flex item keep the same dimensions when it is forced to a new row?
Selector for an element alone in a row?
Aligning elements in last flexbox row
How can I allow flex-items to grow while keeping the same size?
Left-align last row of flexbox using space-between and margins
Inconsistent margin between flex items on last row
How to keep wrapped flex-items the same width as the elements on the previous row?
How to align left last row/line in multiple line flexbox
Last children of grid get giant gutter cause of flexbox space-between
Managing justify-content: space-between on last row
Flexbox space between behavior combined with wrap
Possible to use CSS Flexbox to stretch elements on every row while maintaining consistent widths?
As a quick and dirty solution one can use:
.my-flex-child:last-child/*.product:last-child*/ {
flex-grow: 100;/*Or any number big enough*/
}
You could try using grid instead of flexbox here:
#products-list {
display: grid;
grid-gap: 5px;
grid-template-columns: repeat(auto-fit, minmax(100px, 250px)); //grid automagic
justify-content: start; //start left
}
Fiddle link
There is a great solution that works always.
add a div with class product (The same class for other items that are under flex) and add a style for this div:height:0px;
you need to add as many dives that are possible to be in one row.
<div class="product" style="height:0px">
as many that can be in one row.
That's all. Works always.
If all your rows have the same number of items, you can use :nth-last-child. For example, if all the rows have 3 items, you can do something like this to remove the margin of the last 3 items:
.container{
display: flex;
flex-wrap: wrap;
background: yellow;
}
.item{
width: calc((100% - 2*10px)/3);
height: 50px;
background: blue;
color: white;
margin-right: 10px;
margin-bottom: 10px;
padding: 5px;
box-sizing: border-box;
}
/* last item of each row */
.item:nth-child(3n){
margin-right: 0;
font-size: 150%;
}
/* last 3 items */
.item:nth-last-child(-n+3){
margin-bottom: 0;
background: green;
}
<div class="container">
<div class="item" >1</div>
<div class="item" >2</div>
<div class="item" >3</div>
<div class="item" >4</div>
<div class="item" >5</div>
<div class="item" >6</div>
<div class="item" >7</div>
</div>
A simple trick adds a flexible space to fill the rest of the last row:
#products-list{
display:flex;
flex-flow: row wrap;
justify-content:space-between;
}
#products-list::after {
content: "";
flex: auto;
flex-basis: 200px;/*your item width*/
flex-grow: 0;
}
But you shouldn't use margins on items then. Rather wrap them into containers with padding.
I used this workaround, even if it's not very elegant and it doesn't use the power of Flexbox.
It can be carried out on the following conditions:
All the items have the same width
The items have a fixed width
You use SCSS/SASS (can be avoided though)
If this is the case, you can use the following snippet:
$itemWidth: 400px;
$itemMargin: 10px;
html, body {
margin: 0;
padding: 0;
}
.flex-container {
display: flex;
flex-direction: row;
flex-wrap: wrap;
margin: 0 auto;
border: solid 1px blue;
}
#for $i from 1 through 10 {
#media only screen and (min-width: $i * $itemWidth + 2 * $i * $itemMargin) {
.flex-container {
width: $i * $itemWidth + 2 * $i * $itemMargin;
}
}
}
.item {
flex: 0 0 $itemWidth;
height: 100px;
margin: $itemMargin;
background: red;
}
<div class="flex-container">
<div class="item"></div>
<div class="item" style="flex: 500 0 200px"></div>
<div class="item"></div>
<div class="item"></div>
<div class="item"></div>
</div>
Here I have created an example on codepen which also implements margin.
The second and the third conditions can be avoided respectively using css variables (if you decided to provide support for it) and compiling the above scss snippet.
Well, it's true, we could do it also before flexbox, but display: flex can be still essential for a responsive design.
I was facing this same issue where I wanted to have a variable number of items in a resizable container.
I wanted to use all of the horizontal space, but have all of the flex items at the same size.
I ultimately came up with a javascript approach that dynamically added padding spacers as the container was resized.
function padLastFormRow() {
let topList = [];
let nSpacersToAdd = 0;
$('#flexContainer').find('.formSpacer').remove();
$('#flexContainer').find('.formItem').each(function(i, formItem) {
topList.push($(formItem).position().top);
});
let allRowLengths = getFlexLineLengths(topList);
let firstRowLength = allRowLengths[0];
let lastRowLength = allRowLengths[((allRowLengths.length) - 1)];
if (lastRowLength < firstRowLength) {
nSpacersToAdd = firstRowLength - lastRowLength ;
}
for (var i = 1; i <= nSpacersToAdd; i ++) {
$('#flexContainer').append(formSpacerItem);
}
}
Please see my Fiddle:
http://jsfiddle.net/Harold_Buchman/z5r3ogye/11/
I was having a similar challenge with menu rows. I wanted more spacing on the top of the second row of menu items.
The use of flex-box's row-gap worked well.
https://developer.mozilla.org/en-US/docs/Web/CSS/row-gap
.menu {
display: flex;
flex-wrap: wrap;
row-gap: 10px;
}
This added a margin-top type effect to menu items were wrapped to the second line.
If all your rows have the same number of items, you can use :nth-last-child. For example, if all the rows have 3 items, you can do something like this:
.container{
display: flex;
flex-wrap: wrap;
background: yellow;
}
.item{
width: calc((100% - 2*10px)/3);
height: 50px;
background: blue;
color: white;
margin-right: 10px;
margin-bottom: 10px;
padding: 5px;
box-sizing: border-box;
}
// last item of each row
.item:nth-child(3n){
margin-right: 0;
background: green;
}
// last 3 items
.item:nth-last-child(-n+3){
margin-bottom: 0;
font-size: 150%;
}
<div class="container">
<div class="item" >1</div>
<div class="item" >2</div>
<div class="item" >3</div>
<div class="item" >4</div>
<div class="item" >5</div>
<div class="item" >6</div>
<div class="item" >7</div>
</div>

Include my Javascript animation on all the html body so it stays while scrolling the page

Hi i'm learning html/css and javascript and I think I'm having an issue with my html structure. Basically what I want to do is that my particles animation stays on the website while scrolling the page. I have a Javascript file that does a getElementById('particles') to run the canvas on a div but it only stays on the first page.
I tried to move the "particles" div as a main div that will contain all the sections but it didn't work.
Here's the repository of the files if anyone is interested: https://github.com/DanielVillacis/DanielVillacis.github.io
Here's my html structure :
document.addEventListener('DOMContentLoaded', function() {
particleground(document.getElementById('particles'), {
dotColor: '#FFFFFF',
lineColor: '#FFFFFF'
});
var intro = document.getElementById('intro');
intro.style.marginTop = -intro.offsetHeight / 2 + 'px';
}, false);
html,
body {
width: 100%;
height: 100vh;
}
canvas {
display: inline-block;
vertical-align: baseline;
}
header,
section {
display: block;
}
#particles {
width: 100%;
height: 100vh;
overflow: hidden;
position: relative;
}
.container {
scroll-snap-type: y mandatory;
overflow-y: scroll;
height: 100vh;
}
section {
height: 100vh;
width: 100%;
display: inline-block;
scroll-snap-align: start;
}
<body>
<div class="container">
<main role="main">
<section class="intro">
<div id="particles">
<header class="splash" id="splash" role="banner">
<div id="intro">
</div>
</header>
</div>
</section>
<section class="AboutMe">
<div class="introduction">
</div>
</section>
<section class="box">
<div class="projectContainer">
</div>
</section>
<section class="Contact">
<h2 class="ContactTitle">Contact</h2>
<div class="contactLinks">
</div>
</section>
</main>
</div>
</body>
Use the CSS position: fixed; property.
With position set to fixed, your canvas is positioned relative to the viewport and hence would remain even while scrolling.
.pg-canvas {
display: block;
position: fixed;
top: 0;
left: 0;
pointer-events: none;
width: 100%;
height: 100vh;
}
You have put the particles (which are shown on a canvas) into a section which will scroll out of view.
The particles library you are using places this canvas just before the element you have given it, which has id particles.
You can fix just the canvas by adding position: fixed to the canvas selector in your style sheet (watch out if you have other canvases to give a more definite selector).
This will work in many cases to fix the canvas with the particles to the viewport. But note this description from MDN
The element is removed from the normal document flow, and no space is
created for the element in the page layout. It is positioned relative
to the initial containing block established by the viewport, except
when one of its ancestors has a transform, perspective, or filter
property set to something other than none (see the CSS Transforms
Spec), in which case that ancestor behaves as the containing block.
(Note that there are browser inconsistencies with perspective and
filter contributing to containing block formation.) Its final position
is determined by the values of top, right, bottom, and left.
You are OK at the moment because you move intro with top but if that were a translate you’d have to put the canvas out of intro.

Grid with fixed column and row (freeze panes) using CSS Grid (and possibly Mask/Clip/Javascript - NOT HTML tables)

I have a need to create a grid of data. Ideally, I would like to use CSS Grid. The grid will be potentially large and need to scroll horizontally and vertically. However, the first row and the first column must always be visible. The effect I'm going for is similar to "freezing panes" in Excel (sometimes called "locking" rows and/or columns).
Additionally, after scrolling, I'll need to be able to click on items in the grid as well as implement some drag/drop behavior.
I have this working using a technique that utilizes HTML tables and the sticky and z-index CSS properties. You can see that working in the following fiddle: https://jsfiddle.net/dmboucher/txcLkq60/26/
Notice how you can scroll horizontally and vertically, but you can always see the first row and the first column.
There are reasons why using html tables for this is less than ideal. I would rather use CSS Grid or FlexBox... no html tables. My experiments with CSS Grid and FlexBox have failed so far.
I investigated the concept of using CSS masking and/or clipping. The idea here would be to have a massive div (the grid) in the background, but only be able to view it "through" a mask "window", then use scrollbars and javascript to move the large div behind the mask such that you can see the desired portion of the grid. Something to that effect. I have not been able to get that to work either.
The only method that has been successful has been with html tables.
Can this be done without using html tables? Other suggestions?
Thank you for your help!
I eventually figured out a way to do this. I laid out a FlexBox grid of div's with the appropriate stylings. Conceptually, I laid out a 9 "cell" matrix like so:
.
.
.
Upper left
Times
Spacer
Employees
Schedule
Vertical Scrollbar
Spacer
Horizontal Scrollbar
Spacer
In this way, Times will scroll horizontally, Employees will scroll vertically, and Schedule will scroll both horizontally and vertically.
The key was that Employees, Schedule, and Vertical Scrollbar all had to have the same computed height. Similarly, Times, Schedule, and Horizontal Scrollbar all had to have the same computed width. If not, then the scrolling would get janky.
Once all the styles were added such that everything rendered properly (i.e. overflow, flex-shrink, etc.), then, I added onscroll functions to the Horizontal and Vertical Scrollbar elements. Whenever horizontal scroll events fire, I synchronize the schedule and times scrollLeft values. Whenever vertical scroll events fire, I synchronize the schedule and times scrollTop values.
The relevant sync functions are like so:
function bodyOnLoad() {
let id = "timeline";
let tag = `<div class="timeline_grid_row timeline_grid_header_row">
<div class="timeline_grid_controls">Upper Left</div>
<div id="${id}_timeline_calendar_container_grid_times" class="timeline_grid_times">
${[...Array(24).keys()].map((i) => {
return `<div class="timeline_grid_cell">hour ${i}</div>`;
}).join("\n")}
</div>
<div class="timeline_grid_spacer_cell ur"> </div>
</div>
<div class="timeline_grid_row timeline_grid_body_row">
<div id="${id}_timeline_calendar_container_grid_employees" class="timeline_grid_employees">
${[...Array(35).keys()].map((i) => {
return `<div class="timeline_grid_employee_cell">worker ${i}</div>`;
}).join("\n")}
</div>
<div id="${id}_timeline_calendar_container_grid_schedule" class="timeline_grid_schedule">
${[...Array(35).keys()].map((i) => {
return `<div class="timeline_grid_schedule_row">
${[...Array(24).keys()].map((j) => {
return `<div class="timeline_grid_cell">content ${i}-${j}</div>`;
}).join("\n")}
</div>`;
}).join("\n")}
</div>
<div class="timeline_grid_scroll_vertical" onscroll="TimelineSynchronizeVerticalScroll('${id}', this)">
${[...Array(35).keys()].map((i) => {
return '<div class="timeline_grid_spacer_cell cr"> </div>';;
}).join("\n")}
</div>
</div>
<div class="timeline_grid_row timeline_grid_footer_row">
<div class="timeline_grid_spacer_cell ll"> </div>
<div class="timeline_grid_scroll_horizontal" onscroll="TimelineSynchronizeHorizontalScroll('${id}', this)">
${[...Array(24).keys()].map((i) => {
return '<div class="timeline_grid_horizontal_filler_cell"> </div>';
}).join("\n")}
</div>
<div class="timeline_grid_spacer_cell lr"> </div>
</div>`;
document.getElementById(`${id}_calendar_container_grid`).innerHTML = tag; // write to DOM
}
function TimelineSynchronizeHorizontalScroll(id, scrollControl) {
document.getElementById(`${id}_timeline_calendar_container_grid_schedule`).scrollLeft = scrollControl.scrollLeft;
document.getElementById(`${id}_timeline_calendar_container_grid_times`).scrollLeft = scrollControl.scrollLeft;
}
function TimelineSynchronizeVerticalScroll(id, scrollControl) {
document.getElementById(`${id}_timeline_calendar_container_grid_schedule`).scrollTop = scrollControl.scrollTop;
document.getElementById(`${id}_timeline_calendar_container_grid_employees`).scrollTop = scrollControl.scrollTop;
}
.timeline_control .timeline_calendar {
display: flex;
width: 500px;
height: 150px;
flex-shrink: 0;
}
.timeline_control .timeline_calendar .timeline_calendar_container_grid * {
display: flex;
}
.timeline_control .timeline_calendar .timeline_calendar_container_grid {
display: flex;
flex-direction: column;
overflow: hidden;
}
.timeline_control .timeline_calendar .timeline_grid_row {
overflow: hidden;
}
.timeline_control .timeline_calendar .timeline_grid_row.timeline_grid_header_row,
.timeline_control .timeline_calendar .timeline_grid_row.timeline_grid_footer_row {
flex-shrink: 0;
}
.timeline_control .timeline_calendar .timeline_grid_controls {
flex-shrink: 0;
width: 150px;
}
.timeline_control .timeline_calendar .timeline_grid_times {
overflow: hidden;
}
.timeline_control .timeline_calendar .timeline_grid_employees {
flex-direction: column;
flex-shrink: 0;
overflow: hidden;
width: 150px;
}
.timeline_control .timeline_calendar .timeline_grid_schedule {
flex-direction: column;
overflow: hidden;
}
.timeline_control .timeline_calendar .timeline_grid_cell {
flex-shrink: 0;
margin: 0 10px;
width: 100px;
}
.timeline_control .timeline_calendar .timeline_grid_scroll_horizontal {
height: 18px;
overflow-x: auto;
overflow-y: hidden;
}
.timeline_control .timeline_calendar .timeline_grid_scroll_vertical {
flex-direction: column;
flex-shrink: 0;
overflow-x: hidden;
overflow-y: auto;
width: 18px;
}
.timeline_control .timeline_calendar .timeline_grid_horizontal_filler_cell {
flex-shrink: 0;
margin: 0 10px;
width: 100px;
}
.timeline_control .timeline_calendar .timeline_grid_spacer_cell {
flex-shrink: 0;
}
.timeline_control .timeline_calendar .timeline_grid_spacer_cell.ur {
width: 8px;
}
.timeline_control .timeline_calendar .timeline_grid_spacer_cell.cr,
.timeline_control .timeline_calendar .timeline_grid_spacer_cell.lr {
width: 18px;
}
.timeline_control .timeline_calendar .timeline_grid_spacer_cell.ll {
width: 150px;
}
<body onload="bodyOnLoad()">
<div class="timeline_control">
<div class="timeline_calendar">
<div id="timeline_calendar_container_grid" class="timeline_calendar_container_grid"></div>
</div>
</div>
</body>
Now I have the UI I'm going for... with no html tables. Mission Accomplished!
I just thought I'd share this solution in case it helps others in the future.
Here is a working fiddle of this solution: https://jsfiddle.net/dmboucher/Lwqdcpv8/

How do I make the center element move to the line above when lacking space?

I have three elements (divs for simplicity's sake). How do I make it so that, when the space is narrowed, the center element pops up to the line above the other two? I'm using media queries right now, but the center content can change width, so it breaks.
For example with:
<div class="container">
<div class="Q">QWERTYUIOP</div>
<div class="A">ASDFGHJKL0</div>
<div class="Z">ZXCVBNM123</div>
</div>
When there is extra space it looks like this
QWERTYUIOP ASDFGHJKL0 ZXCVBNM123
and when there is limited space it looks like this
ASDFGHJKL0
QWERTYUIOP ZXCVBNM123
You can achieve this behavior using media queries. So if you want to force the last 2 elements to next line when screen size is less than 560px, your css and html should be like this:
.container {
display: flex;
flex-wrap: wrap;
justify-content: space-between;
}
.nl {
display: none;
width:100%;
}
#media only screen and (max-width:560px){
.A {
order: 1;
flex: 0 0 100%;
text-align: center;
}
.Q {
order: 2;
}
.Z {
order: 3;
}
}
<div class="container">
<div class="Q">QWERTYUIOP</div>
<div class="A">ASDFGHJKL0</div>
<div class="Z">ZXCVBNM123</div>
</div>
I don't know if the author is allowed to use javascript?! ...but I don't know any other solution.
The css was not shown, so I made my own rules for the main .container. Flex rules are used. Also, I don't know if the class has a .container specific width ?!
In my example, I used the window resize event to visualize how my code works:
window.onresize = function() { ... }
but in production it needs to be replaced with a window load event:
window.onload = function() { ... }
If this example does not suit the author, then I think that this example will be useful to others :)
window.onresize = function() {
let container = document.querySelector('.container');
let Q_div = document.querySelector('.Q');
let A_div = document.querySelector('.A');
let Z_div = document.querySelector('.Z');
let sum_div = Q_div.offsetWidth + A_div.offsetWidth + Z_div.offsetWidth;
if (container.offsetWidth <= sum_div) {
A_div.classList.add('class_for_A');
} else {
A_div.classList.remove('class_for_A');
}
}
.container {
display: flex;
flex-wrap: wrap-reverse;
justify-content: space-between;
width: 500px;
max-width: 100%;
}
.A.class_for_A {
order: 1;
margin: auto;
}
<div class="container">
<div class="Q">QWERTYUIOP</div>
<div class="A">ASDFGHJKL0</div>
<div class="Z">ZXCVBNM123</div>
</div>

How to align html?

I'm using display flex to display multiple items in one big container (parentDiv). The code is working fine but I get big problems with horizontal centering the items (especially If there are only a few items they should get horizontally centered) so I was using justify-content what leads to big issues:
The parent div is not able to display all items anymore. The first item that gets displayed is the item "04" while it should be "01". How to avoid this?
Please have a look at this code:
#bigDiv {
position: absolute;
width: 100%;
height: 100%;
}
#parentDiv {
width: 90%;
height: 50%;
overflow-y: hidden;
overflow-x: scroll;
text-align: center;
white-space: nowrap;
background: red;
display: flex;
justify-content: center;
align-items: center;
}
.item {
color: white;
background: blue;
flex: 0 0 4%;
margin: 0 3%;
}
.item::after {
content: "";
display: block;
padding-bottom: 100%;
}
<div id="bigDiv">
<div id="parentDiv">
<div class="item">01</div>
<div class="item">02</div>
<div class="item">03</div>
<div class="item">04</div>
<div class="item">05</div>
<div class="item">06</div>
<div class="item">07</div>
<div class="item">08</div>
<div class="item">09</div>
<div class="item">10</div>
<div class="item">11</div>
<div class="item">12</div>
<div class="item">13</div>
<div class="item">14</div>
<div class="item">15</div>
<div class="item">16</div>
</div>
</div>
See this image:
My intentions: The parent div should be able to show all of the items (starting with "01" - and the last element should be the "16"-one)
Note: If there are only 4 or less items they should get centered horizontally. (The reason why I added justify-content).
You're fiting 160% into 100%. And you want it centered. And it works: the 160% total width of the resulting children is nicely centered.
But you're also expecting whatever is outside the parent to be accessible.
It's pretty much like making a child element go outside of its parent by -30% to the left or to the top (by any other method) and expecting the parent to allow you to scroll to it. It's not going to happen!
If it did, the child would no longer be placed at -30%, it would be placed at 0%. Scrollbars will never scroll to left or top negative space. It's by design. You need to take it into consideration when designing your page.
Whenever you center a bigger child into a smaller parent you won't be able to use parent's scrollbars to scroll to the beginning of the child. So anything preventing the child positioning in the parent's left negative space will fix it.

Categories