# postcss-import inlines @import rules at build time, and it expects to run first

> postcss-import is an MIT licensed PostCSS plugin that replaces @import rules with the contents of the imported file, resolving local paths, node_modules packages and web_modules alike, and it deduplicates by resolved path so a file is inlined once per scope. It also carries a strictness the browser does not have, since it attempts to follow the CSS @import spec and will not inline an import that appears after other rules.

**postcss/postcss-import** — PostCSS plugin to inline at-import rules content

- Repository: https://github.com/postcss/postcss-import
- Stars: 1,409 · Forks: 119
- Language: JavaScript
- License: MIT
- Published: 2026-10-16 · Updated: 2026-10-16 · Language: en
- Canonical page: https://hysenlabs.com/projects/postcss-postcss-import

## The plugin replaces imports with content, so it wants to run first

The description is the whole mechanism. It is a PostCSS plugin to transform @import rules by inlining content, and it can consume local files, node modules or web_modules.

The usage example in the README shows the shape of the integration.

```js
const fs = require("fs")
const postcss = require("postcss")
const atImport = require("postcss-import")

const css = fs.readFileSync("css/input.css", "utf8")

postcss()
  .use(atImport())
  .process(css, {
    // `from` option is needed here
    from: "css/input.css"
  })
  .then((result) => {
    const output = result.css

    console.log(output)
  })
```

The note at the top of the README says the plugin should probably be used as the first plugin of your list, so that other plugins will work on the AST as if there were only a single file to process, and will probably work as you can expect. That ordering advice is the single most useful sentence in the file, because every plugin after it inherits a flattened stylesheet.

The related advice follows from the same flattening. Running postcss-url after postcss-import in your plugin chain will allow you to adjust assets url(), or even inline them, after inlining imported files. Which is the correct order, since the URLs only make sense once the imported content is in place.

The from option is the other thing to get right. Unless your stylesheet is in the same place where you run postcss, process.cwd(), you will need to use the from option to make relative imports work, and the example sets it to css/input.css to match the file it reads.

## Resolution order decides what a bare import name means

The resolution rules are what make the plugin useful in a project with dependencies, and the README describes them in a sentence each.

To resolve the path of an @import rule, it can look into the root directory, by default process.cwd(), then web_modules, then node_modules, or local modules. And when importing a module, it will look for index.css or the file referenced in package.json in the style or main fields.

So an import of a bare package name in the example input resolves into node_modules, and the two forms the README shows are the same thing written differently.

```css
@import "cssrecipes-defaults";
@import "normalize.css";
```

The style and main fields matter because package layouts differ. A package whose main points at a JavaScript file is not usable, so the plugin follows the convention that a CSS entry is either index.css or whatever the manifest names, which is why the second example resolves to the package's own CSS file rather than to a directory.

The root option is the override for all of this, and the README is candid that it should not be used that much. It defaults to process.cwd() or the dirname of the postcss from option, and it is where you set the place where node_modules are. The note that nested @import will additionally benefit of the relative dirname of imported files is the important part, because it means an import inside an imported file resolves relative to that file rather than to the root, which is what the CSS spec asks for and what a naive implementation gets wrong.

The path option is a simpler escape hatch, a string or an array of extra directories to look in, and addModulesDirectories adds folder names to the default resolver directories of node_modules and web_modules.

## Duplicate skipping is per scope, and the option that changes it

In order to optimize output, the plugin will only import a file once on a given scope, where the scope is a root, a media query, and so on. Duplicates are detected by the resolved file path, and if that behavior is not what you want, the README points at the skipDuplicates option.

```js
const postcss = require("postcss")
const atImport = require("postcss-import")

postcss()
  .use(atImport({
    path: ["src/css"],
  }))
  .process(cssString)
  .then((result) => {
    const { css } = result
  })
```

The option defaults to true, so repeated imports of the same file are skipped, and setting it to false makes the plugin inline a file each time it is imported.

