☕ 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";
- 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 as css.
Step 2: Create a CSS module ID and options
const
cssModuleId = "fancy-button",
cssOptions = { debug: !!process.env.DEBUG_CSS };
- 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.
- The
options object supports a single debug option. This option expands the
generated class names, which usually look something like a4eds5a, into more
recognizable names like fancy-button-a4eds5a-container.
Step 3: Create style rules
const [_css, styles] = 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 case styles 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 = 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 own container-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] = cssRules(cssModuleId, {
color: "black"
});
✅ Multi rule
const [_css, styles] = cssRules(cssModuleId, {
container: {
appearance: "none",
padding: 0
},
content: {
padding: 4
}
});
✅ Nested selectors
const [_css, styles] = cssRules(cssModuleId, {
color: "black",
"&:hover": {
color: "red"
}
});
✅ Animation keyframes
const [_css, styles] = cssRules(cssModuleId, {
animationKeyframes: {
"0%, 100%": {
opacity: 0
},
"50%": {
opacity: 1
}
},
animationDuration: 1000,
animationIterationCount: "infinite"
});
✅ At-rules
const [_css, styles] = cssRules(cssModuleId, {
"@supports (display: grid)": {
display: "grid"
}
});
✅ Implicit units
const [_css, styles] = cssRules(cssModuleId, {
transitionDuration: 1000,
width: 100,
});
👍 Theming support
via custom properties
const [_css, styles] = 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.