David Khourshid

Crafting Stateful Styles with State Machines

Conference talk at CSSConf Budapest 2019. 28:51 long, 9.9K views on YouTube. Topics: state machines, statecharts, css & animation, visual modeling.

Watch on YouTube (JSConf)

What is this talk about?

“See how we could craft stateful styles with state machines, which Mike talked about at the very beginning.”

“Easily see all these possible states and be able to know how states change from one state to another in our app.”

From the opening of the talk.

Key takeaways

  1. See how we could craft stateful styles with state machines, which Mike talked about at the very beginning. 0:00
  2. Easily see all these possible states and be able to know how states change from one state to another in our app. 7:00
  3. Statecharts extend the idea of state machines by adding things such as actions, whether you're entering state, exiting a state, or performing a transition between states. 20:00
  4. Instead of instead of structuring it in a nice modeled way, it increases linearly, but, as we saw, finite state machines can increase exponentially. 27:00

Quoted from the transcript.

Full transcript

From YouTube auto-captions. Punctuation and capitals were restored automatically. Technical terms were normalized; some errors remain. 4,892 words.

0:00 Introduction

Hello everyone, I'm really excited to be here because the first international conference talk that I ever gave was actually right here. On this stage. I came in 2016 for. I think it was the very first CSS company the past. And what I talked about was actually styling using just CSS and HTML. But now today, we're gonna bring CSS, HTML in, the little bit of JavaScript together. And see how we could craft stateful styles with state machines, which Mike talked about at the very beginning. So basically, the hello world of any design system, any component library, is, of course, your button, so imagine we have this button, you get this design, you're happy it looks good, but there's many different variants, like Mike talked about that. This button can be in. So we have to consider what happens if you hover the button, what happens if the button is active, and what happens that the button is disabled. So you might be thinking, we're done here, but we have to also think about the application states that this button can be into.

For example, if we're fetching data, we might want to fade it out, if it's a success, we might want to have it be green and say, Hey, we've got your data. Or, if it's an error, you might want it to be a different variants such as red. But looking at application behavior and component Styles this way is very limited. Because we also have to think about how we transition between these states. To illustrate this problem, let's say we have this fetch button, user clicks it. We're waiting for data and we eventually get it. That's great, but what happens if the data takes a long time to load? Then maybe we want to show a spinner and eventually we get our data. But if you're like me, then you'll get frustrated after a while and start clicking this button a few times. And this is gonna make a lot of unnecessary API requests until you finally get your data.

But you're a developer and you're smart, you think, you know what? I'm going to disable the button? And that way, they could only click it once they have to wait for the data, and then it comes back successfully. But guess what, I'm a developer too, I know Chrome Dev. Tools. So I could just go in under Sabol, that button and make as many requests as I want. So I ran into this problem a lot of times. And I wasn't always good at JavaScript. Which is why, at the very beginning, I decided to see how much I could do with just HTML and CSS. This was one of the very first code pens that I made, using hover states. To just go between the days and recreate this dribble by 2bic studio as well as this one now to do. To do complex animations like this, you sort of have to use a few hacks if you're not going to use JavaScript.

Play from 0:00

3:00 I used check boxes because check boxes can represent states and…

And so that's exactly what I did, just like, was talked about in the very first talk. I used check boxes because check boxes can represent states and radio buttons as well, so what I did here was if you have a checkbox and it sort of lives. Outside the app, you could use the squiggly selector, a tilde selector, which is more formally known as the adjutant sibling selector. But I mean, no one's gonna remember that. And you could apply a label inside the app that labels are great because they magically map the check box over to the label in your app, so when you click the label, it doesn't matter where it is. It will check that checkbox. And so what happens is when you click that label, which could be a button or anything else, then that check box is going to be checked. You could use the squiggly selector and select that box and make it change styles. And you could also do this with more than one check box, which gives you sort of that dynamic behavior and dynamic styling. Now, like I mentioned, you could do this with both check boxes and radios, with one distinction with check boxes, you could have zero or more check boxes checked.