That default is the right one for most projects, and the reason to turn it off is narrow but real. Because the scope is what defines the boundary, the same file imported once at the root and once inside a media query is inlined twice, and that is not a duplicate in the deduplication sense, it is two different scopes that happen to reference the same file.

The detection is by resolved path rather than by the text of the import, so two different spellings of the same file, a relative path and a package name that resolve to the same place, count as one. That is the behaviour that makes the plugin safe to run over a stylesheet that imports a shared partial from several places.

The other two behaviours in the notes are worth pairing with this. Imports which are not modified, whether because the filter option excluded them or because they are remote imports, are moved to the top of the output, which is what the CSS spec requires, and the plugin warns when an empty file is imported unless warnOnEmpty is set to false.

## Two hook options let you replace resolution and loading

The options list is long, but only two of them are extension points in the sense that matters, and both are functions.

The resolve option takes a custom path resolver, defaulting to null. The function gets id, basedir, importOptions and astNode arguments, and should return a path, an array of paths, or a promise resolving to the path or paths. If you do not return an absolute path, your path will be resolved to an absolute path using the default resolver. The README suggests using the resolve package for this, and that is also what the plugin depends on internally, alongside postcss-value-parser for parsing the import value and read-cache for avoiding reading the same file twice.

The load option is the other one. It overwrites the default loading way, and the function gets filename and importOptions and returns content or promised content. That is the hook to use when your imports live somewhere a file reader cannot reach, or when you want to transform the content on the way in.

Two of the remaining options are worth calling out. The filter option takes a function defaulting to () => true, and only transform imports for which the test function returns true, with imports that return false left as they are. And the plugins option is an array of plugins to be applied on each imported file, which is how you can, for example, run a nested transformation over the content of every imported stylesheet.

The addModulesDirectories option completes the resolver story. It takes an array of folder names to add to Node's resolver, with values appended to the default resolve directories of node_modules and web_modules, and the README notes that it is ignored if you supply your own resolver through the resolve option.

If you are going to change one thing, change resolve. Everything else is a convenience around the default node resolution.

## The plugin enforces the CSS @import ordering rule

There is a note in the README that reads like a footnote and is really a behavioural contract. The plugin attempts to follow the CSS @import spec, and @import statements must precede all other statements, besides @charset and @layer.

That is the browser's rule, and it is the reason some stylesheets that look fine in a dev environment fail when they are processed. A stylesheet with a @import after a rule is invalid CSS, and a browser will ignore the import. Inlining it silently changes what the page loads, because the imported rules end up after the rules that were already there and, depending on the properties involved, the cascade result can differ from what the author intended.

The example input in the README makes the point by construction, since every import is at the top of the file and the body rule is last. The output shows the same ordering, with the imported content placed where each import was.

Two syntax details are covered as well. All standard notations of the url value are supported, so a bare url form and a quoted url form both work, and the media and layer syntaxes are handled, with an import carrying a media query becoming an @media block in the output and one carrying a layer becoming an @layer block.

So a stylesheet that already uses @layer keeps its layers through the import, and a media-scoped import keeps its condition. That is more faithful than a plugin that simply concatenates, and it is the reason the plugin warns about the ordering rather than reordering around it.

## Dependency messages exist for file watchers, not for humans

One feature is easy to overlook because it produces no output in the stylesheet. The plugin adds a message to result.messages for each @import, in a fixed format.

The message has a type of dependency, a file field holding the absolute path of the imported file, and a parent field naming the file that contained the import.

The README says this is mainly for use by postcss runners that implement file watching. So the plugin is not only rewriting a stylesheet, it is also reporting the dependency graph it discovered while doing it, and the consumer of that report is a watcher that wants to rebuild when an imported partial changes.

That is a small design decision with a large reach. Without it, a watcher sees only the entry stylesheet and has no way to know that a file three directories away was pulled in, so editing that partial would not trigger a rebuild and the change would silently not appear. With the messages, the runner knows the edges.

