Generate declaration files for CSS modules
The type definition from @types/css-modules
is easy for humans to understand, but not specific enough for programs:
declare module '*.css' {
const styles: Record<string, string>;
export default styles;
}
First, you will run into poor developer experience (DX) when noPropertyAccessFromIndexSignature
is enabled.
Ember: Glimmer component
Ember: <template>
tag
/* app/components/ui/page.gts */
import styles from './page.css';
<template>
// This should work, but results in an error.
<div class={{styles.container}}>
// ↳ Property 'container' comes from an index signature, so it must be accessed with ['container'].
</div>
// A workaround
<div class={{styles['container']}}>
</div>
</template>
Second, the loose definition may be incompatible with libraries that provide types (e.g. qunit-dom
). You will overuse the non-null assertion operator !
.
Ember: Rendering test
/* tests/integration/components/ui/page-test.ts */
import styles from 'app/components/ui/page.css';
// This should work, but results in an error.
assert
.dom('[data-test-container]')
.hasClass(styles.container);
// ↳ Argument of type 'string | undefined' is not assignable to parameter of type 'string | RegExp'.
// Type 'undefined' is not assignable to 'string | RegExp'.
// A workaround
assert
.dom('[data-test-container]')
.hasClass(styles['container']!);
When you provide accurate types, libraries (e.g. Glint
, embroider-css-modules
) improve your DX in return. You can catch typos and type issues early.
Ember: Glimmer component
Install type-css-modules
as a development dependency. Ensure that CSS declaration files exist before checking types; for example, you can write a pre-script.
/* package.json */
{
"scripts": {
"prelint:types": "type-css-modules <arguments>",
"lint:types": "tsc --noEmit" // or "glint"
},
"devDependencies": {
"type-css-modules": "...",
"typescript": "..."
}
}
You must pass --src
to indicate the location(s) of your CSS files.
# One source directory
type-css-modules --src app
# Multiple source directories
type-css-modules --src app/components app/controllers
Optional: Specify the project root
Pass --root
to run the codemod on a project somewhere else (i.e. not in the current directory).
type-css-modules --root <path/to/your/project>
type-css-modules
adds quotation marks in declaration files. This way, the names of CSS class selectors can always be used as object keys.
To separate formatting concerns, configure Prettier to handle *.css.d.ts
files differently.
/* .prettierrc.js */
module.exports = {
overrides: [
{
files: '*.css.d.ts',
options: {
quoteProps: 'preserve',
},
},
],
};
Yes! You may use *.module.css
to indicate the stylesheets that are for CSS modules. type-css-modules
will create declaration files with the extension *.module.css.d.ts
.
The Prettier configuration (shown above) can remain as is.
To reduce complexity, type-css-modules
expects you to follow the conventions of embroider-css-modules
:
- Give the local scope to the styles that you own1
- Avoid nesting styles2
- Use the default import to import styles
Here are some examples that meet the syntax requirements.
Ember: Glimmer component
/* app/components/ui/page.css */
.container {
display: grid;
grid-template-areas:
"header"
"body";
grid-template-columns: 1fr;
grid-template-rows: auto 1fr;
height: calc(100% - 3em);
overflow-y: auto;
padding: 1.5rem 1rem;
scrollbar-gutter: stable;
}
.header {
grid-area: header;
}
.body {
grid-area: body;
}
/* app/components/ui/page.ts */
import Component from '@glimmer/component';
import styles from './page.css';
export default class UiPageComponent extends Component {
styles = styles;
}
Ember: <template>
tag
/* app/components/ui/page.gts */
import { local } from 'embroider-css-modules';
import styles from './page.css';
<template>
<div class={{local styles "container"}}>
<h1 class={{styles.header}}>
{{@title}}
</h1>
<div class="{{styles.body}}">
{{yield}}
</div>
</div>
</template>
And some counterexamples (what not to do):
Don't use the :local()
pseudo-class selector
/* app/components/ui/page.css */
:local(.container) {
display: grid;
grid-template-areas:
"header"
"body";
grid-template-columns: 1fr;
grid-template-rows: auto 1fr;
height: calc(100% - 3em);
overflow-y: auto;
padding: 1.5rem 1rem;
scrollbar-gutter: stable;
}
:local(.header) {
grid-area: header;
}
:local(.body) {
grid-area: body;
}
Don't nest styles
/* app/components/ui/page.css */
.container {
display: grid;
grid-template-areas:
"header"
"body";
grid-template-columns: 1fr;
grid-template-rows: auto 1fr;
height: calc(100% - 3em);
overflow-y: auto;
padding: 1.5rem 1rem;
scrollbar-gutter: stable;
.header {
grid-area: header;
}
.body {
grid-area: body;
}
}
Don't use named imports to import styles
/* app/components/ui/page.gts */
import { container, header, body } from './page.css';
<template>
<div class={{container}}>
<h1 class={{header}}>
{{@title}}
</h1>
<div class="{{body}}">
{{yield}}
</div>
</div>
</template>
1. With webpack
, for example, you can configure mode
to be a function that returns 'local'
or 'global'
. In stylesheets, you can use the :global()
pseudo-class selector to refer to "things from outside."
2. CSS nesting is in spec. To reduce maintenance cost, type-css-modules
will leave it up to css-tree
to parse nested styles (see issue #210).
- Node.js v18 or above
See the Contributing guide for details.
This project is licensed under the MIT License.