But while I found myself using more and more for radio buttons, because it enforced the idea that you could have only one of them checked at a time. So I was playing around with this and I was playing around with crazier things like this. CSS only dog animation that I showed at the very first CSS company, the Pest that I was at, and I decided. To go with my friends Sha. And we do. This weekly livestream called the key Framers, in which we bring imaginative user interfaces to, like using HTML, CSS, and a little bit of JavaScript. So what we do is we take these really complex animations from Tribble and we try to recreate them live on the air in under two hours. But what we found ourselves seeing more and more is that a lot of these animations are states based, and not just two states.

Play from 3:00

5:00 If the logic changes of the design changes…

So we can't use a checkbox, but three states. Where you click and you know, you might go back, or you might have this play button and then you go to something else. So that's more easily illustrated here, because we have this plateless state at the top, which shows what state our app is in. And you could scroll through these albums and this is another recreation of a dribble. And so you could drag back and forth and you could see that. The state of our entire application changes as the user interacts with this application. So you could see those moving side to side. So this sort of illustrates the problem that we have with our currents design systems. In the current way that we develop is that a designer might present you a very high fidelity mock-up, or, you know, just like a simple prototype. But that mock-up represents only one of potentially many, if not hundreds thousands of possible states that your app of your components can be in.

And so the problem, I'm sorry, the problem is that this Ireson cycle is a bit slow. Because not only does a designer have to make all of those changes in all of those different screens. But if the logic changes of the design changes, you have to go through all of those possible changes or all those possible screens and apply those changes. So what, what we do currently today? And I'm sure most of you are guilty of this, I know I was guilty of the for the longest time is we apply classes. We're like, okay, if this button is in the loading state, then we have a class of loading. Of course, so we could do the same with success, but the problem with classes we can't enforce that, we only have one possible value, so we. Might have a button that shows loading and success, and some of you might think like, I'm, I'm smart enough not to do this. I have class list, I could remove the loading class and then at the success class by this.

Many of you know this does happen in the real world, and this is something that we wants to avoid. So is there a better way that we could model the state for our dynamic UI's and have it in a way that we could visualize it? And easily see all these possible states and be able to know how states change from one state to another in our app. And can we do it in a more formal way that designers and developers can understand equally? So I looked at a lot of different prototyping apps like Origami design, Envision app, which you define each of the different states in it smoothly transitions between each of them. Prototype does the same thing, where it's sort of like magic move for designers, because you could define shapes and it will magically transform.

Play from 5:00

8:00 I employed the use of data attributes…

Then scale them, translate them into the shape that they're supposed to be. And you could add triggers, and then there's more formal user flow tools, such as overflowed IO, in which you could design each of these screens and add these custom triggers. So what I realized was common between all of these tools and more, is that user flows are these transitions between these user interface states. And these transitions could be caused based on either user events or other external events, such as a timer, an API, requests, coming in, etc. And so this brings me to the topic of finite state machines and statecharts, which, I mean, if you know me long enough, you know that this is what I talked about. You could type state machine on Twitter and I will find you and I will respond to you. It's just my thing. One of my favorite definitions of the states is actually in the ARIA guidelines in the W3..org website, where a states is a dynamic property that expresses the characteristics of an object in a certain point of time.

So this point of time can be in response to a user action or other automated processes or external events that come in. And so this is a distinction. Between properties such as like, an anchor might have an H ref tag, or an HREF attribute, or a button can have a variant. Where you know that's not going to change, but state is different, state describes. At one point in time, this represents my component, and that could change at any time. And area also has all of these builds and attributes for states that they describe a stateful attributes which describe, you know, certain finite states of components, such as invalid, valid, disabled currents. And yeah, so finite state machines are pretty much the same concept where it has five parts. I'm gonna review finite state machines real briefly. There's a ton of resources on this, though, but the finite state machine is one that has an initial state.

