I like to think of CSS as a conditional design language. Over the years, CSS was known as a way to style web pages. Now, however, CSS has evolved a lot to the point you can see conditional rules. The interesting bit is that those CSS rules aren’t direct (i.e: there is still no if/else in CSS), but the way features in CSS work is conditional.

Design tools like Figma, Sketch, and Adobe XD made a huge improvement for us designers, but they still lack a lot of the flexibility that CSS has.

In this article, I will go over a few CSS features that we use every day, and show you how conditional they are. In addition to that, I will compare a few examples where CSS is much more powerful than design tools.

What is conditional CSS?

In simple words, it’s about design that has certain conditions. When one or more conditions are met, the design is subject to change due to that.

For example, adding a new section to a design must push the other elements underneath it. In the following figure, we have a stack of items on the left. When adding a new one, the other items below it must move down.

Logically, that sounds expected and normal. In design tools, we got this a few years ago. In Figma, we have “Auto Layout” features that do the above. On the web, we have had that from day 1, even without CSS at all.

Conditional CSS

You might be thinking about what the heck conditional CSS is. Is that even a thing? No, there hasn’t been a direct “if” statement in CSS.

The main thing to distinguish is that some CSS properties work in specific conditions or scenarios. For example, when using the CSS :empty selector to check if an element is empty or not, it’s a conditional pseudo selector.

.alert p:empty {
  display: none;
}

If I want to explain the above to my 2 years old daughter, I will do it like this:

If there is nothing here, it will disappear.

Did you notice the if statement here? This is conditional design indirectly. In the following section, I’m going to explore a few CSS features which work similarly to an if/else statement.

The goal? To have a stronger idea and expectation about the CSS you wrote. I mean, you will be able to spot conditional CSS by just looking at the CSS for a component, a section, or a page.

CSS versus Figma

Why Figma? Well, I consider it as the standard for UX design these days, I thought it’s a good idea to do my comparison based on it. I want to share a simple example. There is list of tags that are displayed horizontally.

When you think deeply about it, you will spot some major differences. For example, the CSS version:

  • Can wrap into a new lines if there is no enough space.
  • Works with both LTR and RTL directions.
  • The gap will be used for rows when the items wrap.

Figma doesn’t have any of the above.

In CSS, there are three conditional rules happening:

  • If flex-wrap is set to wrap, then the items can wrap when there is no available space.
  • When the items wrap into a new line, the gap will work for the horizontal and vertical spaces.
  • If the page direction is RTL (right-to-left), the items will switch their order (e.g: design will be the first one from the right).

This is just one example, and I can write a book like that. Let’s explore a few cases where CSS can be conditional.

Conditional CSS examples

Media query

We can’t talk about conditional CSS without mentioning CSS media queries. The CSS spec is named CSS Conditional Rules Module. To be honest, this is the first time that I learn about that title.

When I did my research about who asks or mentions “Conditional CSS”, I found more than one time that media queries are the closest thing to an “if” statement in CSS.

.section {
  display: flex;
  flex-direction: column;
}

@media (min-width: 700px) {
  .section {
    flex-direction: row;
  }
}

If the viewport width is 700px or larger, change the flex-direction of .section to column. That’s explicit if statement, isn’t it?

The same thing can apply to media queries like @media (hover: hover). In the following CSS, the hover style will be applied only if the user is using a mouse or a trackpad.

@media (hover: hover) {
  .card:hover {
    /* Add hover styles.. */
  }
}

Size container query

With container queries, we can check if the parent of a component has a specific size and style the child component accordingly.

.card-wrapper {
  container-type: inline-size;
}

@container (min-width: 400px) {
  .card {
    display: flex;
    align-items: center;
  }
}

I have written about container queries multiple times, and have a place where I share demos about it.

Style container query

At the time of writing this article, this is behind a flag in Chrome Canary and is intended to ship in Chrome stable.

With a style query, we can check if a component is placed within a wrapper that has a specific CSS variable and if yes, we style it accordingly.

