CSS, SCSS, and Tailwind analysis
Find unused CSS Module classes and Tailwind tokens, and stop false unused-file and unused-dependency reports for stylesheets. Fallow resolves CSS imports, SCSS partials, @use, @forward, and Tailwind directives.
Fallow reads your stylesheets, so it can report unused CSS Module classes and Tailwind tokens, and it does not report used stylesheets or Tailwind packages as unused. It resolves @use, @forward, SCSS partials (_prefix files), and Tailwind directives. You do not need to configure anything.
SCSS import resolution
Fallow supports the full SCSS module system. Each import adds an edge to the module graph, and fallow follows re-exports through the dependency chain.
| Directive | How fallow handles it |
|---|---|
@use 'path' | A module import that adds a graph edge |
@forward 'path' | A re-export in the module graph |
@import 'path' | A legacy import, tracked as a side-effect edge |
@use 'sass:math' | A Sass built-in module, never reported as missing |
Fallow resolves SCSS partials with the standard convention. For @use 'components/button', it tries these candidates in order:
components/_button.scss(partial convention)components/button/_index.scss(directory index)components/button/index.scss(plain index)
@use 'sass:color';
@use 'variables';
@use 'mixins/responsive';
@forward 'tokens' show $primary, $secondary;
Fallow reads this file as follows:
- It imports the
sass:colorbuilt-in. Fallow ignores it, because it is not a project dependency. - It imports
./variables, which resolves to_variables.scssthrough the partial convention. - It imports
./mixins/responsive, which resolves tomixins/_responsive.scss. - It re-exports
$primaryand$secondaryfrom./tokens.
Fallow also supports SCSS include paths, which frameworks such as Angular configure. When fallow cannot resolve a bare specifier such as @use 'variables' locally, it searches the configured include directories with the same partial and index conventions.
CSS Module class tracking
CSS Module class tracking finds the classes that no component uses. For files with the .module.css, .module.scss, .module.sass, or .module.less extension, fallow reads every class name in the selectors as a named export. A class that appears in more than one selector is one export, so it does not cause duplicate findings.
.root {
display: flex;
align-items: center;
}
.primary {
background: var(--color-primary);
}
.disabled {
opacity: 0.5;
pointer-events: none;
}
This file exports three named symbols: root, primary, and disabled.
When a component imports a CSS module, fallow tracks which classes the component accesses. A default import can use any local name. If the code passes or uses the whole imported object, fallow marks every class as used:
import styles from './Button.module.css';
export const Button = ({ variant }: Props) => (
<button className={`${styles.root} ${styles.primary}`}>
Click me
</button>
);
Here, fallow marks styles.root and styles.primary as used. No code references the disabled class, so fallow reports it as an unused export.
── Unused Code ─────────────────────────────────────
● Unused exports (1)
src/Button.module.css
:10 disabled
Exported symbols with no known consumers
https://docs.fallow.tools/explanations/dead-code#unused-exports
✗ 1 export (0.02s)
Run fallow dead-code --unused-exports to see the unused CSS module class names in your project.
Tailwind CSS integration
When a CSS or SCSS file has an @apply or @tailwind directive, fallow adds a synthetic dependency on the tailwindcss package. This prevents a false "unused dependency" report.
@tailwind base;
@tailwind components;
@tailwind utilities;
.btn {
@apply px-4 py-2 rounded-md font-medium;
}
Fallow finds the @tailwind and @apply directives and marks tailwindcss as a used dependency.
@plugin directive (Tailwind v4)
Tailwind v4 registers plugins in CSS with the @plugin directive. Fallow reads each @plugin target as a default import. A package plugin counts as a used dependency. For a relative plugin file, fallow marks the default export as used.
@import "tailwindcss";
@plugin "@tailwindcss/typography";
@plugin "daisyui" {
themes: light --default;
}
@plugin "./tailwind-local-plugin.js";
Fallow marks @tailwindcss/typography and daisyui as used dependencies. It marks ./tailwind-local-plugin.js as reachable and its default export as used. Fallow also reads the directive in Vue and Svelte <style> blocks. A package name with no extension, such as daisyui, stays a package specifier, also in .scss and .sass files. Fallow does not resolve it as a local partial, as it does for SCSS @use.
Unused Tailwind v4 @theme tokens
With fallow health --css, fallow also reports @theme design tokens in Tailwind v4 projects that you can possibly remove. A token is a cleanup candidate when fallow sees no reference to its generated utility.
@import "tailwindcss";
@theme {
--color-brand: #f05a28;
--shadow-glow: 0 0 8px red;
}
export const App = () => <div className="bg-brand" />;
Here, --color-brand counts as used, because bg-brand uses the generated utility suffix. Fallow reports --shadow-glow in css_analytics.unused_theme_tokens with a read-only verification action.
These also count as a use of a token:
var(--token)reads@applyutilities- arbitrary values such as
rounded-[--radius-card] - token-to-token references inside
@themeblocks
The check is conservative. It runs only for Tailwind v4 projects. It reports nothing when Tailwind plugins or published CSS can use tokens in ways fallow cannot see. Each result is a candidate, never an auto-fix.
The other CSS cleanup signals follow the same rules. Each is a candidate with a read-only verification action:
| Field | What fallow reports |
|---|---|
css_analytics.unreferenced_css_classes | A plain-CSS class that no class or className in project markup matches |
css_analytics.unresolved_class_references | The reverse: a markup class one edit away from a defined class, probably a typo |
css_analytics.unused_font_faces | Unused font faces |
css_analytics.undefined_keyframes | Undefined keyframes |
css_analytics.font_size_unit_mix | Mixed font-size units |
These signals point a reviewer to a class, font, animation, or type-scale mismatch that is probably stale. They do not claim that it is always safe to delete.
Token blast-radius (token_consumers)
Before you change a token, you want to know what uses it. token_consumers answers this question: it lists the consumers of each token. On Tailwind v4 projects, fallow health --css --format json adds css_analytics.token_consumers. It is a reverse index over the same gated set of @theme tokens:
{
"token": "--color-brand",
"namespace": "color",
"definition_path": "src/theme.css",
"definition_line": 4,
"consumer_count": 12,
"consumers": [
{ "path": "src/Button.tsx", "line": 7, "kind": "utility" },
{ "path": "src/theme.css", "line": 9, "kind": "theme-var" }
]
}
Each consumer location has a kind:
utility: a markup class that ends in-<name>, such asbg-brandapply: a class inside an@applybodycss-var: avar(--token)read in regular CSStheme-var: avar()reference inside another@themetoken
consumers is a capped sample. consumer_count is the full total.
consumer_count is a static lower bound. The scan cannot see a computed class name such as bg-${color}. A 0 therefore means that fallow sees no consumer. These are the same tokens that unused_theme_tokens reports, and a 0 does not prove that the token is safe to delete. token_consumers gives context for a change. The cleanup candidate is still unused_theme_tokens.
CSS-in-JS tokens
token_consumers also covers CSS-in-JS token DEFINITIONS. You see the consumers of these tokens in the same way as for an @theme token:
- StyleX
defineVars - vanilla-extract
createTheme/createThemeContract/createGlobalTheme - PandaCSS
defineTokens
JS objects define these tokens. Code uses them through member access across modules (import { vars } from './tokens'; vars.color.primary) or through Panda token('colors.brand') calls. The entry shape therefore depends on the origin:
{
"token": "vars.color.primary",
"namespace": "vars",
"definition_path": "src/tokens.stylex.ts",
"definition_line": 3,
"consumer_count": 4,
"consumers": [
{ "path": "src/Card.tsx", "line": 12, "kind": "js-member" }
]
}
The consumer kind and the token shape tell the origins apart:
| Origin | token shape | Consumer kind |
|---|---|---|
| Tailwind | The ---prefixed custom property (--color-brand) | theme-var / css-var / utility / apply |
| StyleX, vanilla-extract | A dotted access path with the binding name (vars.color.primary) | js-member, or js-call for a StyleX createTheme call |
| PandaCSS | The defining binding plus the token path (tokens.colors.brand) | js-call for token(...) consumers |
Before it matches, the cross-file scan resolves each import to its defining file. A vars with the same name from an unrelated module never counts.
Two limits are stricter than for Tailwind:
- The cross-file scan uses the shared import resolver of fallow. Relative imports, tsconfig
pathsaliases, and workspace package imports can resolve to the token definition. Dynamic import strings, unresolved aliases, generated package state, and computed token access still makeconsumer_counta lower bound. - CSS-in-JS has no dead-token finding that confirms the count (no equivalent of
unused_theme_tokens). A CSS-in-JSconsumer_countof0is a weaker signal that fallow sees no consumers. It does not mean that you can delete the token.
Fallow reports CSS-in-JS token consumers only when the project declares a CSS-in-JS library (@stylexjs/stylex, @vanilla-extract/css, or @pandacss/dev).
Tailwind v3 config
The Tailwind plugin also parses tailwind.config.{js,ts,cjs,mjs}:
- Content globs. Fallow treats the globs in the
contentarray as always-used file patterns. - Plugin packages. Plugins in
require()calls or string arrays count as used dependencies. - Presets. Fallow tracks presets as referenced dependencies.
- Imports. Fallow tracks imports in the config file (for example
import defaultTheme from 'tailwindcss/defaultTheme').
import defaultTheme from 'tailwindcss/defaultTheme';
export default {
content: ['./src/**/*.{js,ts,jsx,tsx}'],
plugins: [
require('@tailwindcss/typography'),
require('@tailwindcss/forms'),
],
};
Fallow marks tailwindcss, @tailwindcss/typography, and @tailwindcss/forms as used dependencies.
CSS @import tracking
Each plain CSS @import statement adds an edge to the module graph. Fallow tracks relative paths and package imports.
@import './reset.css';
@import './typography.css';
@import '@fontsource/inter/400.css';
Fallow resolves ./reset.css and ./typography.css as local files, and @fontsource/inter as a package dependency. It ignores remote URLs (https://...) and data URIs.
Styling health grade
The styling health grade gives you one number for the quality of your CSS. fallow health --css computes it from the raw css_analytics: an A-F letter and a 0-100 score. Fallow scores it separately from the JavaScript and TypeScript code health score. The JSON output has it as styling_health. The human output shows a Styling health: line under CSS health.
"styling_health": {
"formula_version": 3,
"score": 96.0,
"grade": "A",
"penalties": { "duplication": 0.0, "dead_surface": 0.0, "broken_references": 0.0, "token_erosion": 4.0, "structural": 0.0 },
"confidence": "high",
"confidence_reason": null
}
The score starts at 100 and subtracts five capped penalties:
| Penalty | What it measures |
|---|---|
duplication | Removable declarations in copied declaration blocks. It has a low weight, because exact CSS duplication does the least harm: gzip removes it, and CSS has no native abstraction. |
dead_surface | Unused @theme tokens (as a share of all defined tokens), unreferenced classes, unused @property and @layer at-rules, and dead @font-face families. |
broken_references | Markup classes one edit from a defined class, and animations that reference a @keyframes that no file defines. These are probably typos or old names. |
token_erosion | Design-token erosion: mixed font-size units, Tailwind arbitrary values that bypass tokens, and hardcoded value drift. Drift is the number of distinct hardcoded box-shadow / border-radius / line-height values above a healthy palette per property. |
structural | !important density above a healthy floor, and deep style-rule nesting. |
Value drift over exact repetition
The grade gives design-token drift (inconsistent values) more weight than identical repetition, because design tokens exist to fix inconsistent values. The token_erosion value-drift signal counts only hardcoded literals. Fallow never counts var(--*) references and @theme definitions as distinct hardcoded values. A system that uses its scale through custom properties (box-shadow: var(--shadow-md)) therefore scores zero drift, however many tokens it defines. A system that hardcodes many one-off shadows, radii, and line heights gets a small penalty that grows slowly. The penalty points you toward tokens and never jumps sharply.
Confidence and scope
Some grades come from little CSS: few authored declarations, or mostly compile-time atomic CSS-in-JS such as StyleX or Panda. Fallow marks such a grade confidence: "low" and states the reason. You can then tell it from a grade for a full design system. Confidence is metadata only and never changes the score.
Styling health is descriptive-only. It never changes an exit code, a badge, or a CI result, and it never affects the JavaScript and TypeScript health_score. Use it to judge the state of your design system, not as a pass or fail gate.
formula_version increases each time the styling-health rubric changes. It is separate from the code-health formula version. You can therefore tell a score change from new weights apart from a change in your CSS. If you compare styling_health.score / grade over time (snapshot CIs, trend dashboards), check formula_version and make a new baseline when it increases.
Common patterns that just work
| Pattern | Example | How fallow handles it |
|---|---|---|
| SCSS partial imports | @use 'mixins' | Resolves to _mixins.scss |
| Sass built-in modules | @use 'sass:math' | Ignored, never reported as missing |
@use with namespace | @use 'colors' as c | Tracked as module import |
@forward with show or hide | @forward 'tokens' show $primary | Re-export with filtering |
| CSS custom properties | var(--color-primary) | File-level tracking (not per-property) |
| PostCSS plugins | postcss.config.js | Fallow detects plugin dependencies from the PostCSS config |
| Scoped CSS packages | @import '@company/tokens/base.scss' | Resolved as npm package, not local file |
| SCSS directory index | @use 'components' | Resolves to components/_index.scss |