Skip to main content
Fallow tracks CSS and SCSS files using dedicated extraction, not regex pattern matching against source text. @use, @forward, SCSS partials (_prefix files), and Tailwind directives are all resolved accurately. No manual configuration needed.

SCSS import resolution

Fallow understands the full SCSS module system. Imports create edges in the module graph, and re-exports propagate through the dependency chain.
DirectiveHow fallow handles it
@use 'path'Resolved as a module import, creating a graph edge
@forward 'path'Treated as a re-export in the module graph
@import 'path'Legacy import, tracked as a side-effect edge
@use 'sass:math'Recognized as a Sass built-in module, not flagged as missing
SCSS partial resolution follows the standard convention. When fallow encounters @use 'components/button', it resolves through these candidates in order:
  1. components/_button.scss (partial convention)
  2. components/button/_index.scss (directory index)
  3. components/button/index.scss (plain index)
styles/theme.scss
@use 'sass:color';
@use 'variables';
@use 'mixins/responsive';

@forward 'tokens' show $primary, $secondary;
Fallow sees this file as:
  • Importing the sass:color built-in (correctly ignored, not a project dependency)
  • Importing ./variables, resolved to _variables.scss via partial convention
  • Importing ./mixins/responsive, resolved to mixins/_responsive.scss
  • Re-exporting $primary and $secondary from ./tokens
SCSS include paths (configured via frameworks like Angular) are also supported. When a bare specifier like @use 'variables' cannot be resolved locally, fallow searches configured include directories with the same partial and index conventions.

CSS Module class tracking

Files with .module.css or .module.scss extensions receive special treatment. Fallow extracts every class name from selectors and exposes them as named exports.
Button.module.css
.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 are actually accessed:
Button.tsx
import styles from './Button.module.css';

export const Button = ({ variant }: Props) => (
  <button className={`${styles.root} ${styles.primary}`}>
    Click me
  </button>
);
Here, styles.root and styles.primary are marked as used. The disabled class is never referenced, so fallow reports it as an unused export.
$ fallow dead-code --unused-exports
 Unused exports (1)
  src/Button.module.css
    :3 disabled
  Exported symbols with zero references: https://docs.fallow.tools/explanations/dead-code#unused-exports

 1 issue (0.02s)
Start with fallow dead-code --unused-exports to see which CSS module class names are unused across your project.

Tailwind CSS integration

When fallow encounters @apply or @tailwind directives in any CSS or SCSS file, it creates a synthetic dependency on the tailwindcss package. This prevents false “unused dependency” reports.
globals.css
@tailwind base;
@tailwind components;
@tailwind utilities;

.btn {
  @apply px-4 py-2 rounded-md font-medium;
}
Fallow detects both @tailwind and @apply directives and marks tailwindcss as a used dependency.

@plugin directive (Tailwind v4)

Tailwind v4 moves plugin registration into CSS via the @plugin directive. Fallow extracts each @plugin target as a default import, so package plugins are credited as used dependencies and relative plugin files have their default export marked used.
app.css
@import "tailwindcss";
@plugin "@tailwindcss/typography";
@plugin "daisyui" {
  themes: light --default;
}
@plugin "./tailwind-local-plugin.js";
Fallow marks @tailwindcss/typography and daisyui as used dependencies, and treats ./tailwind-local-plugin.js as a reachable file whose default export is consumed. The directive is also recognized inside Vue and Svelte <style> blocks. Unlike SCSS @use, extensionless package names such as daisyui stay bare even in .scss and .sass files (they are package specifiers, not local partials).

Unused Tailwind v4 @theme tokens

With fallow health --css, Tailwind v4 projects also get cleanup candidates for @theme design tokens whose generated utility is not referenced anywhere fallow can see.
theme.css
@import "tailwindcss";

@theme {
  --color-brand: #f05a28;
  --shadow-glow: 0 0 8px red;
}
App.tsx
export const App = () => <div className="bg-brand" />;
In this example, --color-brand is credited because bg-brand uses the generated utility suffix. --shadow-glow is reported in css_analytics.unused_theme_tokens with a read-only verification action. Fallow also credits var(--token) reads, @apply utilities, arbitrary values such as rounded-[--radius-card], and token-to-token references inside @theme blocks. The check is conservative: it only runs for Tailwind v4 projects, abstains when Tailwind plugins or published CSS surfaces may consume tokens invisibly, and treats every result as a candidate, never as an auto-fix. Other CSS cleanup signals follow the same contract. css_analytics.unreferenced_css_classes (a plain-CSS class defined but matched by no class/className in project markup), css_analytics.unresolved_class_references (the reverse: a markup class one edit away from a defined class, a likely typo), css_analytics.unused_font_faces, css_analytics.undefined_keyframes, and css_analytics.font_size_unit_mix are advisory candidates with read-only verification actions. They are meant to send a reviewer to the likely stale class, font, animation, or type-scale mismatch without claiming it is always safe to delete.

Token blast-radius (token_consumers)

