Skip to main content

What the scanner sees

The literal strings the tailess scanner can read at your call sites, the forms it cannot see, and how to work around each case.

What it sees

The scanner reads the literal strings at your call sites and never runs your code, so a class becomes a candidate only when it is written out in the source. Within that limit the scan is deliberately generous: both branches of a ternary, every key of an object, every element of an array.

The asymmetry is what justifies over-approximating. An extra candidate costs nothing, because Tailwind ignores the ones that do not resolve, while a missing one costs you the style.

Seen
ss({ md: "text-2xl", hover: "underline" })          // literalsss({ md: isWide ? "grid-cols-3" : "grid-cols-1" })  // both branchesss({ md: ["flex", cond && "gap-4"] })               // arraysss({ md: [{ "text-lg": cond }] })                   // clsx object form, in an arrayss({ md: "text-lg", /* both survive */ lg: "xl" })  // comments anywheress({ dark: { hover: "bg-black" } })                 // nested — dark:hover:bg-blackss(a, cond && { sm: "bg-red-500" })                 // a map behind a conditionss(a, open ? { md: "p-6" } : { md: "p-2" })         // both branches, as mapson(["dark", "hover"], "bg-black")                   // compound variantsdata("state", open ? "open" : "closed", "p-2")      // both values

What it cannot see

Every case below fails for the same reason: the value is not at the call site to read, so the finished class exists only once your code runs.

Not seen
const size = "text-2xl";ss({ md: size });                    // a variabless({ md: `text-${scale}` });         // an interpolated templatess({ ...spread });                   // a spreadss({ md: { [key]: "grid" } });       // a computed keywithPrefix(dynamicPrefix, "grid");   // a computed prefix

The class still lands on the element with no rule behind it, so the element is simply unstyled. With the plugin installed its integration marker is in place, so no warning fires and the missing style is the only symptom.

One class, or all of them

If every prefixed class is unstyled, the plugin is not running, and in dev it says so. If one class is unstyled while the rest work, it is almost certainly not a literal at the call site.

Renamed imports

The scanner matches helpers by name, so renaming one at the import boundary hides every call behind it.

Renamed import
import { ss as tw } from "tailess";tw({ md: "p-6" });                   // ✗ not found — nothing supplies md:p-6 import * as t from "tailess";t.ss({ md: "p-6" });                 // ✓ a namespace import is fine

This one is worth remembering because the code around it is correct and fully literal — the class is spelled out, and only the name the scanner looks for is missing. Import ss under its own name, or reach it through a namespace import, which keeps that name in the source.

The match workaround

If you need one of the forms above, put the literal somewhere the scanner can reach it — usually by writing the full class in a match() lookup, so the dynamic part stays in the discriminant instead of in the class name.

match
const size = match(scale, { sm: "text-sm", lg: "text-2xl" });

That needs no build integration at all: every class in it is a literal Tailwind finds by itself. See match for the exhaustiveness rules and the fallback argument.

Scanned files

Scanned by default:

Extensions
tsx ts mts cts jsx js mjs cjs mdx md html vue svelte astro

Markup files work the same as JS ones — an apostrophe in your prose or a :class="…" attribute will not throw the scanner off.

The extensions option replaces that list rather than adding to it, so pass the whole set you want scanned; content chooses the files or directories to scan, including ones outside the default root. Both are covered in Plugin options.