Writing Logic in CSS
Daniel Schulz

Daniel Schulz @iamschulz

About: Hi! I'm a frontend guy.

Location:
Germany
Joined:
Sep 17, 2018

Writing Logic in CSS

Publish Date: Feb 21 '22
506 44

CSS is a highly specialized programming language focusing on style systems. Because of this unique use case and its declarative nature, it's sometimes hard to understand. Some folks even deny it's a programming language altogether. Let's prove them wrong by programming a smart, flexible style system.

Control Structures

More traditional, general-purpose languages (like JavaScript) give us tools like Conditions (if/then), Loops (for, while), Logical Gates (===, &&, etc.) and Variables. Those structures are named differently in CSS, their syntax is wildly different to better accommodate the specific use case of styling a document, and some of those simply weren't available in CSS up until a few years ago.

Variables

Variables are the most straightforward ones. They're called Custom Properties in CSS (although everyone calls them variables anyway, even their own syntax).

:root {
    --color: red;
}
span {
    color: var(--color, blue);
}
Enter fullscreen mode Exit fullscreen mode

The double-dash declares a variable and assigns a value. This has to happen in a scope because doing so outside of a selector would break the CSS syntax. Notice the :root selector, which works as a global scope.

Conditions

Conditions can be written in a number of ways, depending on where you want to use them. Selectors are scoped to their elements, media queries are scoped globally, and need their own selectors.

Attribute Selectors:

[data-attr='true'] {
    /* if */
}
[data-attr='false'] {
    /* elseif */
}
:not([data-attr]) {
    /* else */
}
Enter fullscreen mode Exit fullscreen mode

Pseudo Classes:

:checked {
    /* if */
}
:not(:checked) {
    /* else */
}
Enter fullscreen mode Exit fullscreen mode

Media Queries:

:root {
    color: red; /* else */
}
@media (min-width > 600px) {
    :root {
        color: blue; /* if */
    }
}
Enter fullscreen mode Exit fullscreen mode

Loops

Counters are both the most straightforward form of loops in CSS, but also the one with the narrowest use case. You can only use counters in the content property, displaying it as text. You can tweak its increment, its starting point, and its value at any given point, but the output is always limited to text.

main {
    counter-reset: section;
}

section {
    counter-increment: section;
    counter-reset: section;
}

section > h2::before {
    content: 'Headline ' counter(section) ': ';
}
Enter fullscreen mode Exit fullscreen mode

But what if you wanted to use a loop to define a recurring layout pattern? This kind of a loop is a bit more obscure: It's the Grid's auto-fill property.

.grid {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
}
Enter fullscreen mode Exit fullscreen mode

This fills the grid with as many elements as it can fit, while scaling them to fill the available space, but breaking them into multiple rows when it needs. It repeats for as long as it finds items and caps them to a minimum width of 300px and a maximum width of one fraction of its own size. It's probably easier to see than to explain:

And finally, there are looped selectors. They take an argument, which can be a formula to select very precisely.

section:nth-child(2n) {
    /* selects every even element */
}

section:nth-child(4n + 2) {
    /* selects every fourth, starting from the 2nd */
}
Enter fullscreen mode Exit fullscreen mode

For really special edge cases, you can combine :nth-child() with :not(), like:

section:nth-child(3n):not(:nth-child(6)) {
    /* selects every 3rd element, but not the 6th */
}
Enter fullscreen mode Exit fullscreen mode

You can replace :nth-child() with :nth-of-type() and :nth-last-of-type() to change the scope of those last few examples.

Logic Gates

Ana Tudor wrote an article on CSS Logic Gates. Those work on the idea of combining variables with calc. She then goes on 3D modeling and animating objects with that. It kinda reads like arcane magic, gets way more insane as the article goes on, and is generally one of the best explanations why CSS in fact is a programming language.

Techniques

The Owl Selector

* + * {
    margin-top: 1rem;
}
Enter fullscreen mode Exit fullscreen mode

The Owl Selector selects every item that follows an item. Applying a margin-top to that effectively adds a gap between the items, like grid-gap does, but without the grid system. That also means it's more customizable. You can overwrite your margin-top and adapt for any kind of content. Want to have 1rem of space between each item, but 3rem before a headline? That's easier to do with an owl selector than in a grid.

