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 bonkers behaviours that cause you to question your own sanity...

  • Last updated: 18 Oct 2014
  • Est. Read Time: 6 mins
  • Tagged: #gotchas, #lesigh, #web

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”.

.container {
  width: 300px;
  height: 200px;

  /* Applying this makes the
     fixed position child 300x200px */
  transform: translateY(10px);
}

.container__fixed-position-child {
  position: fixed;
  width: 100%;
  height: 100%;
  background: blue;
  left: 0;
  top: 0;
}

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 bonkers 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.

Oh, it also applies to will-change.

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 html and body to 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 beforescroll.

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 popstate then, 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:

  1. Have a container element with overflow: hidden and transition width, height, left, and top. Changing those properties triggers layout and paint so that’s a performance no-no.
  2. Use 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. Bummer.

Conclusions

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.