Some Gotchas That Got Me
I've been building a web app recently and I've had one of those builds. You know, one of the ones where you seem to stumble across a bunch of bugs or unhelpful behaviours that cause you to question your own life choices...
In the interests of perhaps helping someone else, here are some of the things I've hit:
1. Fixed position isn't always, well, fixed #
I tend to work on the basis that
position:fixed sticks things to the viewport. That's what the spec says anyway:
Fixed positioning is similar to absolute positioning. The only difference is that for a fixed positioned box, the containing block is established by the viewport.
Turns out that's not true if an ancestor of the
position:fixed element has a
transform applied to it. If it does then browsers seem to redefine what "viewport" means, and in particular it seems to mean "the element that has the transform".
/* Applying this makes the
fixed position child 300x200px */
In my head the viewport is the initial containing block, not something else, but there's a bug open for Chrome, and apparently the spec language needs clarification.
Personally I'd make
position: fixed viewport-centric, because I think its current behavior is a little bizarre and I'm not sure the spec language is vague, but maybe I missed something. Here's a demo of translates affecting fixed positioning, in case you want to see it for yourself.
2. Body scrolling is impossible to stop #
Let's say you have a child element that takes over the full viewport with
position: fixed (assuming it can... sigh.) and that the element has
overflow: scroll because its content is longer than the viewport. Totally legit for an app with - say - a details view.
You can’t cancel scroll events, either, so there’s no evt.preventDefault() shenanigans that can save you.
When the child element reaches the limits of its scrollable area, like when the user has scrolled to the top or bottom, the scroll event bubbles up and the body starts scrolling. Even if you tell it not to, seemingly. You can't cancel
scroll events, either, so there's no
evt.preventDefault() shenanigans that can save you.
An idea: how about we don't put anything directly on the
body element? Instead set
height: 100% and set
overflow: hidden and then create a new container element with
overflow: scroll that acts as a replacement for the
body. Now when your child element does its takeover you can set the container to
overflow: hidden! (You can't just do this to the body on the fly because it seems to change the scroll value when you change
overflow and it snaps to the top of the page.)
Problems with this approach are two-fold:
- The container element doesn't get compositor-supported fast scrolling in all browsers. That means slow scrolling and paints on every scroll change. Which generally means you just killed performance.
- It looks weird on mobile. On mobile the address bar either disappears (Chrome on Android) or collapses (Mobile Safari) and this behavior is based on the scroll position of the body. Since we just killed that the bar stays there and you're in an uncanny valley.
Seems to me that this is a good reason to have
3. Popstate causes scrolls #
This one I did not expect. If you're using the HTML5 history gubbins, like
pushState and all that, then the browser stores the scroll position whenever you use it. Then if the user clicks back or forward then the scroll position is restored. There's nothing you can do about it. If you're using the URL to toggle UI elements then it's super jarring to have the page jump around the place.
I was all like you can totally capture the scroll position before the browser changes it in
popstatethen, when it's done, just, you know, set the value back with
window.scrollTo. Haha no.
I did have an epiphany, I was all like you can totally capture the scroll position before the browser changes it in
popstate then, when it's done, just, you know, set the value back with
window.scrollTo. Haha no. Firefox takes it upon itself to restore the scroll position before
popstate fires, so you have no idea what to restore it to.
Probably another thing that could benefit from
beforescroll, i.e. "Dear browser, please do not jump all over the place, I got this."
4. There seems to be no form of clipping that's accelerated #
You know me, I jabber on about performance a lot. I've specifically advocated to sticking to compositor-friendly animations like transform and opacity changes. But occasionally you need to do something which involves something more complex like clipping, where - say - a box expands or contracts and you don't want the contents to flow out. Think a card expansion effect like the ones on Google+'s native app. Yeah, that's the one.
Options for this are:
- Have a container element with
overflow: hiddenand transition
top. Changing those properties triggers layout and paint so that's a performance no-no.
clip: rect(), or its newer cousin,
clip-path. Both of these trigger paint, which is unfortunate as again its difficult to guarantee 60fps when you do that.
On the basis that there's an OpenGL scissor test, I'd hoped
clip might get mapped to a scissor test for elements with their own compositor layer. Seemingly not. Shame.
These are only a few of the issues I hit building my current project, and there have been others I've worked around. But generally I find it's these kinds of problems that cause developers to go offroad and reimplement things like scrolling, the DOM, or painting themselves. In my view that's invariably a bad idea, because you're fighting with the browser and reimplementing anything always involves a lot of extra code, maintenance and cross-browser wrangling.
I also feel like these deficiencies in the platform make it very difficult to make convincing web apps today, and if that's what we're trying to make then we should solve these problems, because they are solvable.
And mostly, what I think we need to do to solve them, is have more control.