Kevin Pennekamp has an in-depth article on it that even explains its algorithm in pseudo code.

Conditional Styling

We can create toggles in our css code that switch certain rules on and off with variables and calc. This gives us very versatile conditions.

.box {
    padding: 1rem 1rem 1rem calc(1rem + var(--s) * 4rem);
    color: hsl(0, calc(var(--s, 0) * 100%), 80%);
    background-color: hsl(0, calc(var(--s, 0) * 100%), 15%);
    border: calc(var(--s, 0) * 1px) solid hsl(0, calc(var(--s, 0) * 100%), 80%);
}

.icon {
    opacity: calc(var(--s) * 100%);
    transform: scale(calc(var(--s) * 100%));
}
Enter fullscreen mode Exit fullscreen mode

Depending on the value of --s, .box it will either enable or disable its alert styles.

Automatic contrast colors

Let's take the same logic one step further and create a color variable that's dependent on its contrast to the background color:

:root {
    --theme-hue: 210deg;
    --theme-sat: 30%;
    --theme-lit: 20%;
    --theme-font-threshold: 51%;

    --background-color: hsl(var(--theme-hue), var(--theme-sat), var(--theme-lit));

    --font-color: hsl(
        var(--theme-hue),
        var(--theme-sat),
        clamp(10%, calc(100% - (var(--theme-lit) - var(theme-font-threshold)) * 1000), 95%)
    );
}
Enter fullscreen mode Exit fullscreen mode

This snippet calculates a background color from HSL values and a black or white font color, by inverting the background's lightness value. This alone could result in low color contrast (a 40% grey font on a 60% grey background is pretty much illegible), so I'll subtract a threshold value (the point where the color switches from white to black), multiply it by an insanely high value like 1000 and clamp it between 10% and 95%, to get a valid lightness percantage in the end. It's all controllable by editing the four variables at the beginning of the snippet.

This method can also be used to write intricate color logic and automatic themes, based on HSL values alone.

Cleaning up the Stylesheet

Let's combine what we have so far to clean up the stylesheet. Sorting everything by viewports seems a bit spaghetti-like, but sorting it by component doesnt feel any better. With variables we can have the best of both worlds:

/* define variales */
:root {
    --paragraph-width: 90ch;
    --sidebar-width: 30ch;
    --layout-s: "header header" "sidebar sidebar" "main main" "footer footer";
    --layout-l: "header header" "main sidebar" "footer footer";
    --template-s: auto auto minmax(100%, 1fr) auto /
        minmax(70%, var(--paragraph-width)) minmax(30%, var(--sidebar-width));
    --template-l: auto minmax(100%, 1fr) auto /
        minmax(70%, var(--paragraph-width)) minmax(30%, var(--sidebar-width));
    --layout: var(--layout-s);
    --template: var(--template-s);
    --gap-width: 1rem;
}

/* manipulate variables by viewport */
@media (min-width: 48rem) {
    :root {
        --layout: var(--layout-l);
        --template: var(--template-l);
    }
}

/* bind to DOM */
body {
    display: grid;
    grid-template: var(--template);
    grid-template-areas: var(--layout);
    grid-gap: var(--gap-width);
    justify-content: center;
    min-height: 100vh;
    max-width: calc(
        var(--paragraph-width) + var(--sidebar-width) + var(--gap-width)
    );
    padding: 0 var(--gap-width);
}
Enter fullscreen mode Exit fullscreen mode

All the global variables are defined at the very top and sorted by viewport. That section effectively becomes the Definition of Behavior, clearing questions like:

  • Which global aspects of the stylesheet do we have? I'm thinking of things like font-size, colors, repeating measures, etc.
  • Which frequently changing aspects do we have? Container widths, Grid layouts and the like come to mind.
  • How should values change between viewports? Which global styles do apply to which viewport?

Below are the rule definitions, sorted by component. Media Queries aren't needed here anymore, because those are already defined at the top and put into variables. We can just code along in out stylesheets uninterrupted at this point.

Reading the hash parameter