In the following figure, we have an article body that is coming from a CMS. We have a default style for the figure and another style that looks featured.

To implement that with style queries, we can style the default one, and then check if the figure has a special CSS variable to allow the custom styling.

figure {
  container-name: figure;
  --featured: true;
}

/* Featured figure style. */
@container figure style(--featured: true) {
  img {
    /* Custom styling */
  }

  figcaption {
    /* Custom styling */
  }
}

And if --featured: true isn’t there, we will default to the base figure design. We can use the not keyword to check when the figure doesn’t have that CSS variable.

/* Default figure style. */
@container figure not style(--featured: true) {
  figcaption {
    /* Custom styling */
  }
}

That’s an if statement, but it’s implicit.

Another example is having a component styled differently based on its parent. Consider the following figure:

The card style can switch to dark if it’s placed within a container that has the --theme: dark CSS variable.

.special-wrapper {
  --theme: dark;
  container-name: stats;
}

@container stats style(--theme: dark) {
  .stat {
    /* Add the dark styles. */
  }
}

If we read the above, it feels like:

If the container stats have the variable --theme: dark, add the following CSS.

CSS @supports

The @supports feature lets us test if a certain CSS feature is supported in a browser or not.

@supports (aspect-ratio: 1) {
  .card-thumb {
    aspect-ratio: 1;
  }
}

We can also test for the support of a selector, like :has.

@supports selector(:has(p)) {
  .card-thumb {
    aspect-ratio: 1;
  }
}

Flexbox wrapping

According to MDN:

The flex-wrap CSS property sets whether flex items are forced onto one line or can wrap onto multiple lines. If wrapping is allowed, it sets the direction in that lines are stacked.

The flex-wrap property allows flex items to wrap into a new line in case there is not enough space available.

Consider the following example. We have a card that contains a title and a link. When the space is small, each child item should wrap into a new line.

.card {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
}

.card__title {
  margin-right: 12px;
}

That sounds like a conditional thing to me. If no available space, wrap into a new line(s).

When each flex item wraps into a line, how do I manage the spacing between the flex items, you asked? Currently, there is a margin-right on the heading, and when they are stacked, that should be replaced by margin-bottom. The problem is we don’t know when the items will wrap because it depends on the content.

The good thing is that the spacing can be conditional with the gap property. When they are in the same line, the spacing is horizontal, and with multiple, the spacing is vertical.

.card {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 1rem;
}

This is one of my favorite flexbox features. Here is a visual of how gap switches the spacing.

By the way, I consider flex-wrap as defensive CSS. I almost add it to any flex container to avoid any unexpected issues.

The flex property

Even more, the flex property can work conditionally, too. Considering the following example. I added flex: 1 to the card title to make it fill the available space.

.card__title {
  flex-grow: 1;
}

That works fine, but when the width of the card is too small, the card title will wrap into a new line.

Nothing too bad, but can we do better? For example, I want to tell the title: “Hey, if your width is less than X, then wrap into a new line”. We can do that by setting the flex-basis property.

In the following CSS, I set the maximum width of the title to 190px. If it’s less than that, it will wrap into a new line.

.card__title {
  flex-grow: 1;
  flex-basis: 190px;
}

To learn more about the flex property in CSS, I wrote a detailed article on that.

Take things further, and explain about adding flex-grow, string.. etc along the way.

The :has selector

For me, this is the closest thing to an “if” statement in CSS right now. It works in a way that mimics an if/else statement.

Changing a card style

In this example, we need to have two different styles, depending on if the card has an image or not.

If the card has an image:

.card:has(.card__image) {
  display: flex;
  align-items: center;
}

And if it doesn’t have an image:

.card:not(:has(.card__image)) {
  border-top: 3px solid #7c93e9;
}

That’s an if statement, and I strongly think so. Sorry, I got too excited.

Hiding or showing form items conditionally

In forms, it’s common to have an input field or a group of inputs hidden by default, and it will be shown once the user activates an option from a