Position element fixed vertically, absolute horizontally
Here's what I'm trying to accomplish:
I need a button which is positioned a certain distance from the right side of a div, and is always that same distance from the side of the div no matter the size of the viewport, but will scroll with the window.
So it is x pixels from the right side of the div at all times, but y pixels from the top of the view port at all times.
Is this possible?
Solution 1:
Position Fixed Element Horizontally Based Off Another Element
(Disclaimer Note: The solution offered here is not technically "absolute horizontally" as the question title stated, but did achieve what the OP originally wanted, the fixed position element to be 'X' distance from the right edge of another but fixed in its vertical scroll.)
I love these types of css challenges. As a proof of concept, yes, you can get what you desire. You may have to tweak some things for your needs, but here is some sample html and css that passed FireFox, IE8 and IE7 (IE6, of course, does not have position: fixed
).
HTML:
<body>
<div class="inflow">
<div class="positioner"><!-- may not be needed: see notes below-->
<div class="fixed"></div>
</div>
</div>
</body>
CSS (borders and all dimensions for demonstration):
div.inflow {
width: 200px;
height: 1000px;
border: 1px solid blue;
float: right;
position: relative;
margin-right: 100px;
}
div.positioner {position: absolute; right: 0;} /*may not be needed: see below*/
div.fixed {
width: 80px;
border: 1px solid red;
height: 100px;
position: fixed;
top: 60px;
margin-left: 15px;
}
The key is to not set the left
or right
at all for the horizontal on the div.fixed
but use the two wrapper divs to set the horizontal position. The div.positioner
is not needed if the div.inflow
is a fixed width, as the left margin of the div.fixed
can be set to known width of the container. However, if you desire than container to have a percentage width, then you will need the div.positioner
to push the div.fixed
to the right side of the div.inflow
first.
Edit: As an interesting side note, when I set overflow: hidden
(should one need to do that) on the div.inflow
had no effect on the fixed position div being outside the other's boundaries. Apparently the fixed position is enough to take it out of the containing div's context for overflow
.
Solution 2:
After much digging (including this post) I couldn't find a solution that I liked. The Accepted Answer here doesn't do what the OP's title read, and the best solutions I could find admittedly resulted in jumpy elements. Then, it hit me: Have the element be "fixed", detect when a horizontal scroll occurs, and switch it to be absolutely positioned. Here is the resulting code:
View it as a Code Pen.
HTML
<div class="blue">
<div class="red">
</div>
</div>
CSS
/* Styling */
.blue, .red {
border-style: dashed;
border-width: 2px;
padding: 2px;
margin-bottom: 2px;
}
/* This will be out "vertical-fixed" element */
.red {
border-color: red;
height: 120px;
position: fixed;
width: 500px;
}
/* Container so we can see when we scroll */
.blue {
border-color: blue;
width: 50%;
display: inline-block;
height: 800px;
}
JavaScript
$(function () {
var $document = $(document),
left = 0,
scrollTimer = 0;
// Detect horizontal scroll start and stop.
$document.on("scroll", function () {
var docLeft = $document.scrollLeft();
if(left !== docLeft) {
var self = this, args = arguments;
if(!scrollTimer) {
// We've not yet (re)started the timer: It's the beginning of scrolling.
startHScroll.apply(self, args);
}
window.clearTimeout(scrollTimer);
scrollTimer = window.setTimeout(function () {
scrollTimer = 0;
// Our timer was never stopped: We've finished scrolling.
stopHScroll.apply(self, args);
}, 100);
left = docLeft;
}
});
// Horizontal scroll started - Make div's absolutely positioned.
function startHScroll () {
console.log("Scroll Start");
$(".red")
// Clear out any left-positioning set by stopHScroll.
.css("left","")
.each(function () {
var $this = $(this),
pos = $this.offset();
// Preserve our current vertical position...
$this.css("top", pos.top)
})
// ...before making it absolutely positioned.
.css("position", "absolute");
}
// Horizontal scroll stopped - Make div's float again.
function stopHScroll () {
var leftScroll = $(window).scrollLeft();
console.log("Scroll Stop");
$(".red")
// Clear out any top-positioning set by startHScroll.
.css("top","")
.each(function () {
var $this = $(this),
pos = $this.position();
// Preserve our current horizontal position, munus the scroll position...
$this.css("left", pos.left-leftScroll);
})
// ...before making it fixed positioned.
.css("position", "fixed");
}
});
Solution 3:
I arrived here looking for a solution to a similar problem, which was to have a footer bar that spans the width of the window and sits below the (variable height and width) content. In other words, make it appear that the footer is "fixed" with respect to its horizontal position, but retains its normal postion in the document flow with respect to its vertical position. In my case, I had the footer text right-aligned, so it worked for me to dynamically adjust the width of the footer. Here is what I came up with:
HTML
<div id="footer-outer">
<div id="footer">
Footer text.
</div><!-- end footer -->
</div><!-- end footer-outer -->
CSS
#footer-outer
{
width: 100%;
}
#footer
{
text-align: right;
background-color: blue;
/* various style attributes, not important for the example */
}
CoffeeScript / JavaScript
(Using prototype.js).
class Footer
document.observe 'dom:loaded', ->
Footer.width = $('footer-outer').getDimensions().width
Event.observe window, 'scroll', ->
x = document.viewport.getScrollOffsets().left
$('footer-outer').setStyle( {'width': (Footer.width + x) + "px"} )
which compiles into:
Footer = (function() {
function Footer() {}
return Footer;
})();
document.observe('dom:loaded', function() {
return Footer.width = $('footer-outer').getDimensions().width;
});
Event.observe(window, 'scroll', function() {
var x;
x = document.viewport.getScrollOffsets().left;
return $('footer-outer').setStyle({
'width': (Footer.width + x) + "px"
});
});
This works nicely in FireFox, and pretty well in Chrome (it's a little jittery); I haven't tried other browsers.
I also wanted any spare space below the footer to be a different colour, so I added this footer-stretch
div:
HTML
...
</div><!-- end footer -->
<div id="footer-stretch"></div>
</div><!-- end footer-outer -->
CSS
#footer-outer
{
height: 100%;
}
#footer-stretch
{
height: 100%;
background-color: #2A2A2A;
}
Note that for the #footer-stretch div to work, all the parent elements up to the body element (and possibly the html element - not sure) must have a fixed height (in this case, height = 100%).