A special case of pseudo classes is the :target selector, which can read the hash fragment of the URL. Here's a demo that uses this mechanic to simulate an SPA-like experience:

I've written a post on that. Just be aware that this has some serious accessibility implications and needs some JavaScript mechanics to actually be barrier free. Don't do this in a live environment.

Setting Variables in JavaScript

Manipulating CSS Variables has become a very powerful tool by now. We can also leverage that in JavaScript:

    // set --s on :root
    document.documentElement.style.setProperty('--s', e.target.value);

    // set --s scoped to #myID
    const el = document.querySelector('#myID');
    el.style.setProperty('--s', e.target.value);

    // read variables from an alement
    const switch = getComputedStyle(el).getPropertyValue('--s');
Enter fullscreen mode Exit fullscreen mode

The codepen examples above work just like that.

Wrapping up

CSS is very much capable of difining smart and reactive layout systems. It's control structures and algorithms may be a bit weird compared to other languages, but they're there and they're up to the task. Let's stop just describing some styles and start making them work.

Comments 44 total

  • Paul C. Ishaili
    Paul C. Ishaili Feb 21, 2022

    Real Brilliant!

  • yw662
    yw662Feb 21, 2022

    I wonder...would these styles perform better than plain javascript ?

    • Daniel Schulz
      Daniel SchulzFeb 21, 2022

      No, you'd only add another step to the render pipeline. No Javascript is always faster than Javascript.

  • Nino Filiu
    Nino FiliuFeb 22, 2022

    I knew about the awesome CSS-only SPA-like navs thanks to :target, but I didn't know about CSS counters, thanks for sharing!

  • ΛGΣΣK
    ΛGΣΣKFeb 22, 2022

    Even if it's possible, just dont use it!

    • Daniel Schulz
      Daniel SchulzFeb 22, 2022

      Don't use CSS? Why wouldn't you?

    • lucas-ayabe
      lucas-ayabeMay 8, 2024

      its not like you'll be using this to do a game with limited possibilities, or some weird workaround, is just the most eficient and elegant solution for common styling problems, it is much better use CSS to solve these problems than JS for example

  • Cauê Oliveira
    Cauê OliveiraFeb 23, 2022

    Great content and totally useful. Thanks for sharing.

  • Sean
    SeanFeb 23, 2022

    Negl ten minutes ago I definitely would deny that CSS was a language. It's always had a lesser place in my mind considering I could just use javascript but I think this has changed my mind. I'm going to try to deepen my knowledge of css thanks to this post, I think there is a lot to be gained from enlarging what I know. I know damn well that it'll probably save me many hours of scrolling google. Great post!!🔥🔥👌

    • Christopher Peacock
      Christopher PeacockMay 9, 2024

      I was thinking just the same thing. Worrying part i reckon this could be harder than Js 👀😆

  • Lara Schenck
    Lara SchenckFeb 24, 2022

    Folks here might like my talk about algorithms in CSS!
    notlaura.com/algorithms-of-css-sou...

    • Daniel Schulz
      Daniel SchulzFeb 24, 2022

      Yes! Your talk has been a huge inspiration. Can't recommend it enough

    • Well
      WellFeb 28, 2022

      A lot of thanks for sharing!

  • Rob OLeary
    Rob OLearyFeb 25, 2022

    Thanks for putting this together. I have wanted to look further into this. Do you have an opinion on when you should use JS instead of these techniques?

    • Daniel Schulz
      Daniel SchulzFeb 25, 2022

      When you absolutely hit the limits of CSS. Container queries would be an example, until its native CSS counterpart is widely adopted in browsers.

      I also think you should use JS alongside CSS to help accessibilty.

      • АнонимMar 7, 2022

        [hidden by post author]

        • Daniel Schulz
          Daniel SchulzMar 7, 2022

          For example setting showing/hiding elements for screenreaders. Dis/enabling inputs. Switching out aria attributes. Stuff like that. CSS isn't the right tool for that.

          • АнонимMar 7, 2022

            [hidden by post author]

            • Daniel Schulz
              Daniel SchulzMar 7, 2022

              That works on user input, but how would you hide an element on a responsive layout change? How would you set aria-expanded on a drawer? How would you try and build a mega menu in CSS only while keeping it accessible to SRs? How would you tab-lock a modal in CSS? Just because you didn't come across a use case yet doesn't mean they aren't plentiful.

              • RRKallan
                RRKallanMar 8, 2022

                Responsive layout changes when browser size changed. I will use media queries.

                No need for aria-expanded with HTML5 elements details & summary
                Also aria-live

                <details>
                    <summary>Title</summary>
                    <div>
                        Place your content which will show / hide on press 
                    </div>
                </details>
                
                Enter fullscreen mode Exit fullscreen mode

                To prevent the ability to tab / focus outside your element you can use

                element:focus-within {}
                
                Enter fullscreen mode Exit fullscreen mode

                A massive menu. Assumption more then multiple levels. Here I would go to talk first with business. Why so many. Levels. Looking for better better cleaner structure.
                Also for menu there is no need to use aria-expanded. Show hide text based on checked
                And yes for multi level menu and keyboard support there will be need JS for esc, arrows pageup and pagedown.

                • Daniel Schulz
                  Daniel SchulzMar 8, 2022

                  Aria-live and focus-within don't work like you describe. Please read up on those topics. And please manually test in screen readers, keyboard only navigation and/or other assistive tech.

                  • RRKallan
                    RRKallanMar 9, 2022

                    It's magic. your response said enough. Yes the focus-within is correct not totally described. It's right when you can speek. ?? i wooul=ld me more and moer lrss

    • RRKallan
      RRKallanMar 7, 2022

      My opinion when not to use JS, if it can be done without JS

  • Ronald Rowe
    Ronald RoweFeb 25, 2022

    A great article, keep up the good work! 💯

  • Zachary Price
    Zachary PriceFeb 25, 2022

    This is probably the best explanation on this topic that I persoannly have ever read. There's so much to unpack here and I am thankful that you presented this.

  • Stylestheandriod
    Stylestheandriod Feb 26, 2022

    Ohh wow I love this

  • Subhan Ali
    Subhan AliFeb 27, 2022

    this post changes my mind completely about css . i love your explination.

  • Rob Levin
    Rob LevinFeb 27, 2022

    I thought of [aria-hidden] { ... } when I saw your [data-attr='true'] based logic examples. Suppose it's an option for any attribute really. Only issue with this is its very generalized on its own.

    • Daniel Schulz
      Daniel SchulzFeb 27, 2022

      How's providing generalized examples a drawback?

      • Rob Levin
        Rob LevinMar 1, 2022

        I meant that using [aria-hidden] alone or any data attribute is overly generalized. I was not intending to communicate that you providing generalized examples is a drawback; just that usage of it alone is generalized. Hope that clarifies misunderstanding. Thanks for the article.

  • cycool29
    cycool29Feb 28, 2022

    Thanks for sharing. Really useful post.

  • Asifur Rahaman
    Asifur RahamanFeb 28, 2022

    GOAT

  • Kevin Pennekamp
    Kevin PennekampMar 1, 2022

    @iamschulz Thanks for the mention!

  • Jaycelin
    JaycelinMar 6, 2022

    Thanks for such a post. CSS is a declarative programming language, rather than an imperative language.

  • Bharat Saraswat
    Bharat SaraswatMar 9, 2022

    crazy, crazy...

  • The Volks NFT
    The Volks NFTMar 15, 2022

    Nice one!

  • Greg
    GregMar 28, 2022

    Great Article! Thank you!
    I love CSS 💪🏼

  • Christopher Peacock
    Christopher PeacockMay 9, 2024

    THIS! has blown my mind and sparked so much creativity in my head! Im defo going to have fun with this when i get home.

  • doug-source
    doug-sourceAug 5, 2024

    I translated this article here to Brazilian Portuguese.

    Thanks, 😉

    • Daniel Schulz
      Daniel SchulzAug 5, 2024

      Please ask in advance before you republish other people's content.

      • doug-source
        doug-sourceAug 5, 2024

        Ok. Thanks for your response.
        From now, I go to follow your advice 😉, and if you want, I go to remove the translation from your post. Do you want?

Add comment