demitasse
v2.2.0
Published
Zero-runtime CSS-in-TS
Downloads
1,763
Maintainers
Readme
☕ demitasse
Zero-runtime CSS-in-TypeScript
Demitasse offers the developer experience of CSS-in-TypeScript (CSS-in-JS) without the typical runtime cost or configuration burden of other approaches.
💅 Author style rules in TypeScript with type-checking via csstype.
👬 Colocate styles and markup in the same TypeScript module…or don't.
⚒️ Extract static CSS at build time.
📦 Locally-scoped class names
🔎 Transparent and uncomplicated build configuration
Installation
npm install demitasse
How to use
Step 1: Imports
import { cssRules, cssExport } from "demitasse";
import { ComponentBase, css as baseCSS } from "./component-base"; // optional
- The
cssRules
function is used to define CSS rules. It outputs a record of CSS class names (or just a single class name) along with the CSS model (a data structure from which a style sheet will be generated). - The
cssExport
function is used to export the aforementioned CSS models. - The
css as baseCSS
import will be used to re-export the CSS model exported from another module. This is required when the current module has some CSS dependency, e.g. when leveraging a base component.
ℹ️ In the example above, we imported a variable called
css
from the upstream module. This follows a suggested convention of exporting CSS models ascss
.
Step 2: Create a CSS module ID and options
const
cssModuleId = "fancy-button",
cssOptions = { debug: !!process.env.DEBUG_CSS }; // optional
- The
cssModuleId
serves dual purposes:- When generating style sheets, the name of the style sheet is the module ID,
e.g.
fancy-button.css
. - When the
debug
option is enabled, generated class names will include the module ID to allow CSS rules to be identified more easily.
- When generating style sheets, the name of the style sheet is the module ID,
e.g.
- The
options
object supports a singledebug
option. This option expands the generated class names, which usually look something likea4eds5a
, into more recognizable names likefancy-button-a4eds5a-container
.
Step 3: Create style rules
const [_css, styles] = /*#__PURE__*/ cssRules(cssModuleId, {
appearance: "none",
font: "inherit",
border: 0,
padding: "4px 8px 4px 8px",
background: "#06f",
color: "#fff",
"&:hover": {
animationKeyframes: {
"0%, 100%": {
transform: "none",
},
"50%": {
transform: "scale(1.1)",
}
},
animationDuration: 1000,
animationIterationCount: "infinite"
}
}, cssOptions);
- The
_css
variable references the CSS model that will be exported later in order to generate the style sheet. - The
styles
variable references the generated class name.
ℹ️ This example shows a single rule, which is why
styles
references a single generated class name string. It is also possible to specify a record of rules, in which casestyles
would return a record of generated class names.
Step 4: Use generated class names
This library is framework-agnostic; but suppose you are building a simple React
component FancyButton
on the basis of some other component ContainerBase
.
Here is how you would use the styles
object from the previous step:
export const FancyButton: FC<...> = ({ children, ...props }) => (
<ContainerBase as="button" className={styles} {...props}>
{children}
</ContainerBase>
);
Step 5: Export CSS models
export const css = /*#__PURE__*/ cssExport(cssModuleId, [
...baseCSS,
..._css,
]);
- The
cssExport
function prepares the CSS models to allow the corresponding style sheet outputs to be produced. - The
cssModuleId
is provided again to distinguish re-exports. Re-exported CSS is included in a_common
style sheet to prevent duplication across dependent components' style sheets. - The CSS models are spread into a single array. For simpler use cases without
CSS dependencies, this is unnecessary: You can simplify this to something like
cssExport(cssModuleId, fancyButtonCSS)
.
ℹ️ This example follows a suggested convention of naming CSS exports as
css
.
Step 6: Create style sheet module
e.g. src/styles.ts
:
import { css as fancyButton } from "./fancy-button";
import { css as textBox } from "./text-box";
// ...
import { sheets } from "demitasse";
export default sheets([
...fancyButton,
...textBox,
]);
- CSS models are imported from each component module.
- The
sheets
function is used to produce static CSS style sheet outputs. - The style sheets are exported as a record, with each key corresponding to a module name, and values as generated CSS code.
ℹ️ It is unnecessary to include any modules that client code wouldn't depend on directly. For example, you shouldn't include the CSS for a
ContainerBase
component intended only for internal use because it will automatically be included in the dependent module's CSS output and/or_common.css
, and it doesn't warrant its owncontainer-base.css
file.
Step 7: Generate style sheet outputs
The module shown in Step 6 now exports a record object in the following format:
{
"_common": "/* CSS shared across multiple modules/components */",
"fancy-button": "/* CSS from the fancy-button module/component */",
"text-box": "/* CSS from the text-box module/component */"
}
The remaining task is to extend the existing build process for your app or component library to include writing the CSS code in this object to CSS files and/or adding it to the JavaScript bundle. Strictly speaking, this is beyond the scope of this library, but some examples are provided to help you get started.
CSS Features
✅ Single rule
const [_css, styles] = /*#__PURE__*/ cssRules(cssModuleId, {
color: "black"
});
✅ Multi rule
const [_css, styles] = /*#__PURE__*/ cssRules(cssModuleId, {
container: {
appearance: "none",
padding: 0
},
content: {
padding: 4
}
});
✅ Nested selectors
const [_css, styles] = /*#__PURE__*/ cssRules(cssModuleId, {
color: "black",
"&:hover": {
color: "red"
}
});
✅ Animation keyframes
const [_css, styles] = /*#__PURE__*/ cssRules(cssModuleId, {
animationKeyframes: {
"0%, 100%": {
opacity: 0
},
"50%": {
opacity: 1
}
},
animationDuration: 1000,
animationIterationCount: "infinite"
});
✅ At-rules
const [_css, styles] = /*#__PURE__*/ cssRules(cssModuleId, {
"@supports (display: grid)": {
display: "grid"
}
});
✅ Implicit units
const [_css, styles] = /*#__PURE__*/ cssRules(cssModuleId, {
transitionDuration: 1000, // 1000ms
width: 100, // 100px
});
👍 Theming support
const [_css, styles] = /*#__PURE__*/ cssRules(cssModuleId, {
color: "var(--primary-color)",
});
🤷 Dynamic CSS
For dynamic CSS, probably just use inline styles in addition to style sheets and class names. Inline styles are usually criticized because:
- performance concerns. But this is not likely a significant factor for these one-off edge cases.
- specificity (priority). But for dynamic CSS values determined at runtime, high specificity is almost certainly what you want, i.e. feature not bug.
- maintainability. But if you believe that CSS and markup shouldn't be colocated, then CSS-in-JS is probably not the architecture you are looking for. Go Get BEM or something. 😉
API
Formal API documentation is available here.
Examples
A few examples are provided here.