Tip of the week #010: Embrace simplicity

Building for the web has never been easier, but it's also never been easier to over-engineer the simplest of things.

Embracing simplicity can mean many different things, but when I'm talking about it I'm usually saying "start simple — add complexity when necessary".

This goes for everything, either we're talking about CSS selectors in a stylesheet or installing external libraries to solve trivial problems.

Responsive styles

One example of this in CSS could be this:

Every website, before we add CSS, is responsive by default*. It's up to us how much we want to fuck with that starting point.

*Given that the HTML document have <meta name="viewport" content="width=device-width, initial-scale=1.0"> in its <head>. Also, tables and word-wrapping is not the best, but the point still stands.

This idea is what gave birth to the "mobile-first" approach in web development: that we first and foremost write our styles to apply for the smallest screens (or large screens with a high zoom setting), while tweaking the styles as the viewport gets larger, adding only what's necessary. Most of the time this could be some layout shifts, increasing font-size or spacing etc.

I'm not saying that it's easy to write responsive styles, but you should embrace the simplistic idea that all things are responsive from the beginning, and work your way from there.

Write as much "vanilla" HTML or CSS as possible

A browser can only read HTML, CSS and JavaScript (and SVG, XML, JSON...). That's it. However, when you create your fancy NextJS-applications, or whatnot, you rarely write any vanilla version of these languages. JavaScript is most likely written in Typescript, HTML in JSX/TSX and styles written using CSS-in-JS libraries like styled-components or Emotion (or by applying TailwindCSS classes).

These are all great tools, but we must still not forget the basics. Browsers consume our compiled code, so I prefer that the code I write is as close as possible to what the browser will read. Some may call me "nostalgic" or a "front-end purist" if I declare my disdain for CSS-in-JS and advocate for us to write pure HTML and CSS, but I'm not. I'm just a fan of using these tools if they actually enhance my developer experience and that the trade-off of having more dependencies is worth it. This can be hard to measure, but let me give you an example:

Let's say you're building a blog website, much like this one. Its content is created using Sanity. Sanity has a great relationship with NextJS, so it's easy to set up and use NextJS for the project. Since NextJS is React-based you write your components with JSX/TSX. That's great - and it's super easy to work with components and layouts fragmented. "Separations of concern" and all that stuff.

However! I want you to really think about if you need a CSS-in-JS library or not. The site you're building isn't really big, it scales fairly easy. So why the need for external tooling?

Let me guess, the reasons you'd opt for a CSS-in-JS library like styled-components could be because of these things:

  • Locally scoped CSS - naming is hard. Coming up with a good class name is hard. Scoping your CSS to only apply within the confides of a component is nice. The "C" in CSS, the cascade, is no longer a concern of yours
  • JavaScript-features like conditionals - you want to change the style of a component based on what props your component is given

These are both solvable by using normal CSS classes in a normal CSS stylesheet. Allow me to explain using this simple card component:

Language: tsx
import styled from 'styled-components';

const CardContainer = styled.div`
  width: 300px;
  padding: 20px;
  border-radius: 10px;
  box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
  background-color: #fff;
`;

const Title = styled.h2`
  font-size: 1.5rem;
  margin-bottom: 10px;
  color: #333;
`;

const Description = styled.p`
  font-size: 1rem;
  color: #666;
`;

interface CardProps {
  title: string;
  description: string;
}

const Card: React.FC<CardProps> = ({ title, description }) => {
  return (
    <CardContainer>
      <Title>{title}</Title>
      <Description>{description}</Description>
    </CardContainer>
  );
};

export default Card;

Here we use styled-components to add styling. It's works, but the only problem it's really solving is scoping.

Locally scoped CSS is not something unique to CSS-in-JS. CSS is a scoped language by default, it's what a selector is. We have classes. The styles you write in one class is only applied (scoped) to the elements that use that class. Coming up with good names for these is hard, but we can rely on naming conventions like BEM to help us.

There's no real reason for you to add a whole library just to accomplish what easily can be accomplished like this:

Language: css
.card {
  width: 300px;
  padding: 20px;
  border-radius: 10px;
  box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
  background-color: #fff;
}

.card__title {
  font-size: 1.5rem;
  margin-bottom: 10px;
  color: #333;
}

.card__description {
  font-size: 1rem;
  color: #666;
}
Language: tsx
interface CardProps {
  title: string;
  description: string;
}