It has a finite number of states, which means we could only be in one of those states at a time, it has a finite number of vents. And these are the signals that cause transitions between those states, it has a mapping of transitions that go between those states. So, for example, for an idle, and the user makes a fetch request by pressing a button. Now we're in the pending states, and it also has a finite number of final states, which represent the app being done, there being no more left to do so. The beauty of these finite state machines is number one they could be visualized, such as this state diagram, and number two. They outline just all the possible transitions and what's not possible as well. So if we do a search action on the searching state, we realize that there is no transition outgoing from the searching state. So that search should have no effect. And this is exactly what we wants to occur, so that the user isn't keep making all of these different API requests.

So to do this in CSS, in HTML, and to do it in a scalable way. I employed the use of data attributes, so whereas we can't use ARIA attributes for everything, we could use data attributes to describe a more colorful and more varied way of describing all of the possible states of our application. Or of the individual components as well, the other attributes are something that are very underused, but this goes back to the beginning of HTML and I. Highly encourage you to use it because they end up being a lot simpler than class names to use. So, for example, we could have a data state equals loading, and we could target that using an attribute selector in CSS, data state equals loading, and so in JavaScript. It actually makes it really easy to change the data attribute of whatever you're working with. So we could have a data state. And we could say, you know, it's loading your success.

And then we could change that, or we could delete it completely, and then it's gone. And so changing it is just setting that attribute and deleting it is just deleting. So there's no weird API's like classless dot, add class list that has class lists that remove that you have to worry about, it's just JavaScript. So these data attributes represents each one of these finite states in your components or your application. For example, this button. We could have data state of idle, which means nothing happened yet, we could have loading, we could have a success and we could have a failure. So you could also imagine that you, you can name these states whatever you want. And that's the beauty of these data attributes, so how do we implement data attributes with these finite states?

Play from 8:00

13:00 You could say, if I'm in the idle state and these…

Traditionally, finite state machines, especially in other languages, can be done using switch case statements. Where you would first figure out what state you're in and you would switch on that. So, for example, for in the searching state, we go to the key statement for search and then we switch on what events occurred. So if you're used to Redux on them, getting into JavaScript a little bit here by Redux or NG RX, or any of those reducers. Then typically, you would do action first. But now we're doing it the other way around. We're looking at what state were on first, and then we're going to the event. I call it embeds instead of action, because that's a more accurate term. But we check what events just happened and what state we should go to next based on that event, such as success. And, of course, if that is not handled by the switch case statement, then you just default to the same state.

Now, personally, I find switch case statements a little bit. Verbose? So I like to do object mapping instead, and so this is just you. It looks like a big JSON object, and it's essentially the same idea where you have a state such as searching. You look at the on property to see which events just occurred. And if there's a mapping between the events and the next state, that should happen. And if there isn't, you just default to the current state. So I also wrote a library for this, called XE, which you do not have to use. It doesn't apply directly to what I'm talking about because you could do this today without any JavaScript library. But I just felt like mentioning it because it does have a lot of these things built in. To use X8, you would just have a machine function which takes in that object configuration. And it gives you a few nice things, such as automatic transitions, so you don't need to code that transition function yourself.

And you could say, if I'm in the idle state and these search events happens, then we should go to the searching state. That's actually wrong, I'll correct that, but yeah, so here's an example over here. Have you ever wondered how they implement sort of a drag-and-drop type of thing? I mean, it looks pretty simple, but then you get around to trying to implement it and seeing, Okay, which event listeners do I need to listen to? What do I have to do? When do I stop listening to these event listener? And it just gets really confusing. So you probably reach for a library. But it's actually a lot simpler than it seems if you think about drag-and-drop as two states, either idle or dragging. Anything. About four properties the X and Y, which represents the position of the object, and the DXdy, which represents how far you move the object. Then it becomes pretty easy to visualize this as two states idle. And then when you pick something up like this, that's like you're dragging state's own.

