Control UI state with pure CSS and HTML

Łukasz Pawłowski
Frontend Weekly
Published in
6 min readDec 9, 2019

These days interactions and animations are everywhere on the Internet. Static content becomes less attractive, and even little transitions can change how the page is received. Most visual effects can be done with pure CSS, but still, interactions are written in javascript. What if user action affects only visualization and does not change any data? Today I’ll show you a straightforward trick and a few examples of tracking UI state without javascript.

Base concept

We’ll discuss the general idea with a simple example. In our case, we have three buttons. Each button represents color. When a user clicks on a specific color, the box is displayed with the selected color. As we mentioned before, we want to use only CSS and HTML. Let’s look at our options.

First of all, CSS gives us few possibility to track user interactions:

  • :hover — pseudoclass which indicate if the cursor is over a specific element
  • :active — when element is activated by a user, for example at the moment when he clicks on the element
  • :focus — when user focus on specific input
  • :placeholder-shown — when text input or textarea display placeholder (for example when user did not put any data)
  • :valid/:invalid — when data in input passed or not passed validation
  • :checked — when checkbox, option in select, radio has state “on”
  • ::selection — pseudoelement which indicate selected text

Second, we need to keep the state of UI. We do not want to use javascript, so any variables and memory is unavailable for us. We also can’t add new DOM elements. But, HTML provides us with an old fashioned solution — forms. We can use inputs to keep information about the current state. Each input represent some data, which we can track:

  • checkbox — binary data, equivalent of boolean
  • select, radio — one value selected from a predefined set, equivalent of enum
  • text, text area — text value, equivalent of string
  • number — floating value, equivalent of float/decimal
  • date, datetime, datetime-local, time, etc. — date or time values. equivalents of Date
  • file — file input, equivalent of binary data
  • other text/numeric inputs with specif formats

For now, we can’t read the values of inputs in CSS, but with checked pseudoclass, we can track if a specific element was selected. This way, we are limited to boolean and enum kept in a checkbox, select, and radio inputs.

The last thing is to react to specific actions, which changes the state of UI. To do that, we need to build selectors in CSS, which contains a definition of state and element to change. CSS is using a cascade of elements — each part of the selector represents a definition of node lower in the DOM hierarchy. If we want to define the style of element A based on element B, we need to put A inside B or make them siblings. Inputs element do not contain other items. We need to put UI elements, which depend on states, as siblings for inputs or as a child of inputs siblings.

Options tags for select are always inside select tag. Currently, we are not able to select elements that contain other elements. So we are not able to use the state of option outside select element. For that, we need to wait a little bit longer until :has() will be implemented. But we can use radio input, which has a similar functionality.

Wow, that was a lot of theory. Let’s get back to our example. Here is code for it:

Let’s review the code in the context of functionality. First, we look at HTML.

We create a box, which changes based on user action. It is div with id = box.

Next, we need to create buttons. Usually, we would use buttons and javascript. CSS support button clicks only at the moment of click. There is no persistent state, which can be changed by the user and used in CSS. As we mentioned, we can track the state of inputs. We want to support multiple colors, so we need some equivalent of enum. For that, we use radio input. Each input has an id equal to color, which it represents. For example, input for red color has id = red.

We need to put inputs as siblings to our box, that we can build the correct CSS selector.

We can’t style radio to look like buttons. For that, we need labels. If we define that label is for specific checkbox or radio, click actions on that label will transfer to defined input (based on id attribute on input and for attribute on the label).

Now let’s check CSS. First, we style labels to look like buttons. In the end, we hide radio inputs. But that not fulfill functionality requirements.

Interaction is based on two elements: action and response. First, let’s look at the action. The user clicks on the label, and this way, he selects a specific radio. So if the user clicks red button #red:checked selector is active.

If we have the option to track action, we need to define system response. Our default behavior is to hide the black box. We use CSS selectors with higher specificity to overwrite default behavior. As mentioned before, we use custom property to set a specific color. We set default color on the global root element, and then with each radio :checked, we’ll overwrite its value. Thanks to CSS, we can find siblings of specific elements with ~ sign. This way, we create selectors like #red:checked ~ #box. It means — an element with id = box, which is a sibling of checked element with id = red.

Show/hide element

The example described above showed you how to use checked pseudoclass to track interactions. Let’s see how to use it in real projects.

How many times you had a simple toggle written in javascript? How many times did you need to display and hide part of UI when a user clicked something? Now you know that you do not need any javascript for that. One of the primary elements of UI is the menu. Let’s see how to hide/display the menu with pure CSS. Here is example code:

As you see, the trick is straightforward. We use the checkbox to track the state of the menu. We hide checkbox and use its label as facade. Label is our hamburger menu button, which also changed based on the checkbox state. When a user clicks the label, the checkbox is checked, and a specific style is applied on the menu.

Change elements values

Menu example showed you how to represent a single binary state. What do we do if we want to use described technique for something more complicated? As you saw in the first example, it is possible with radio input.

Let’s say we have a gallery of images. We display thumbnails on a page. When a user clicks on the specific thumbnail, we want to display modal with that image in a specific size. Here is an example of that gallery:

Each thumbnail is label for specific radio. When a radio is selected, the modal is displayed. Also, for each radio, we define what background image is used. The same trick as we did for colors in the first example. The problem is that we also want to close modal. Here we use the next radio, which defines a “closed” state for our modal. We use a higher specificity selector to override default modal behavior for radio:checked.

Think on a larger scale

For now, we presented examples for specific elements on the page. We can use the presented technique on a different level of abstraction. For example, if you check any ready to use themes, you’ll see multiple examples of different layout settings: colors, RTL, position for menu, etc. In most cases, each setting is managed by javascript or is presented on a different page. What if you want to make it interactive? You’ll still need javascript to remember the selected option for the user in his profile, but you do not need javascript for switching selected options on preview. Here is an example:

As you see, it is the same trick as before. The difference is that we have more CSS selectors. But if you use any preprocessor like SASS, it will not be a problem. You will be able to define multiple scopes and define selectors in them.

Summary

We do not have many options to implement interaction with CSS. We can use checkbox and radio inputs to keep information about the UI state. Based on that, we can create complex selectors to change the display of specific UI elements. We use checked pseudoclass to track what state is set. Sibling selector is required to find elements, which should be changed based on state set by inputs. We can use labels as a facade for checkbox and radio, which gives us the possibility to style our action elements however we want.

I hope this article give you some overview of tracking UI state with inputs and responds on UI state change with pure CSS.

--

--

Łukasz Pawłowski
Frontend Weekly

Web developer, tech advisor, manager, husband & father. Tech Manager at Boozt