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.
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 valuesWhat 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.
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 prefixThe 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
Renamed imports
The scanner matches helpers by name, so renaming one at the import boundary hides every call behind it.
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 fineThis 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.
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:
tsx ts mts cts jsx js mjs cjs mdx md html vue svelte astroMarkup 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.