const Card: React.FC<CardProps> = ({ title, description }) => {
  return (
    <div className="card">
      <h2 className="card__title">{title}</h2>
      <p className="card__description">{description}</p>
    </div>
  );
};

export default Card;

The are several reasons to why I like this approach better, and why it works just as well in small projects like these:

  • We don't manipulate the CSS in any way with JavaScript, so there's really no reason to write the CSS in JavaScript
  • The HTML will be the same as the one our browser will read. No need to create custom tags (like <CardContainer>) for things as simple as a div with a class attribute (className in this case, since we need to use React's syntax). It's WYSIWYG!
  • We separate styling from markup, while still maintaining a relationship between them due to the strict naming convention. In my opinion the markup looks cleaner, and it's easier to add the necessary JavaScript if we need to transform the data coming through the props

In short: classes solve the scoping problem.

Using JavaScript to render different styles based on different props is also solvable by classes.

Let's look at an example of this.

Language: tsx
import styled from 'styled-components';

interface CardProps {
  title: string;
  description: string;
  variant: 'primary' | 'secondary'
}

const CardContainer = styled.div<{ variant: CardProps['variant'] }>`
  width: 300px;
  padding: 20px;
  border-radius: 10px;
  box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
  background-color: #fff;

  ${(props) =>
    props.variant === 'primary' &&
    css`
      background-color: #000;
      color: #fff;
    `}

  ${(props) =>
    props.variant === 'secondary' &&
    css`
      background-color: #eee;
      color: #333;
    `}
`;

const Title = styled.h2`
  font-size: 1.5rem;
  margin-bottom: 10px;
  color: #333;
`;

const Description = styled.p`
  font-size: 1rem;
  color: #666;
`;

const Card: React.FC<CardProps> = ({ title, description, variant }) => {
  return (
    <CardContainer variant={variant}>
      <Title>{title}</Title>
      <Description>{description}</Description>
    </CardContainer>
  );
};

export default Card;

The code here adds a prop called "variant" that you can use if you want to present the card component in different colors. The prop is passed to the styled-component <CardContainer>, giving it a chance to render different values to the properties background-color and color based on the variant string.

Great, right?

Well, I believe this can be solved just as easily thinking in terms of native CSS, by adding/removing classes. It's inherently what CSS was made for and why we’re allowed to have more than one class on an element.

Here's how that would look:

Language: css
.card {
  width: 300px;
  padding: 20px;
  border-radius: 10px;
  box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
  background-color: #fff;
}

.card.primary {
  background-color: #000;
  color: #fff;
}

.card.secondary {
  background-color: #eee;
  color: #333;
}

.card__title {
  font-size: 1.5rem;
  margin-bottom: 10px;
  color: #333;
}

.card__description {
  font-size: 1rem;
  color: #666;
}
Language: tsx
interface CardProps {
  title: string;
  description: string;
  variant: 'primary' | 'secondary';
}

const Card: React.FC<CardProps> = ({ title, description, variant }) => {
  return (
    <div className={`card ${variant}`}>
      <h2 className="card__title">{title}</h2>
      <p className="card__description">{description}</p>
    </div>
  );
};

export default Card;

There are drawbacks to this of course. Here the typed variant prop is not used in the CSS, making it possible to create a mistake if you were to alter the allowed prop values (from "primary" to "main", for instance). However, the same could be said for how I solved it using styled-components: it still needs to check if the prop is "primary" or "secondary" and alter the styles based on that.

I prefer the vanilla CSS approach as it gives me a cleaner markup and, in my opinion, a more readable stylesheet. This will also be easy to debug in DevTools, as the class will be rendered in the DOM and the styles for that class visible.


In conclusion

This post began as a simple tip and ended in a rant about CSS-in-JS libraries, but that does not mean that I'm against them. I can summarize this post by saying "don't overcomplicate things", but what's complicated for some may not be complicated for others. I'm not very good at JavaScript, but I'm fluent in CSS — and I enjoy writing semantic HTML. My version of "simple" is "the less Javascript, the better", but that's not the case for everyone. However, I believe that we should have as few dependencies as possible (without compromising the quality of our code or the scalability of the project). I think trying to find a way to write as natively as possible before we add complexity is inherently a good thing.


PS! Here's some posts that inspired this tip: