npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

typed-css-imports

v0.2.0

Published

Typed CSS and Sass module imports with plain tsc: `styles.buttn` is a compile error. A TypeScript 7.1 content mapper.

Readme

Neovim: renaming a class in a CSS module shows the type error in the component before saving, and completion offers the new name

/* button.module.css */
.button { color: white; }
.button-primary { background: royalblue; }
import styles from "./button.module.css";

styles.buttonPrimary; // string
styles.buttn;
// error TS2551: Property 'buttn' does not exist on type '{ readonly button: string; ... }'. Did you mean 'button'?
  • Real types, not { [key: string]: string }. Every class, id, @keyframes and @value name the bundler exports, and nothing else. Typos, removed classes and :global names are errors.
  • Same names as your bundler. Names come from postcss-modules, the plugin Vite uses, including localsConvention (camelCase, dashes, …).
  • No codegen, no .d.ts files to commit. Built on TypeScript 7.1 content mappers (7.1 is in beta). Add a class in the CSS file and it autocompletes in your component immediately, even before you save.
  • Go to definition, find references and rename. styles.button jumps to .button in the CSS, references include the rule, and renaming the class from either file edits the CSS and every component. See Editor features.
  • Overrides the generic stubs. The declare module "*.module.css" declarations from vite/client or Next.js stay in place; the mapped types take precedence.
  • Sass too. .module.scss and .module.sass are compiled with your project's Sass first, as your bundler does, so classes generated by mixins, loops and partials are typed.
  • No runtime. Your bundler already loads CSS modules. This package only provides types.

Setup

Requires TypeScript 7.1 and Node >= 22.12. Until 7.1 is released, install the beta:

pnpm add -D typed-css-imports [email protected]

tsconfig.json:

{
  "compilerOptions": {
    "allowArbitraryExtensions": true
  },
  "contentMappers": [
    {
      "package": "typed-css-imports",
      "extensions": [".module.css"], // add ".module.scss", ".module.sass" for Sass modules
      "options": { "localsConvention": "camelCase" } // optional: match your bundler's setting
    }
  ]
}

Run tsc with --runExternalCode

Content mappers run package code during type-checking, so TypeScript only starts them with an explicit opt-in:

tsc --noEmit --runExternalCode

Editors need the same opt-in from their language-server client; see Editor setup.

Can't use --runExternalCode or TypeScript 7.1? Generate files instead

typed-css-imports generate writes the same types to declaration files next to each module:

typed-css-imports generate [--locals-convention camelCase] src/**/*.module.css   # writes x.module.d.css.ts
typed-css-imports generate --load-path src/styles src/**/*.module.scss           # writes x.module.d.scss.ts
typed-css-imports generate --check src/**/*.module.css                           # exit 1 if any are stale

With allowArbitraryExtensions, tsc picks up button.module.d.css.ts for import styles from "./button.module.css" and no content mapper is needed.

Check it's working

If the mapper isn't running, imports don't fail: they fall back to the { [key: string]: string } stub from vite/client or Next.js, and typos pass silently. Add one file that turns that into an error:

// src/css-modules.check.ts
import styles from "./Button.module.css"; // any CSS module in your project

// @ts-expect-error typed-css-imports is not running if this line is not an error
styles.thisClassDoesNotExist;

With the mapper running, this file type-checks. If the mapper isn't running (for example an editor on a TypeScript version without content mappers, or a tsconfig.json that lost the contentMappers entry), it reports TS2578 "Unused '@ts-expect-error' directive", in CI and in the editor. examples/vite-react includes one.

Sass modules

Add .module.scss (and .module.sass) to extensions. The modules are compiled with the sass or sass-embedded package installed in your project (the one your bundler uses), so install it if it isn't already there. If your bundler config has Sass options that affect the output, mirror them under options.sass:

"options": {
  "sass": {
    "loadPaths": ["src/styles"],                  // relative to tsconfig.json; Vite: css.preprocessorOptions.scss.loadPaths
    "additionalData": "@use \"tokens\" as *;\n"  // Vite: css.preprocessorOptions.scss.additionalData (string form)
  }
}

