Skip to content
HomePagesHomePages template kit
Menu

template-kit/no-css-import-from-render-path

The template-kit/no-css-import-from-render-path authoring rule — what it enforces, why, and how to fix it.

No local .css import in a section’s render path. A package’s .css import belongs there — it is how its stylesheet reaches the section’s cascade layer.

An import whose specifier ends in .css, in any of the section’s .ts/.tsx files, is reported only when the specifier is local — relative (import "./gallery.css";) or absolute (import "/gallery.css";) — including the side-effect-only form with no bindings.

A bare specifier (import "swiper/css";, import "@scope/pkg/dist/style.css";) is not reported. That is the sanctioned channel for a third-party package’s stylesheet.

What it does not match:

  • .scss and .less — a different pipeline, out of this rule’s scope;
  • require("./x.css");
  • a dynamic import("./x.css").

There is no island exemption: a "use client" file is part of the render path too — it is server-rendered inline before it is hydrated.

The section’s CSS build works by bundling the render path itselfRenderer.tsx and every module it imports, components and islands alike — and harvesting the CSS those imports pull in. A bare specifier resolves into node_modules, which is exactly what that build looks for: import "swiper/css"; in Renderer.tsx is not a mistake, it is the mechanism.

A local stylesheet is a different problem. Every .css file that already lives in the section’s folder is collected on its own, by filename — the build doesn’t need an import to find it. Importing it again from the render path doesn’t just ship duplicate rules harmlessly: it can also smuggle a stray @layer order statement into a nested layer and corrupt the cascade for the whole page.

Delete the import. A local stylesheet already ships — the build picks up every .css file in the section folder on its own; importing it again only makes it double-emit.

A package’s stylesheet is different: import it by its bare specifier, from the module that needs it. The build bundles Renderer.tsx’s import graph transitively, so a bare specifier is equally legal in the Renderer itself or in any component or island it imports — not only at the top of the tree. That import is how the build finds the stylesheet and routes it into the section’s cascade layer.

sections/gallery/Renderer.tsx
import { Section } from "@homepages/template-kit";
import "./styles.css"; // template-kit/no-css-import-from-render-path — already collected by filename
import type { Props } from "./schema.js";
export function Renderer({ slots }: Props) {
return (
<Section>
<div className="tr-gallery-track">{slots.headline}</div>
</Section>
);
}

The local import is simply deleted — styles.css was already shipping:

sections/gallery/Renderer.tsx
import { Section } from "@homepages/template-kit";
import type { Props } from "./schema.js";
export function Renderer({ slots }: Props) {
return (
<Section>
<div className="tr-gallery-track">{slots.headline}</div>
</Section>
);
}

Compare a package’s stylesheet, which is the legal counterpart — imported by its bare specifier, from the same file, and left in place:

sections/gallery/Renderer.tsx
import { Section } from "@homepages/template-kit";
import "swiper/css"; // legal — a bare specifier is how package CSS reaches this section's layer
import type { Props } from "./schema.js";
export function Renderer({ slots }: Props) {
return (
<Section>
<div className="tr-gallery-track">{slots.headline}</div>
</Section>
);
}
  • no-bare-css-import — the other half of this mental model. Render path: no local CSS imports. Hand-written CSS: no @imports at all, package or otherwise. An author who reads only one of the two pages will be confused by the other.
  • Theme and CSS — the two CSS entries and how they load.