I'm changing the DxDy properties and then I would move it somewhere, and then once I stop, now I'm in the idle state again. And so we could visualize this here. Once I start dragging, you see those DXDy properties moving, and then I drop it and then now I change those. To X and Y, and then I could do it again. And so you can see, it's very easy to visualize these two states within the context of your application, in your animations. And this on this playing by the way, using those data attributes as well. So we could also, if you want to, do more advanced animations. Based on what state you're on and what state you came from. Because the different actions that could happen depend on which state that you may have come from. And so that's why using a data pre state attribute stands for previous state can be useful as well. Because, you know, we could have two different things.

For example, if we're coming to a loading state from an idle state, that might do one thing. But if we're coming from an error state, then maybe we want to show an error. Gone animation and then we're back to loading. And then so, in our same function where we apply those data attributes, we would just set the previous state to whatever the state was before. And we could target those right in our CSS just by combining those selectors like so and so, this, if it ever loads, is an example of that. So we have different behaviors based on whether going up, for example, and then we have a difference animation over here and then going down, it does something different. So we do want to keep track of which state that we came from, and so again, this is using data attributes to change which state were in. So, so, as I started playing around more with this idea of using finite state machines for animations.

I saw more and more complex animations on Tribble that I really want to recreate, such as this one by Cal Shear. Which lets you extend your weekend, which I really hope I was I want to do this weekend. So I broke this animation down and I start to see all of the different states of it. So at the beginning, when the user starts dragging their mouse, then we start searching. Or, sorry, we start selecting each of the days, and then once the user, let's go. Now we're in the selected state, so now a mouse movement is going to mean something different, because then we're in the dragging state. And we could move those states around and then we're in the disposed dates. So once we start grabbing. Now, the mouse movement means something different, it means we're extending a day, so the exact same events can mean something different, depending on what state you're on. And I think that's a really important point.

So just to show you over here, if I start dragging and selecting, remember, mouse movement means one thing. So I could start dragging over here. And if I don't go to the trash can, then it just goes back there and back to that state, and so I could drag it all the way here. Disposes. And then now I'm in the grabbing state, which means that if I move my mouse and I start dragging outside. It's not gonna have that initial behavior of showing the selection rectangle, because I'm not in that state where that behavior is defined. So that's an important point when dealing with application. State is not just mapping an event to an action or to side effects, but instead mapping it to what state you're in and what events. Just have mapping those two together. So using the state machine, which I created using X8. But of course, you don't need to.

You could also automatically generate visualizations such as this, which explained the whole process of the logic of. You know how the app is supposed to behave once you're dragging and dropping certain things. So does this scale. That's a big problem, which we have to talk about, because, of course, once you have so many different combinations, like Mike was talking about then. We could have an exponentially large number of states and we sort of need to manage that. Thankfully, a solution in 1987 by a computer scientist by the name of David Harel sort of solved that for us, it says. 1997 there, but it was actually a decade earlier, so these are really, really old ideas. Statecharts extend the idea of state machines by adding things such as actions, whether you're entering state, exiting a state, or performing a transition between states. It has guards, so you can make sure that transitions only occur when a condition based on the state is met.

There's also the idea of hierarchy, so you could have nested states you can think of. Those as sub routes, or if you're in one state and you could be in a sub state of that state. Also, orthogonality, which represents two different ideas of states, which don't really have too much to do to each with each other. But you want to represent them both equally. And the idea of history states as well, so hierarchical states can really help simplify how we combine states and make the logic easier. So we see in the success and failure state, the search events can occur from both of them. So instead, we could group those two together and make it so that whether we're in the success or failure state, that search events can take us back to the searching state. So in XState, that's done just by nesting those states within the search state over there. And then we could see that when we perform the search event on any of those states, then we go back to the searching state.

So it really simplifies the design of these of the logic now, to represent these nested states and hierarchical states in CSS. We have to get a little bit into the weeds of data attribute selectors. So, of course, we have the existence selector, which just checks if it exists, we have the data attribute selector of. If it's exactly equal to if it contains anywhere at all. If it starts with something such as idle, if it contains, or if it ends with something such as error. Or this is the one we want if our data state or our data attribute contains the string passed in, such as idle error. But that string is basically a word that's separated by whitespace, and this makes it acts just like a class. Now. This isn't the same as saying that we're in two states at the same time. We're really in this idle dot error state, but we want that idle, just to let CSS know that we are in that parent state.

