Ok, so the other day, after creating a new Next.js project, I suddenly became curious about its default global.css file—the one located in the root folder. Here is its entire content:
@import "tailwindcss";
:root {
--background: #ffffff;
--foreground: #171717;
}
@theme inline {
--color-background: var(--background);
--color-foreground: var(--foreground);
--font-sans: var(--font-geist-sans);
--font-mono: var(--font-geist-mono);
}
@media (prefers-color-scheme: dark) {
:root {
--background: #0a0a0a;
--foreground: #ededed;
}
}
body {
background: var(--background);
color: var(--foreground);
font-family: Arial, Helvetica, sans-serif;
}
This got me curious because, besides the normal body rule, the @import at-rule, the var() function, and the variables --background, --foreground, etc., I didn’t really understand the other structures.
So, I decided to have a little discussion with ChatGPT, and here is what I learned.
The :root selector
The :root selector targets the <html> element. As a result, CSS variables declared here (--background and --foreground) are available to the entire document, much like global variables in a programming language.
Later, they are consumed like this:
body {
background: var(--background);
color: var(--foreground);
}
At runtime, the browser resolves var(--background) and var(--foreground) to actual values.
The @media (prefers-color-scheme) at-rule
@media (prefers-color-scheme: dark) {
:root {
--background: #0a0a0a;
--foreground: #ededed;
}
}
The @media at-rule works like an if statement for CSS. You can think of it as:
if (prefersColorScheme === "dark") {
--background = "#0a0a0a";
--foreground = "#ededed";
}
In other words, when the operating system prefers dark mode, the CSS variables declared in :root are overridden with the values #0a0a0a for --background and #ededed for --foreground.
Tailwind class syntax
Before continuing with our CSS file, we need to take a step back and look at how Tailwind works.
If you want to change the style of an HTML element using Tailwind, you add utility classes to it. Each Tailwind class changes only a single style. For example:
bg-whitesetsbackground-colorto whitetext-bluesets thecolorproperty to bluemt-4setsmargin-topto1rem(16px by default)
You can combine multiple utilities like this:
<div className="bg-white text-blue mt-4"></div>
So how does this work?
All Tailwind utility classes follow the same structure:
<prefix>-<token>
Prefixes: choosing the CSS property
Prefixes correspond to utility generators built into Tailwind. Each generator emits a specific CSS property.
Examples:
| Prefix | CSS property emitted |
|---|---|
bg- | background-color |
text- | color |
border- | border-color |
font- | font-family |
p- | padding |
You can visualize this mapping like so:
bg-* → background-color: VALUE
text-* → color: VALUE
border-* → border-color: VALUE
In CSS terminology, background-color: #123456; is a declaration, where background-color is the property and #123456 is the value. Prefixes decide the property in the declaration.
Tokens: choosing the value
Tokens decide the value of the CSS declaration.
Examples:
| Token | Value |
|---|---|
white | #ffffff |
2 | 0.5rem |
sans | system-ui, sans-serif |
background | var(--color-background) |
Internally, Tailwind groups tokens into categories. Conceptually, you can imagine objects like these somewhere in Tailwind’s codebase:
tokens.colors = {
white: "#ffffff",
black: "#000000",
};
How Tailwind generates CSS at build time
At build time, the Tailwind compiler scans your codebase to see which utility classes are used. For example, if it encounters the following markup:
<div className="bg-white text-black">Hello world!</div>
Tailwind will activate the bg- and text- utility generators. These generators read from tokens.colors, retrieve the values for white and black, and generate the following CSS:
.bg-white {
background-color: #ffffff;
}
.text-black {
color: #000000;
}
This CSS is included in the final stylesheet sent to the browser.
Little knowledge check
What would happen if we declared something crazy like this?
<div className="p-white">Hello crazy world!</div>
The p- utility generator reads from the spacing category, which might look conceptually like this:
tokens.spacing = {
0: "0px",
1: "0.25rem",
2: "0.5rem",
};
Since white does not exist in tokens.spacing, Tailwind does not generate any CSS for the p-white class.
The @theme inline at-rule
Now let’s go back to our original CSS file. What does this block mean?
@theme inline {
--color-background: var(--background);
}
Here’s how to read it:
background(in--color-background) is the token namecolor(in--color-background) is the category the token belongs toinlinetells Tailwind to keep the value as a CSS variable and defer resolution until runtime
In other words, this code tells Tailwind to register a new color token named background with the value var(--background).
Conceptually, the token object now looks like this:
tokens.colors = {
white: "#ffffff",
black: "#000000",
background: "var(--background)",
};
Using the new token
Later, when Tailwind scans your codebase and finds:
<div className="bg-background">Hello world!</div>
The bg- generator emits a rule using the registered token:
.bg-background {
background-color: var(--background);
}
At runtime, the browser resolves var(--background) using the value defined on :root (for example, #ffffff in light mode). The CSS rule itself is not modified; only the computed value changes.
Another little knowledge check
So what happens if we remove inline?
@theme {
--color-background: var(--background);
}
Without inline, Tailwind assumes token values are static and must be resolved at build time. Since var(--background) cannot be resolved during compilation, the token is not registered in tokens.colors.
As a result, when Tailwind encounters bg-background, no CSS rule is generated, and the class has no effect.
Conclusion
In short, here’s how Tailwind works:
- prefixes choose CSS properties
- tokens provide values
- categories determine which generators can consume which tokens
@theme inlineallows runtime resolution via CSS variables
Who would have thought that a short conversation with an AI, starting from a single CSS file, could provide such a lesson? That’s one of the reasons I enjoy learning with AI.