css-class-positions
v1.0.0
Published
Find CSS class selector positions in any CSS-like file (CSS, SCSS, Less, Sass, Stylus)
Maintainers
Readme
css-class-positions
A fast, zero-dependency scanner that finds where CSS classes are defined in any stylesheet — CSS, SCSS, Less, Sass, Stylus, SugarSS. No AST or parser needed.
[!NOTE] For CSS-like file formats with
.classNameselector syntax. CSS-in-JS (styled-components, Emotion, etc.) is not supported.
Install
npm install css-class-positionsUsage
import { cssClassPositions } from 'css-class-positions'
const positions = cssClassPositions(`
.button { color: red; }
.header { color: blue; }
`, { fileName: 'style.module.scss' })
positions.get('button') // { line: 2, column: 1 }
positions.get('header') // { line: 3, column: 1 }Returns a Map<string, Position> of decoded class name → 1-based position of the . character. Records the first occurrence of each class (the definition site).
Why
CSS tooling like CSS Modules knows which class names exist in a file, but not where they're defined. That position data is needed for source maps and IDE go-to-definition. Getting it normally requires a full AST parse — and every preprocessor needs its own parser.
This module does it with a single O(n) scanner. No AST, no dependencies, every syntax. It skips the many places where .identifier appears but isn't a class selector — comments, strings, URLs, at-rule headers, property values, Less mixins, preprocessor interpolation — so only real definitions are returned.
Benchmarks
vs. PostCSS parse() + walkRules():
| | css-class-positions | PostCSS | | :--- | ---: | ---: | | Install size | 36 KB (1 package) | 976 KB (7 packages) |
| Input | css-class-positions | PostCSS | Speedup | | :--- | ---: | ---: | ---: | | CSS, 50 classes | 52 µs | 111 µs | 2.1x | | CSS, 500 classes | 478 µs | 1.16 ms | 2.4x | | CSS, 5000 classes | 4.9 ms | 12.6 ms | 2.6x | | SCSS, 50 classes | 107 µs | 237 µs | 2.2x | | SCSS, 500 classes | 1.08 ms | 2.60 ms | 2.4x | | Less, 50 classes | 81 µs | 501 µs | 6.2x |
Speed: mitata, Apple M2 Max, Node 24.11.1. Run pnpm bench to reproduce.
API
cssClassPositions(css, options?)
cssstring— CSS source textoptions.fileNamestring— file path (used to infer syntax from extension)options.lineCommentsboolean | 'auto'— whether//is a comment (default: inferred fromfileName, or'auto')- Returns
Map<string, Position>
fileName
Pass the file path and the scanner configures itself:
| Extension | // handling |
| :--- | :--- |
| .css | data (not a comment) |
| .scss .less .sass .styl .sss | line comment |
| other / not set | heuristic |
lineComments
Override // handling explicitly. This is the main cross-syntax ambiguity: // is data in CSS but a comment in SCSS/Less/Sass/Stylus.
false— never a commenttrue— always a comment'auto'— heuristic (comment unless it looks like a URL path)
Position
type Position = {
line: number // 1-based
column: number // 1-based, UTF-16 code units (matches VS Code, source maps, LSP)
}The scanner identifies .className patterns while skipping all non-selector contexts:
- Block comments (
/* ... */) and line comments (// ...) - Strings (
"..."and'...') - URL functions —
url(),url-prefix(),domain(),regexp() - At-rule headers —
@layer,@supports,@scope,@container,@when/@else,@document,@nest,@import,@apply,@variant,@custom-selector,@custom-variant @extend/@extendsreferences (SCSS, Sass, Stylus):export { }blocks (CSS Modules):extend()arguments (Less)- Attribute selectors (
[data-type=.foo]) - Property values (backward look detects
:before.) - Less mixin calls (
.name(),.name;) and definitions (.name() { }) - Less variable assignments (
@var: value;) and detached rulesets (@var: { ... };) - Preprocessor interpolation —
#{}(SCSS),@{}(Less),-{var}(Stylus)
Identifiers are decoded per the CSS spec, including hex and character escape sequences.
