If you haven't wrapped your head around it, you may be wondering why you're seeing the phrase "declarative programming" all over the place, and why there seems to be a unanimous opinion it's better than "imperative" code. Once you understand the difference, it's a natural and simple distinction — I want to help you see it that way. There's a lot of information on this topic, but much of it doesn't really seem to educate those who don't already understand. Take the following definition:

Declarative programming expresses logic without expressing an explicit control flow.

That summarizes the essence perfectly, but it's also pretty dense. How exactly do you achieve that in your code?

There are plenty of metaphors floating around on this topic. For example, take the act of telling a friend how to get to your house. An imperative approach would involve providing turn-by-turn directions. A declarative approach is to simply state your address and let their smartphone do the rest. It's easy to see that the second approach avoids expressing an explicit control flow. Still, such a metaphor can raise more questions. What does stating an address have to do with programming? We generally think of computer code as representing a series of instructions, and simply declaring your address is not really a series of instructions. And hey, didn't someone have to program the app that gives the directions?

Worse yet are the many contrived code examples that show functional methods like .map and .reduce, or a small React component as a way to explain this declarative concept. These are real world examples, but it's helpful to zoom out a bit and understand how we get there.

I've often seen this disconnect between the definition of declarative programming and how it applies to one's work. To help make it clearer, let's look at a real programming language you're already very familiar with that is 100% declarative: CSS. So what makes CSS declarative?

:hover Example

Take the following snippet:

.button:hover {
  background-color: blue;
}

Of course this code makes any element with class="button" turn blue when hovering over it. It's so basic you've likely written it countless times. Still, there are a few interesting attributes to note:

  • There is no concern for the X and Y coordinate of the user's mouse.
  • There is no concern for where the button is positioned on the user's screen.
  • It works the same whether there is one button on screen, or a hundred.
  • It will apply the hover effect to buttons that are added dynamically with JavaScript after the page has loaded.
  • It reverts the button to its previous state when the mouse is moved off it.

Looking at it this way, it's kind of amazing all those things "just work" with only three lines of CSS.

For comparison, here's an example of achieving a hover effect with JS:

const buttons = document.querySelectorAll('.button');
buttons.forEach(button => {
  button.addEventListener('mouseenter', () => {
    button.style.backgroundColor = 'blue';
  });
  button.addEventListener('mouseleave', () => {
    button.style.backgroundColor = 'red';
  });
});

This mostly achieves the same thing, but there are some drawbacks. If a new button is added dynamically after the above code executes, it won't have the hover effect. Event delegation solves this problem well, but adds more boilerplate code. Another nitpick about the JS version is in the mouseleave handler:

button.style.backgroundColor = 'red';

With the CSS version, we didn't need to know the style of the button in the non-hover state. Now we do, in order to properly reset it. This can also be avoided by caching the original values somewhere prior to changing them, but again that adds more boilerplate.

Describing a proper JS implementation for a task as simple as a hover effect starts to get complicated. Don't get me wrong, it's not crazy complicated — my point is that explaining a hover effect with CSS takes mere seconds and works really well, while explaining how to achieve an identical result with JS is much harder. It's certainly more than just a couple of steps. The CSS version is a distilled declaration of the intended result. There's no control flow to think about, no state to manage, and no cleanup code. It's bulletproof while being simple and easy to read. That is what makes CSS a declarative language, and that is what declarative programming should feel like.

Other Examples

CSS is full of great features that turn what could be complex implementations in your project into simple declarative code that is much easier to read and maintain. Here are some other examples to help drive the point home:

Selectors

section p:not(:first-child)

Selectors will always match the correct elements no matter how the DOM changes at runtime. You declare what you want and the CSS engine consistently provides it. Even though specificity can bite if it's allowed to get out of hand, you'll never have a bug occur because a CSS selector failed to match a corresponding element.

Media Queries

@media screen and (max-width: 40em)

Media Queries allow you to declare unique styles for various environmental states (notably different screen sizes) and the CSS engine applies the right styles when needed. You can declare several different media queries and no matter how they overlap or change, the correct styles will always be applied. With imperative code it's easy to try to think about every possible state transition which gets complicated. CSS doesn't require that.

Transitions

transition: transform ease-in 0.3s;

Transitions provide a method to animate state changes no matter when and how they occur. Without CSS transitions you would need to create an animation using a library like Velocity.js or GreenSock. That animation has to be started and stopped explicitly when certain events occur. That gets complicated fast, and it's very easy to create all sorts of weird animation bugs especially if you're just learning. To me, CSS transitions represent the best animation API that has ever existed for simple, common scenarios (Greensock is still amazing when you have to go beyond simple animations). Entire classes of animation bugs are eliminated and the resulting code you write couldn't be simpler. It's possible because complex animation behavior was distilled into a declarative API.

Conclusion

The hallmark of a great API is simplicity, reliability, and flexibility. A declarative approach facilitates all of this.

Hopefully it's easier to see why declarative APIs are preferable to imperative ones. However, it is important to realize that it's impossible for all code in existence to be declarative. At some level, there is always imperative code. Somebody had to build the CSS engine that allows these examples to work — that surely involves lots of imperative code. While you can type an address into your smartphone to get directions, someone had to develop that app and probably wrote plenty of imperative code doing so. Even the source code of React, a declarative UI library, is full of imperative code. Declarative code is always just an abstraction over imperative code. The goal is to keep the imperative pieces isolated, test them rigorously, and build on top of a declarative abstraction. You can work faster and eliminate entire classes of bugs!