Skip to content
HomePagesHomePages template kit

no-css-import-from-render-path

"No relative/absolute .css import from a section's render path — a package's is the sanctioned channel."

Venue: eslint — No relative/absolute .css import from a section’s render path — a package’s is the sanctioned channel.

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 { defineSchema, Section, text } from "@homepages/template-kit";
import type { SectionProps } from "@homepages/template-kit";
const schema = defineSchema({
label: "Gallery",
anchor: "gallery",
slots: { headline: text.medium({ label: "Headline" }) },
});
type Props = SectionProps<typeof schema>;
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.
  • The CSS pipeline — the two CSS entries and how they load.