The parent field is what makes the graph navigable, because it names the file that contained the import, and combined with the absolute file path of the imported file it is enough to walk back from any dependency to the entry point.

For an adopter, the practical note is that the messages are additive and harmless if you do not use them, and that if you do build a watcher, this is the hook the plugin gives you rather than having to parse the output CSS.

## MIT licensed, Node 22, and a version the release list does not show

The licence is MIT, and the package.json records it as the license field with the LICENSE file at the repository root. That is a licensing fact rather than legal advice, and for a build-time plugin that is about as simple as it gets.

The runtime requirements have moved. The package.json declares engines node >=22.0.0 and a peer dependency on postcss ^8.0.0, and the three runtime dependencies are postcss-value-parser at 4 or above, read-cache at 1 or above, and resolve at 1.1.7 or above. The postcss peer dependency is the one to note, since a project on postcss 7 will not work with this version.

The test setup is modern as well. The tests run on ava with c8 for coverage, the lint script runs eslint with the fix flag, and the pretest hook runs the lint before the tests, so a failing lint fails the test run. The devDependencies include eslint 10 with the problems config and the prettier plugin, prettier 3.9, and postcss-scss and sugarss, which are the two syntaxes the test suite exercises the plugin against.

The one thing that will confuse a reader is the version. The release history attached to the project lists 14.0.0 from December 2020, 13.0.0 from October 2020 and 12.0.0 from 2018, while the package.json in the same tree says version 17.0.1. Those dated entries stop well short of the current package version, so the package.json is the number to trust when you pin the dependency, and the CHANGELOG.md at the root is the file to read for what changed between versions.

On maintenance, the repository is not archived, the default branch is master, and the last push was on 2026-10-13. The root also carries a renovate.json file and a .github directory, which is how this project handles dependency updates, but neither tells you when a breaking change will land.

## Conclusion

postcss-import fits a team that wants one stylesheet at runtime, that imports from node_modules, and that is willing to make the plugin the first step in its PostCSS chain so every later plugin sees a single file. It does not fit a project that wants to keep @import in the output, or one that relies on a bundler's own CSS handling instead. Before adopting it, check where your @import rules sit in the file, because the plugin enforces the rule that imports must precede other statements, and set skipDuplicates to false if you rely on the same file being inlined more than once in one scope.

## FAQ

### How do I install and use postcss-import?

Install it with npm install -D postcss-import, then add it to your PostCSS chain with .use(atImport()) and pass the from option if your stylesheet is not in the directory where you run PostCSS, because relative imports are resolved against it.

### How does postcss-import resolve an import from node_modules?

It looks in the root directory, by default process.cwd(), then web_modules, then node_modules, or local modules. When importing a module it looks for index.css or the file referenced in package.json in the style or main fields, and nested imports additionally resolve relative to the dirname of the imported file.

### What does the skipDuplicates option do in postcss-import?

By default the plugin only imports a file once on a given scope, with duplicates detected by the resolved file path, so the same file at the root and inside a media query is inlined twice as two different scopes. Setting skipDuplicates to false makes it inline a file each time it is imported.

### Can I use my own path resolver with postcss-import?

Yes, through the resolve option, which takes a function receiving id, basedir, importOptions and astNode and returning a path, an array of paths or a promise. If you do not return an absolute path it is resolved with the default resolver, and the addModulesDirectories option is ignored when a custom resolver is supplied.

### Does postcss-import support glob imports and remote URLs?

Not on its own. The README points to postcss-import-ext-glob for glob imports, and to postcss-import-url with its dataUrls plugin option for remote sources, both of which extend postcss-import.

## Sources

- [Issues](https://github.com/postcss/postcss-import/issues)
- [License: MIT](https://github.com/postcss/postcss-import/blob/master/LICENSE)
- [postcss/postcss-import on GitHub](https://github.com/postcss/postcss-import)
- [README](https://github.com/postcss/postcss-import/blob/master/README.md)
- [Releases](https://github.com/postcss/postcss-import/releases)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/postcss-postcss-import
