demitasse

Zero-runtime CSS-in-TS

Usage no npm install needed!

<script type="module">
  import demitasse from 'https://cdn.skypack.dev/demitasse';
</script>

README

β˜• demitasse Build status Latest Release License

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 as css.

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.
  • 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] = /*#__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 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 = /*#__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 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] = /*#__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

via custom properties

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.