2010-03-16

Declarative, context-dependent CSS selectors

When I designed Oplop's UI, I knew I needed some way to delineate what step was the active step (which are individually contained within section tags). I went with a class called open since jQuery has its toggleClass() function which makes it like flipping a bit. Plus I could style the steps in CSS with a default for all steps and then specialization with the section.open selector for that one step that was currently open. This allowed me to have the UI-specific stuff in regards to what was (not) the currently open step be declared in a CSS file.

But there was a shortcoming with how I did it. Having only two selectors gave me only binary control over the UI in terms of steps. What I really needed was three possible states; not yet opened/upcoming, open, and opened previously/closed. I could have used some JavaScript to easily tweak the CSS as I moved about, but it was so nice to have the UI stuff in a declarative format in the CSS file and not interwoven in my JS code that I didn't want to. This was made especially obvious to me thanks to the Restart button which would need to unroll any changes made by the user; the less I had to keep track of the better as that lessened the chance I missed something.

And then some people complained. They didn't like that the same background color was being used for steps that were upcoming and ones already completed. But as I said I didn't want to start keeping track of more UI stuff in the JS code.

Luckily CSS3 has some selectors that allow for context-dependence. Turns out I could style all steps following the current section.open using the selector section.open ~ section. The tilde selects the next siblings. What that means is that my default section would encode the background color for visited steps. My section.open would override that default rule for the currently open step, and finally my section.open ~ section rule would override the default for all the upcoming steps. And this is all done declaratively so I don't have to do any extra bookkeeping.

But this being the web, I ran into one incompatibility problem. Turns out that WebKit and Gecko (the renderer for Chrome/Safari and Firefox, respectively) do not update nodes affected by a sibling CSS selector the same way. In Firefox, when I went through all steps once, removed the open class for all section tags and then  explicitly added the class to the first step the CSS rules were reapplied as if I had loaded the page from the beginning. But in WebKit the section tags that in no way had their state changed by a removal of the open class did not update properly. My guess is that Gecko keeps track of CSS rules that are dependent on classes and such so that when they change they re-apply the relevant selectors, which includes the sibling selector I'm using. In WebKit I bet for performance reasons they do not re-apply the sibling rules as classes update dynamically. It's a little odd because if I was reading the developer console info properly in Chrome it knows how it should restyle it based on what the CSS panel showed me, but the actual value it was using (and was returned when querying from JS the DOM tree) was the old value. Luckily I am able to trigger a recalculation of the CSS by simply toggling the open twice in a row: $('section').toggleClass('open').toggleClass('open'). That doesn't change state, happens so fast the browser has no flashing, and gets the recalculation right.

Anyway, I am rather happy that CSS provided me with a solution that was stateless (for me, more or less) for UI styling even when there is context to depend upon.