The inverse of the unused-token signal: instead of asking “is this token dead?”, token_consumers answers “what consumes this token?” so you can see a token’s blast radius before changing it. On Tailwind v4 projects fallow health --css --format json adds css_analytics.token_consumers, a reverse index over the same gated @theme token set:
{
  "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 carries a kind: utility (a markup class ending in -<name>, such as bg-brand), apply (a class inside an @apply body), css-var (a regular-CSS var(--token) read), or theme-var (a var() reference inside another @theme token). consumers is a capped sample; consumer_count is the full total. consumer_count is a static lower bound: a computed class name like bg-${color} is invisible to the scan, so a 0 here is a “nothing fallow can see consumes this” signal, the same population that surfaces in unused_theme_tokens, not a proof that the token is safe to delete. token_consumers is descriptive context for change decisions; the cleanup candidate stays unused_theme_tokens.

CSS-in-JS tokens

token_consumers also covers CSS-in-JS token DEFINITIONS, so changing a StyleX defineVars, vanilla-extract createTheme / createThemeContract / createGlobalTheme, or PandaCSS defineTokens token shows its blast radius the same way an @theme token does. These tokens are defined in JS objects and consumed through cross-module member access (import { vars } from './tokens'; vars.color.primary) or Panda token('colors.brand') calls, so the entry shape differs by 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 origins are disambiguated by the consumer kind and by the token shape: a Tailwind token’s token is the ---prefixed custom property (--color-brand) with kind theme-var / css-var / utility / apply; StyleX and vanilla-extract tokens use a binding-qualified dotted access path (vars.color.primary) with kind js-member; PandaCSS tokens use the defining binding plus token path (tokens.colors.brand) and token(...) consumers are tagged js-call. The cross-file scan resolves import edges to their defining files before matching, so an unrelated same-named vars from a different module is never counted. Two caveats are sharper than the Tailwind case. The cross-file scan uses fallow’s shared import resolver, so relative imports, tsconfig paths aliases, and workspace package imports can resolve to the token definition. Dynamic import strings, unresolved aliases, generated package state, and computed token access still keep consumer_count a lower bound. And unlike Tailwind there is no corroborating dead-token finding (no CSS-in-JS analogue of unused_theme_tokens), so a CSS-in-JS consumer_count of 0 is a weaker “fallow sees no consumers” signal, not a deletion verdict. CSS-in-JS token blast-radius is gated on a declared CSS-in-JS library (@stylexjs/stylex, @vanilla-extract/css, or @pandacss/dev).

Tailwind v3 config

The Tailwind plugin provides additional coverage by parsing tailwind.config.{js,ts,cjs,mjs}:
  • Content globs from the content array are treated as always-used file patterns
  • Plugin packages referenced via require() or string arrays are marked as used dependencies
  • Presets are tracked as referenced dependencies
  • Imports in the config file (e.g., import defaultTheme from 'tailwindcss/defaultTheme') are tracked
tailwind.config.js
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

Plain CSS @import statements create edges in the module graph. Both relative paths and package imports are tracked.
styles/main.css
@import './reset.css';
@import './typography.css';
@import '@fontsource/inter/400.css';
Fallow resolves ./reset.css and ./typography.css as local file imports, and @fontsource/inter as a package dependency. Remote URLs (https://...) and data URIs are correctly ignored.

Styling health grade

On top of the raw css_analytics, fallow health --css derives a styling health grade: a descriptive A-F letter (and a 0-100 score) for the quality of the CSS itself, scored separately from the JavaScript/TypeScript code health score. It appears as styling_health in JSON and a Styling health: line under CSS health in human output.
"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:
PenaltyWhat it measures
duplicationRemovable declarations from copy-paste declaration blocks. Down-weighted to a soft hint (exact CSS duplication is the least-harmful pattern: it gzips away and CSS has no native abstraction).
dead_surfaceUnused @theme tokens (as a share of all defined tokens) plus unreferenced classes, unused @property/@layer at-rules, and dead @font-face families.
broken_referencesMarkup classes one edit from a defined class, plus animations referencing a @keyframes defined nowhere (likely typos or stale renames).
token_erosionDesign-token erosion: mixed font-size units, Tailwind arbitrary-value bypasses, and hardcoded value drift (the count of distinct hardcoded box-shadow / border-radius / line-height values above a healthy per-property palette).
structural!important density above a healthy floor, plus deep style-rule nesting.

Value drift over exact repetition

The grade weights design-token drift (inconsistency) more than byte-identical repetition, because divergent values are what design tokens exist to fix. The token_erosion value-drift signal counts only hardcoded literals: a system that references its scale through custom properties (box-shadow: var(--shadow-md)) scores zero drift no matter how many tokens it defines, because var(--*) references and @theme definitions are never counted as distinct hardcoded values. A system that hardcodes many ad-hoc shadows/radii/line-heights accrues a small, gently-growing penalty: a nudge toward tokenizing, never a cliff.

Confidence and scope

A grade computed from a thin CSS surface (few authored declarations, or predominantly compile-time-atomic CSS-in-JS like StyleX/Panda) is marked confidence: "low" with a stated reason, so a sparse-surface grade is not presented with the same authority as one from a full design system. Confidence is metadata only: it never changes the score. Styling health is descriptive-only. It never gates an exit code, a badge, or CI, and it never affects the JavaScript/TypeScript health_score. Treat it as a design-system credibility signal, not a pass/fail gate.
formula_version increments whenever the styling-health rubric is recalibrated (independently of the code-health formula version), so you can tell a score shift caused by a weight change apart from one caused by an actual change to your CSS. If you diff styling_health.score / grade over time (snapshot CIs, trend dashboards), gate on formula_version and re-baseline when it bumps.

Common patterns that just work

PatternExampleHow fallow handles it
SCSS partial imports@use 'mixins'Resolves to _mixins.scss
Sass built-in modules@use 'sass:math'Correctly ignored, not flagged as missing
@use with namespace@use 'colors' as cTracked as module import
@forward with show/hide@forward 'tokens' show $primaryRe-export with filtering
CSS custom propertiesvar(--color-primary)File-level tracking (not per-property)
PostCSS pluginspostcss.config.jsPlugin dependencies detected via PostCSS plugin 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

See also

Dead code analysis

How fallow builds the module graph and detects unused code.

Non-JS file types

All non-JavaScript file types fallow analyzes automatically.

Configuration

Configure entry points, rules, and analysis scope.