w3c / csswg-drafts

CSS Working Group Editor Drafts
https://drafts.csswg.org/
Other
4.5k stars 666 forks source link

[selectors] Add :before-load() pseudo-class #4462

Open craigfrancis opened 5 years ago

craigfrancis commented 5 years ago

Page layout will often change when using async JavaScript, which is annoying, especially on a slow internet connection.

Would it be possible to have a CSS selector that's only applied when the JavaScript is being downloaded, and during initial execution.

I'm always reluctant to do something like display: none on some content, then get the JavaScript to show it, as the JavaScript may have a problem (fails to load due to network or ad-blocker, parsing issues, finding the element, etc).

I realise async JavaScript introduces an issue here, because it can download after DOMContentLoaded. And the load event is probably too late (waiting for loads of images), so the exact definition of this pseudo-class is up for debate.


As an example...

On a registration form, it asks if you're a member, and if you select "yes", then additional fields appear.

Today I'd do that by getting the JavaScript to hide the fields on DOMContentLoaded, but this means the additional fields appear while loading.

I'd like to create a rule, that's in a normal <link> style sheet, such as:

html:before-load .member_fields {
    display: none;
}

So the fields are initially hidden, then my JavaScript can set more specific styles; and if I don't, it will fall back to the fields being shown.

As it's async, and the DOM might not contain the fields yet, I suspect my initial JavaScript code would do something like the following (after any compatibility checks):

document.documentElement.className += ' js_register_enabled';
tabatkins commented 5 years ago

As a high-level critique: this doesn't belong on a pseudo-class, as it isn't something that'll vary by element. It belongs in a MQ, like (page-state: loading) or something.


I'd have to go archive-diving, but I'm pretty sure we've discussed something like this in the past. I think there were two big strikes against it:

Like you said tho, it is true that JS cannot both (a) have things invisible immediately on page load, and (b) fail-open by having them become visible if JS is blocked or errors. That would be a decent argument for providing this functionality in a MQ, with examples and warning text telling you how to use it "safely". (Which I suspect would be more natural anyway, as hiding things feels like an "exception" and thus is more natural to segregate away inside a MQ block, rather than hiding by default and putting your normal styles there.)


A big question is thus, what loading transitions would we want to care about? Almost certainly the 'DOMContentLoaded' event, and maybe the 'load' event; anything else? The benefit of those two is that it maps cleanly to the document.readyState enum; a page starts in "loading", goes to "interactive" when DOMContentLoaded fires, and goes to "complete" when load fires.

craigfrancis commented 5 years ago

Thanks @tabatkins.

This probably should be a Media Query.

I originally thought about a pseudo-class because it might help keep rules together:

html:before-load .member_fields,
html[data-member="no"] .member_fields {
    display: none;
}

But I doubt I would actually do this, as the initial load would be a display: none, and after setup I would probably use other styles (e.g. height for animation).


I like your idea of using document.readyState (loading, interactive, complete); but I think we need another state to cover async JavaScript.

As in, if you use:

<script src="./script.js" async="async"></script>

That JavaScript might run after DOMContentLoaded, which is why I use something like:

;(function(document, window, undefined) {

  'use strict';

  if (!document.addEventListener || !document.querySelector) {
    return;
  }

  document.documentElement.className += ' js_register_enabled';

  function init() {
    var inputs = document.querySelectorAll('.member_fields');
    // ...
  }

  if (document.readyState !== 'loading') {
    init();
  } else {
    document.addEventListener('DOMContentLoaded', init);
  }

})(document, window);

Now I could use loading or interactive:

@media (page-state: loading), (page-state: interactive) {
    .member_fields {
        display: none;
    }
}

Which would work, but it would involve waiting for all resources to load (images might be a while).

So is there is a state we can define, a bit like how the load event works, but it just waits for <script> resources; which could be sync, defer, or async?