Editor setup

The TypeScript 7.1 language server (tsc --lsp) starts content mappers only when the client sends initializationOptions: { runExternalCode: true }. Without it, CSS module imports report TS2307, or with vite/client types silently fall back to { [key: string]: string }.

How soon types update depends on what the editor sends the server:

  • As you type: if the editor also attaches the TypeScript server to .module.css (or .module.scss) buffers, each edit sends the unsaved CSS. The server re-runs the mapper on that text and asks the editor to refresh diagnostics, so errors in components appear before the CSS file is saved. This is what the demo above shows.
  • On save: otherwise the server sees the CSS file when it changes on disk.

tsc --noEmit --runExternalCode on the command line always reads saved files.

Editor features

Besides errors and completion, the mapper tells the language server where each name is declared in the module:

| | .button, #main, @keyframes spin | "button-primary", buttonPrimary (camelCase) | Sass &-title, .tone-#{$t} | |---|---|---|---| | Go to definition | The name in the CSS | The class it comes from | The selector that generates it | | Find references (from either file) | Yes | Yes | Yes | | Rename | From the component or the CSS, edits both | Not supported (see Limits) | Not supported |

Names that come from another file (a Sass partial or a mixin defined elsewhere) are typed, but have no definition in the module.

VS Code

  1. Install the TypeScript 7 extension (TypeScriptTeam.native-preview).

  2. Point js/ts.tsdk.path at the project's TypeScript 7.1 package, in .vscode/settings.json (relative to the workspace folder) or in user settings:

    {
      "js/ts.tsdk.path": "node_modules/typescript"
    }
  3. Run TypeScript: Select TypeScript Version... and choose Use Workspace Version. The extension starts with the TypeScript 7.0 it bundles and doesn't prompt to switch by default.

  4. Trust the workspace. In Restricted Mode the extension doesn't run at all, so nothing is type-checked.

The extension enables content mappers by default (js/ts.contentMappers.enabled) and attaches to open .module.css and .module.scss files, so edits update errors and completions in components before you save.

On the bundled 7.0 there are no errors: imports silently fall back to vite/client's { [key: string]: string }. The check file catches this, as long as the workspace is trusted.

Tested with VS Code 1.135 and extension version 1.0.1, with CSS and Sass modules.

LazyVim

In lua/config/options.lua:

vim.g.lazyvim_ts_lsp = "tsc"

In lua/plugins/typescript.lua:

return {
  "neovim/nvim-lspconfig",
  opts = function(_, opts)
    -- cmd and root_dir share a cache of which tsc binary to run, so take both from the same copy of lspconfig's config.
    local tsc = dofile(vim.api.nvim_get_runtime_file("lsp/tsc.lua", false)[1])
    opts.servers.tsc = vim.tbl_deep_extend("force", opts.servers.tsc or {}, {
      mason = false, -- run the project's node_modules/.bin/tsc
      init_options = { runExternalCode = true },
      -- CSS and Sass modules are attached so unsaved edits reach the server. Other stylesheets have no project and are skipped.
      filetypes = { "javascript", "javascriptreact", "typescript", "typescriptreact", "css", "scss", "sass" },
      cmd = tsc.cmd,
      root_dir = function(bufnr, on_dir)
        local ft, name = vim.bo[bufnr].filetype, vim.api.nvim_buf_get_name(bufnr)
        if not vim.list_contains({ "css", "scss", "sass" }, ft) or name:match("%.module%.s?[ac]ss$") then
          tsc.root_dir(bufnr, on_dir)
        end
      end,
    })
  end,
}

Tested with Neovim 0.12, LazyVim 16 and the lang.typescript extra. Neovim 0.11 doesn't handle workspace/diagnostic/refresh, so components update only when they are edited.

Example