And so we could use data attributes as like, even further, like data show and data hide, and use that to show which elements are active based on which state we're on. And so that's just with a little bit of JavaScript. You don't need to read this code right now, but just know that by just figuring out. What state were in? We could show elements based on what state we're on, and so you could play around with the idea. You don't have to use data show where Dala hide. You could just use whatever you want to use, but the idea is that you're not directly toggling. Classes are talking attributes on each of these states. So, for the final example, here's this password form. And you can see that there's this Idol and Idol dot normal sub state that we're in. Because if I enter a password incorrectly and it validates now, we go to isle dot error and eventually it goes back to idle dot normal.

Because we want those two states to be similar, but also we want them to be distinct enough so that we could group its similar characteristics and its unique characteristics. Such as showing the red outline when the password is incorrect, then we eventually have a success date as well. So the main idea that I want to bring home to you is that you should think in states and events, and not just events. And the reason for doing this is because, once you define your application in your components as a finite state machine or a statechart, and you model it in a very explicit way. You could do some really cool things, such as visualize these and so just to show you that animation I just showed you can be visualized right over here. So I copied and pasted of the machine used to define. And we could see that we're in the Isle states, but we're also in the normal state as well.

And so when we change now, we go to the validating state in.

Play from 13:00

25:00 Same ideas of finite state machines and statecharts

If it's an error, then it's invalid, and then we go back to the normal states. Just like that now. So there's a lot of powerful applications of this, such as analysis, automatic testing and other things that I talked about. But there's also other tools that could do this for you. Sketch systems is a very useful tool for prototyping your applications and your designs using these AI. Same ideas of finite state machines and statecharts. Because you could write it in this pseudo syntax and be able to play around with it in a little prototype editor at the bottom. So I highly. Encourage you to check that out. There's a lot of advantages to using statecharts. NASA use them that their selves with the Mars rover. Which, I mean, honestly, if you have one shot at going to production with a multi hundred billion dollar thing, you don't want to it up.

So, I mean, imagine doing this in JavaScript, it would, it would go nowhere, so there's a lot of advantages to this. They had visualized, modeling precise diagrams, they were able to use those statecharts to generate code automatically. And also to make sure that every single possible thing that could happen to this Mars rover was tested and was covered. And also, when requirements changed, they didn't have to go into code to figure out what needs to change. But instead, they could just go to the diagram and see. Okay, this is now this, so now we have to have an arrow here and this goes here. And so it's a lot easier to deal with these automatic late-breaking changes with visual diagrams instead of code. Now, of course, there are disadvantages, because, as you could tell, there is a huge learning curve, and modeling these day charts requires planning ahead. You can't just put your head down in code.

You need to actually stop and plan it and draw those boxes and draw those arrows and give it to your developers and designers. And say, is this a correct behavior in our application? And of course, you can't just use finite state machines for everything, but they could be a very good abstraction for the overall logic of your applications. There are also complexity trade-offs, where you might say, I don't need statecharts because my application is so simple, and that's when you might use finite state machines. Whereas normal code, which I call bottom-up code, because we're putting everything in the event, listeners. Instead of instead of structuring it in a nice modeled way, it increases linearly, but, as we saw, finite state machines can increase exponentially. Statecharts, however, might seem more complex at first, but they manage complexity at scale, and this is why I love using them. Because no trivial app ever stays trivial. Once more features come in, it becomes so much more complex, and you want a good way to manage that complexity.

There's a lot of. Resources online for learning about state machines and statecharts, which I encourage you to check out. There's a community on spectrum Piazza de Chartes, and there's the world of statecharts. Which just explains in the very easy to read way how to get started with stage arts and what kinds of problems they solve. So, in general, I want to make your code do more than just applying the application logic. I want you to be able to visualize, automatically, test, analyze, simulate your applications in your components, and be able to visualize all the possible states and transitions between your components and applications. Thank you very much CSS contest [Applause].

Play from 25:00

Related talks