In the time we live in js web ecosystem is huge. Bigger than it has ever been.
React applications, static websites, and dynamic front-end applications. There are a lot of approaches out there.
But one thing remains the same - CSS. CSS is an essential part of every application.
CSS
When it comes to CSS you want to solve multiple problems:
- Crafting CSS. Maintain simplicity, clarity, and manageability.
- Code organization. Maintain order and coherence in the styling.
- Design system and components styling. Ensuring a cohesive and efficient approach to styling becomes a big concern for all sizes of teams.
- HTML integration. How to embed CSS to the page.
- Performance optimization. It is a must if you want to reduce loading times and enhance user experience.
New CSS frameworks appear as answers to those questions.
Two of these modern frameworks are Tailwind and Linaria.
Let's talk about an example of a Next.js app, see how they work, and compare them by the following categories:
- Installation and adoption
- How to solve real problems
- Ecosystems
- Organization and structure of components
- Build process and optimization
Brief about both
Tailwind is a functional CSS framework, that follows the utility-first approach.
Linaria is a zero runtime CSS-in-JS library with a "Styled Components" syntax.
Installation and adoption
Tailwind
Now let’s discuss how to add a Tailwind to a brand new latest Next.js application. In FR we use Next.js because it’s the easiest way to get full control over rendering strategies and maximum performance out of the box.
The easiest way to set up a new project with Tailwind is to run the command and go with the suggested CSS option, Tailwind:
npx create-next-app@latest
If that approach doesn't suit you can follow a simple 6-step instruction from the official documentation, that takes less than a minute.
It could be added to brand new projects, as well as be incrementally adopted with the same ease. This means that you can install Tailwind and start using it in some components while using your old approach to style all the other components.
The simplicity of installation is one of the reasons why it’s so widely adopted and gaining so much traction.
Linaria
Now let’s take a look at how easy to install Linaria to a fresh Next.js project that we have.
npm install @linaria/core @linaria/react @linaria/babel-preset
And that’s it. After that, you can go ahead and start using Linaria in your project. But it's too early to relax because, after that, we move to the 'Setup' stage.
At the setup stage, you choose which bundler suits you and follow the instructions in the documentation. For example, here's a link to the webpack + Linaria documentation.
But in the current article, we are more interested in Linaria + Next.js, so let’s see what else we should do.
It's not that many steps. First of all, add a library called next-linaria with Linaria to your Next.js project.
npm install next-linaria linaria
After that, wrap your Next.js config with the withLinaria function, and that’s it."
// next.config.js
const withLinaria = require('next-linaria');
module.exports = withLinaria({
linaria: {
/* linaria options here */
},
/* next.js config here */
});
How to solve a real problem
Before we go further let’s solve a simple task: "Add a card with a title, description, and a button. The card should have a shadow and have rounded edges".
Tailwind
When we are using Tailwind, the first thing we should think about to create new elements is HTML structure. Let’s write an HTML structure for the card.
// index.js
function CardTailwind() {
return (
<div>
<h2>Card Tailwind</h2>
<p>
Lorem ipsum is placeholder text commonly used in the graphic.
</p>
<button>
CTA button
</button>
</div>
)
}
When the structure is ready we can add actual styles.
Tailwind classes have different names. For example, few of names:: w-full(width: 100%), m-12(margin: 12rem), and pr-6(padding-right: 6rem). At first, it might be frustrating to look up every class, but you will get used to it and understand the pattern really soon after start using it frequently.
Let’s attach the styles.
// index.js
function CardTailwind() {
return (
<div className="w-96 gap-4 rounded p-5 shadow-xl">
<h2 className="mb-4 text-2xl font-bold">Card Tailwind</h2>
<p className="mb-4">
Lorem ipsum is placeholder text commonly used in the graphic, print, and publishing industries for previewing layouts and visual mockups
</p>
<button className="rounded bg-black p-4 text-white">
CTA button
</button>
</div>
)
}
That’s it. We have a beautiful card.
Linaria
Now let’s do the same using Linaria.
For the reimplementation of the component above using Linaria, you need to write "a little more" code. Let's take a look.
Create CSS components and implement an HTML-like structure.
// index.js
import React from "react";
import { render } from "react-dom";
import { styled } from '@linaria/react';
const StyledWrapper = styled.div`
width: 380px;
padding: 20px;
gap: 16px;
border-radius: 4px;
box-shadow: 0 20px 25px -5px rgb(0 0 0 / 0.1),
0 8px 10px -6px rgb(0 0 0 / 0.1);
`;
const StyledTitle = styled.h2`
margin-bottom: 16px;
font-size: 24px;
font-weight: 700;
line-height: 32px;
`;
const StyledText = styled.p`
margin-bottom: 16px;
`;
const StyledButton = styled.button`
padding: 16px;
border-radius: 4px;
background-color: rgb(0 0 0);
color: rgb(255 255 255);
`;
const CardLinaria = () => (
<StyledWrapper>
<StyledTitle> Lorem ipsum </StyledTitle>
<StyledText>
Lorem ipsum is placeholder text commonly used in the graphic, print, and
publishing industries for previewing layouts and visual mockups
</StyledText>
<StyledButton> CTA button </StyledButton>
</StyledWrapper>
);
It's a good practice to separate "styled" elements from the JSX file for easier readability and to reduce the component's code. However, for a closer visual similarity between the two card structures, everything is included in one file.
Although the steps and results are similar, the code looks different.
The one written on Linaria is more intuitive because it utilizes regular CSS. At the same time amount of code is less if you use Tailwind.
To understand Tailwind, you have to go through some learning first. Usually, it takes no less than a week to get used to a new syntax. In some cases that might be critical, for example, if you have a team of 100 engineers and each of them has to spend this week. Business might not want to go forward with this decision
Ecosystem
Tailwind
According to the "State of CSS 2023" survey, Tailwind CSS stands apart as the one major UI framework that developers are happy to keep using.
More than 50% of developers are using Tailwind CSS over other CSS frameworks (CSS-in-JS has a separate category).
Why is that, why do developers choose Tailwind over and over again?
The Tailwind ecosystem is in a mature state right now, installable on every major web framework, and has a wide range of tools around it.
Tailwind even hosted the conference earlier this year.
Formatters and linters integrations to maintain the quality of your core. Great docs. Bunch of alternative educational materials, videos, and code examples.
But there are more.
Tailwind is not just a utility class framework. You can think about Tailwind like it is a design system. The system, that gives your UI "consistency" and offers essential defaults such as colors, typography, spacing, and breakpoints. It pushes for adopting best practices, such as using rem rather than px, as well as ensuring consistency in size, color palette, etc.
But you still can customize values, used by one or another class, by overriding tailwind.config.js file.
One more great thing about Tailwind, it allows you to copy-and-paste code snippet of any difficulty. You could copy button implementation to your project as well as copy the whole landing page.
A big part of Tailwind’s ecosystem is UI components libs. But libraries could be different:
- Styled low-level components, such as buttons, inputs, datepickers etc. You can use these components either as the base layer of your design system or use default styles right away.
- Bigger components, combining multiple lower-level components. This could be long forms, tables, carousels, collections of cards, and so on. You have all blocks implemented and you just need to use them in the right order.
All UI components shipped with responsiveness. This feature alone significantly speeds up the development process. Also, a common big feature is theming. Having multiple themes for the component out of the box is a huge advantage and time saver
Examples: radix-ui, chakra-ui, shadcn/ui, tailwind-ui.
Tailwind is unopinionated about how you should structure your styles and components. You can use existing Tailwind classes such as mx-2, shadow, border-2 as well as use common CSS syntax using special [font-size:18px] square brackets syntax. Also, you can put long classnames in its variable and UI components into its file.
Let’s wrap it up:
- Tailwind helps you write CSS, but easier by providing DX and great defaults.
- Tailwind is a design system by itself, but if you want to implement your own design system - you can do it on top of it.
- You can copy, paste, and use a piece of ready code.
- Tailwind gives you the ability to choose what architectural approach to pursue.
Linaria
Linaria is a zero-runtime CSS in JS library. Some of the problems that Linaria solves are:
- Performance: Linaria does not have any runtime cost, which means no extra JavaScript code to parse and execute, no style injection at runtime, and no dependency on any framework. This results in faster page loading and rendering, and lower memory usage.
- Compatibility: Linaria works with any JavaScript framework or library, such as React, Preact, and Svelte. It also supports server-side rendering (SSR) and code splitting out of the box. You can use Linaria with any bundler or toolchain, such as webpack, rollup, parcel, etc.
- DX: Linaria lets you use familiar CSS syntax with CSS native nesting and custom properties. You can also use JavaScript for logic and variables, and lint your CSS in JS with stylelint. Linaria provides source maps for easier debugging and development.
When it comes to the difference between Tailwind and Linaria, there are also a few differences:
- Linaria doesn’t make you dependent on a specific design – you could develop any design you want.
- Linaria allows you to style components in separate files, making your code more readable. And you can always use your props for the styled-components to create a function that will provide you with different styles inside of the component.
- You can easily style child components in different ways: by wrapping them or using an SCSS-like style.
Organization and structure of components
Tailwind
When you developing a project, you probably have several reusable components that you use throughout the project. For example, It could be simple buttons and inputs, but it also could be more complex components such as double date pickers.
When you create a UI kit with Tailwind you can use the basic approach of putting components in the special folder, let’s say ui/components. And this folder will include all of your components: ui/components/button.tsx, ui/components/input.tsx, ui/components/select.tsx and etc.
Let’s take a look at a button component and see how we can implement custom styles as you would do in the project.
Initially, the button will look like this:
// ui/components/button.tsx
export type ButtonProps = {
children: React.ReactNode
}
export function Button(props: ButtonProps) {
const { children } = props
return <button>{children}</button>
}
The next step of course is to add some styles to it.
// ui/components/button.tsx
export type ButtonProps = {
children: React.ReactNode
variant?: 'primary' | 'secondary'
}
export function Button(props: ButtonProps) {
const { children, variant } = props
return (
<button className="px-4 py-6 rounded text-white bg-black">
{children}
</button>
)
}
But of course, it is a button, so it could have multiple variants: primary and secondary(you can increase the number of customizable params, but we will limit it to 1, variant). To implement this you can use any library for combining classnames, for example, classnames, clsx. Let’s use the classic one, "classnames".
Your code ends up being look like this:
// ui/components/button.tsx
import cn from 'classnames'
export type ButtonProps = {
children: React.ReactNode
variant?: 'primary' | 'secondary'
}
export function Button(props: ButtonProps) {
const { children, variant } = props
return (
<button
className={cn('px-4 py-6 rounded ', {
'text-white bg-black': variant === 'primary',
'text-black bg-white border border-black': variant === 'secondary',
})}
>
{children}
</button>
)
}
If you want to allow additional customization, you can add classname prop to your button and pass it to button.className.
// ui/components/button.tsx
import cl from 'classnames'
export type ButtonProps = {
children: React.ReactNode
variant?: 'primary' | 'secondary'
className?: string
}
export function Button(props: ButtonProps) {
const { children, variant, className } = props
return (
<button
className={cl('px-4 py-6 rounded ', {
'text-white bg-black': variant === 'primary',
'text-black bg-white border border-black': variant === 'secondary',
}, className)}
>
{children}
</button>
)
}
But this approach leads to certain problems. Classnames don't filter Tailwind classes and passed className might have conflicts with existing within the button classes.
For example, if you want to use a button, and pass a custom padding to it, you probably will do this:
<Button variant="secondary" className="px-2 py-3">
clear
</Button>
But in this case, you won't achieve the desired result. "px-2 py-3" conflicts with the default "px-4 py-6".
In this case, both vertical and horizontal paddings will be applied. The bigger value wins in this case and padding selector weight should be increased using important, which is awful.
How to avoid this problem?
You can use tailwind-merge or a similar package for that. The idea behind this is you wrap your classes with a special util function, in this case, twMerge. It will automatically remove classname duplicates and will leave the one that is defined last.
So you can have your own cn utility function, that will include twMerge inside.
// ui/utils.ts
import cn from "classnames";
import { twMerge } from "tailwind-merge";
export function cn(...classNames) {
return twMerge(cn(classNames));
}
Basically, this cn util is a nice and clean way to encapsulate twMerge logic. The good thing about this approach is - to implement it, all you should do is add new util and change imports.
This is the most common thing devs forget when starting with Tailwind.
Also, you could follow another approach, and put every property you want to customize in a named variable. For example, paddings could be "small" and "big” and they are controlled using a special "size" property, the same as a variant.
Tailwind gives you a lot of freedom, but you should use this freedom wisely.
Linaria
And now let's see how you could implement the same universal Button using Linaria.
// components/Button/styled.js
import React from 'react';
import { styled } from '@linaria/react';
export const StyledButton = styled.button`
padding: 16px;
border-radius: 4px;
background-color: black;
color: white;
`
export const Button = ({children}) => {
return (
<StyledButton>
{children}
</StyledButton>
)
}
As you can see in the exact component, there's less code, and there's no need to use the className for styling. All CSS goes into a separate file, or at least into a separate variable, which makes code reading much easier.
Now, let's take a look at how we can create different variants of this button.
// components/Button/styled.js
import React from 'react';
import { styled } from '@linaria/react';
const StyledButton = styled.button`
padding: 16px;
border-radius: 4px;
background-color: ${({variant}) => {
switch (variant) {
case 'primary':
return '#ff6aae';
case 'secondary':
return: '#ff52a3';
default:
return 'black';
}}};
color: white;
`
export const Button = ({ children, variant }) => {
return (
<StyledButton variant={variant}>
{children}
</StyledButton>
)
}
That's it. Now we have a beautiful button that gets styled by its variants. For sure you could write a ternary statement but for scalability demonstration, I chose the switch-case here. In some unexpected cases when we don't have variant styles, they will fall back to default styles. And all that without using external libraries such as classNames and ternary conditions.
Also, we can make components inheritance like this:
// components/Button/styled.js
export const StyledButtonPrimary = styled(StyledButton)`
background-color: #ff6aae;
`
export const StyledButtonSecondary = styled(StyledButton)`
background-color: #ff52a3;
`
And that's how you create new styled components based on the old ones. The new components will inherit all the styles from their parent components and have rewritten background colors. This is another way of implementing component variations. But don't forget that when you change the parent, the child also changes.
Build
Tailwind
Tailwind is building CSS into a single file.
You might think: "It will build all classes, it’s a lot of them, right?"
Yes, but Tailwind includes only used classes in the bundle. So if you are using a single class, let’s say text-lg
, in the whole project, the resulting bundle will include only a single class. Adding prefixes version of CSS automatically happens because of the autoprefixer postcss plugin.
In addition to that, you can go further in terms of performance.
Since Tailwind is a PostCSS plugin, we can also add cssnano plugin to compress the bundle.
Drawback: can't build conditional classes.
Linaria
The main feature of Linaria is that it is zero-runtime and all CSS is extracted at build time. It allows you to write CSS code inside JavaScript files, but instead of bundling the CSS with JavaScript, it extracts the styles into a separate CSS file during the build process. This way, you can enjoy the benefits of CSS in JS, such as dynamic prop-based styles, without adding any runtime overhead or increasing the bundle size.
At all other points, Linaria has the same pros and cons as styled-components.
Conclusion
Tailwind gave a huge push to the development of functional frameworks. New frameworks take beneficial parts of the existing technologies and mix them with problem-solving logic.
For example twin.macro, which is CSS-in-JS solution but with tailwind classnames. Or another one, unocss. It gives you space to implement your own utility classes or use existing presets.
In summary, Linaria stands out as a versatile tool for modern web development, offering a seamless solution to styling challenges with its unique blend of runtime CSS-in-JS and prop-based styling options. Its concise syntax and minimal bundle size make Linaria an optimal choice, striking a balance between performance, maintainability, and developer experience in the dynamic world of front-end development.
When choosing a solution for your projects, as always, there are pros and cons to each of them. In some cases, you prioritize speed and want to go with the tailwind default structure. Sometimes, you have a huge project with styled components and you want to optimize and increase performance by removing run-time dependency, and you would prefer to use Linaria, over rewriting the whole project.
So to make the right decision, you need to understand the details of 3 things:
- the problem you solve
- the team you work with
- the project you develop
With this knowledge, you should consider all aspects of CSS, and how this will play with your system and help you achieve ultimate results and performance.
In FocusReactie, as optimization and performance specialists, we do a deep analysis of the project and requirements before making any decision. This helps our clients achieve maximum results and gives their users an instantaneous experience when surfing their websites.