examples/vite-react is a Vite + React app with vite/client types and localsConvention: "camelCaseOnly" in both vite.config.ts and tsconfig.json. pnpm --filter example-vite-react run parity loads every CSS and Sass module (the example's and the test fixtures') through Vite and checks that the names Vite exports at runtime match the types exactly. examples/next-parity does the same with Next.js, building with webpack and with Turbopack.

What's exported

| CSS | exported name | |---|---| | .button { }, .a:is(.b, .c), &.active (nesting) | button, a, b, c, active | | #main { } | main | | @keyframes spin { } | spin | | @value brand: #0a7; | brand | | .sm\:p-4 { } | sm:p-4 | | :global(.x), :global .x | (not exported) | | class names in comments, strings, url() | (not exported) | | .button-primary with localsConvention: "camelCase" | button-primary and buttonPrimary | | Sass: &-title nesting, @each $t in a, b { .is-#{$t} {} }, classes from @used partials and mixins | card-title, is-a, is-b, … | | Sass: %placeholder, $variables, empty rules | (not exported, as in the compiled CSS) |

composes: x from "./other.css" is supported. Other files are never read during type-checking, because composes doesn't change which names a module exports.

CSS and Sass syntax errors are reported at their line and column in the module. Errors in a @used file are reported at the top of the module, with the file and line in the message.

Compared with other tools

| | typescript-plugin-css-modules | typed-css-modules, happy-css-modules | css-modules-kit | typed-css-imports | |---|---|---|---|---| | How | tsserver plugin, editor only | Generates a .d.ts per module | tsserver plugin, plus codegen for tsc | Content mapper, inside tsc | | Caught in CI | No | Yes, if generation runs first | Yes, if generation runs first | Yes | | Extra files | None | One per module, committed or generated in CI | Generated declarations | None | | Fresh clone | Works | tsc fails until generation runs | tsc fails until generation runs | Works | | Editor updates | As you type | After save, with a watcher running | As you type | As you type | | Go to definition | Opt-in, experimental | happy-css-modules (declaration maps) | Yes | Yes | | Rename and references into the CSS | No | No | Yes | Yes, for names written as-is | | Sass | Yes | Yes | No | Yes | | TypeScript 7 | Not loaded (see below) | Works | Editor features are a tsserver plugin (see below); codegen works | Requires 7.1 |

css-modules-kit has more editor extras (a quick fix to add a missing class, className completion, updating imports when a module moves) and lint plugins. typed-css-imports needs no codegen step and supports Sass and localsConvention.

typescript-plugin-css-modules and css-modules-kit's editor features are tsserver plugins. The TypeScript team has said that existing tsserver plugins won't work with the TypeScript 7 codebase (microsoft/typescript-go#2154). In our test, the language server of TypeScript 7.1.0-dev.20261015.1 did not load a minimal tsserver plugin that TypeScript 5.9's tsserver loaded.

Limits

  • Next.js: with webpack, names match fully. Turbopack also omits names for empty rules (.x { }) and for @value, so those are typed but undefined at runtime there.
  • No Less or Stylus modules.
  • Editing a Sass partial doesn't update the modules that @use it in the editor until the module itself is edited or the language server restarts. tsc on the command line always compiles the current files.
  • additionalData only in its string form; Vite's function form can't be expressed in tsconfig.json.
  • Names reflect the CSS as written. PostCSS plugins your bundler runs before CSS Modules (for example ones that generate classes) are not applied.
  • Rename is only supported for names written as-is in the CSS. Renaming a quoted or converted name from a component (styles["button-primary"], buttonPrimary) renames the uses in components but not the CSS, so the next type-check reports them; renaming it from the CSS file is refused. Rename those in the CSS and fix the components the errors point to.
  • A class declared in several rules goes to its first declaration.
  • Default export only. Named imports (import { button } from "./x.module.css") work in Vite but not in every bundler, so they aren't typed.

Contributing

Development setup, tests and the release process: CONTRIBUTING.md. Security: SECURITY.md.

License

MIT