Skip to content

Latest commit

 

History

History
1175 lines (936 loc) · 25.1 KB

README.md

File metadata and controls

1175 lines (936 loc) · 25.1 KB

Svelte preprocess CSS Modules

Generate CSS Modules classnames on Svelte components

npm install --save-dev svelte-preprocess-cssmodules

Table of Content

Usage

Add the module attribute to <style>

<style module>
  .red { color: red; }
</style>

<p class="red">My red text</p>

The component will be compiled to

<style>
  .red-30_1IC { color: red; }
</style>

<p class="red-30_1IC">My red text</p>

Approach

The default svelte scoping appends every css selectors with a unique class to only affect the elements of the component.

CSS Modules scopes each class name with a unique id/name in order to affect the elements of the component. As the other selectors are not scoped, it is recommended to write each selector with a class.

<!-- Component A -->
<p>lorem ipsum tut moue</p>
<p class="red">lorem ipsum tut moue</p>

<style module>
  p { font-size: 14px; }
  .red { color: red; }
</style>
<!-- Component B -->
<p class="text">lorem ipsum tut moue</p>
<p class="text red">lorem ipsum tut moue</p>

<style module>
  .text { font-size: 14px; }
  .red { color: red; }
</style>

transformed to

<!-- Component A -->
<p>lorem ipsum tut moue</p>
<p class="red-123qwe">lorem ipsum tut moue</p>

<style module>
  p { font-size: 14px; } /* global rule */
  .red-123qwe { color: red; }
</style>
<!-- Component B -->
<p class="text-456rty">lorem ipsum tut moue</p>
<p class="text-456rty red-123qwe">lorem ipsum tut moue</p>

<style> /* all scoped to component */
  .text-456rty { font-size: 14px; }
  .red-123qwe { color: red; }
</style>

Class directive

Toggle a class on an element.

<script>
  let route = 'home';
  $: isActive = route === 'home';
</script>

<style module>
  .active { font-weight: bold; }
</style>

<a class:active={isActive} href="/">Home</a>
<!-- or -->
<a class="{isActive ? 'active' : ''}" href="/">Home</a>

generating

<style>
  .active-2jIMhI { font-weight: bold; }
</style>

<a class="active-2jIMhI" href="/">Home</a>

Use of shorthand

<script>
  let route = 'home';
  $: active = route === 'home';
</script>

<style module>
  .active { font-weight: bold; }
</style>

<a class:active href="/">Home</a>

generating

<style>
  .active-2jIMhI { font-weight: bold; }
</style>

<a class="active-2jIMhI" href="/">Home</a>

Local selector

Force a selector to be scoped within its component to prevent style inheritance on child components.

:local() is doing the opposite of :global() and can only be used with the native and mixed modes (see preprocessor modes). The svelte scoping is applied to the selector inside :local().

<!-- Parent Component -->

<style module>
  .main em { color: grey; }
  .main :local(strong) { font-weight: 900; }
</style>

<div class="main">
  <p>My <em>main</em> lorem <strong>ipsum tuye</strong></p>
  <ChildComponent />
</div>
<!-- Child Component-->

<style module>
  /** Rule to override parent style **/
  .child em { color: black; }

  /** 
   * Unnecessary rule because of the use of :local()
   .child strong { font-weight: 700 }
   */
</style>

<p class="child">My <em>secondary</em> lorem <strong>ipsum tuye</strong></p>

generating

<!-- Parent Component-->

<style>
  .main-Yu78Wr em { color: grey; }
  .main-Yu78Wr strong.svelte-ery8ts { font-weight: 900; }
</style>

<div class="main-Yu78Wr">
  <p>My <em>main</em> lorem <strong class="svelte-ery8ts">ipsum tuye</strong></p>
  <ChildComponent />
</div>
<!-- Child Component-->

<style>
  .child-uhRt2j em { color: black; }
</style>

<p class="child-uhRt2j">My <em>secondary</em> lorem <strong>ipsum tuye</strong></p>

When used with a class, :local() cssModules is replaced by the svelte scoping system. This could be useful when targetting global classnames.

<style module>
  .actions {
    padding: 10px;
  }
  /* target a css framework classname without replacing it*/
  :local(.btn-primary) {
    margin-right: 10px;
  }
</style>

<div class="actions">
  <button class="btn btn-primary">Ok</button>
  <button class="btn btn-default">Cancel</button>
</div>

generating

<style>
  .actions-7Fhti9 {
    padding: 10px;
  }
  .btn-primary.svelte-saq8ts {
    margin-right: 10px;
  }
</style>

<div class="actions-7Fhti9">
  <button class="btn btn-primary svelte-saq8ts">Ok</button>
  <button class="btn btn-default">Cancel</button>
</div>

CSS binding

Link the value of a CSS property to a dynamic variable by using bind().

<script>
  let color = 'red';
</script>

<p class="text">My lorem ipsum text</p>

<style module>
  .text {
    font-size: 18px;
    font-weight: bold;
    color: bind(color);
  }
</style>

A scoped css variable, binding the declared statement, will be created on the component root elements which the css property will inherit from.

<script>
  let color = 'red';
</script>

<p class="text-t56rwy" style="--color-eh7sp:{color}">
  My lorem ipsum text
</p>

<style>
  .text-t56rwy {
    font-size: 18px;
    font-weight: bold;
    color: var(--color-eh7sp);
  }
</style>

An object property can also be targetted and must be wrapped with quotes.

<script>
  const style = {
    opacity: 0;
  };
</script>

<div class="content">
  <h1>Heading</h1>
  <p class="text">My lorem ipsum text</p>
</div>

<style module>
  .content {
    padding: 10px 20px;
    background-color: #fff;
  }
  .text {
    opacity: bind('style.opacity');
  }
</style>

generating

<script>
  const style = {
    opacity: 0;
  };
</script>

<div class="content-dhye8T" style="--opacity-r1gf51:{style.opacity}">
  <h1>Heading</h1>
  <p class="text-iTsx5A">My lorem ipsum text</p>
</div>

<style>
  .content-dhye8T {
    padding: 10px 20px;
    background-color: #fff;
  }
  .text-iTsx5A {
    opacity: var(--opacity-r1gf51);
  }
</style>

Scoped class on child components

CSS Modules allows you to pass a scoped classname to a child component giving the possibility to style it from its parent. (Only with the native and mixed modes – See preprocessor modes).

<!-- Child Component Button.svelte -->
<script>
  let className;
  export { className as class };
</script>

<button class="btn {className}">
  <slot />
</button>

<style module>
  .btn {
    background: red;
    color: white;
  }
</style>
<!-- Parent Component -->

<script>
  import Button from './Button.svelte';
</script>

<div class="wrapper">
  <h1>Welcome</h1>
  <p>Lorem ipsum tut ewou tu po</p>
  <Button class="btn">Start</Button>
</div>

<style module>
  .wrapper {
    margin: 0 auto;
    padding: 16px;
    max-width: 400px;
  }
  .btn {
    margin-top: 30px;
  }
</style>

generating

<div class="wrapper-tyaW3">
  <h1>Welcome</h1>
  <p>Lorem ipsum tut ewou tu po</p>
  <button class="btn-dtg87W btn-rtY6ad">Start</button>
</div>

<style>
  .wrapper-tyaW3 {
    margin: 0 auto;
    padding: 16px;
    max-width: 400px;
  }
  .btn-dtg87W {
    margin-top: 30px;
  }
  .btn-rtY6ad {
    background: red;
    color: white;
  }
</style>

Import styles from an external stylesheet

Alternatively, styles can be created into an external file and imported onto a svelte component. The name referring to the import can then be used on the markup to target any existing classname of the stylesheet.

  • The option parseExternalStylesheet need to be enabled.
  • The css file must follow the convention [FILENAME].module.css in order to be processed.

Note: That import is only meant for stylesheets relative to the component. You will have to set your own bundler in order to import node_modules css files.

/** style.module.css **/
.red { color: red; }
.blue { color: blue; }
<!-- Svelte component -->
<script>
  import style from './style.module.css';
</script>

<p class={style.red}>My red text</p>
<p class={style.blue}>My blue text</p>

generating

<style>
  .red-en-6pb { color: red; }
  .blue-oVk-n1 { color: blue; }
</style>

<p class="red-en-6pb">My red text</p>
<p class="blue-oVk-n1">My blue text</p>

Destructuring import

/** style.module.css **/
section { padding: 10px; }
.red { color: red; }
.blue { color: blue; }
.bold { font-weight: bold; }
<!-- Svelte component -->
<script>
  import { red, blue } from './style.module.css';
</script>

<section>
  <p class={red}>My <span class="bold">red</span> text</p>
  <p class="{blue} bold">My blue text</p>
</section>

generating

<style>
  section { padding: 10px; }
  .red-1sPexk { color: red; }
  .blue-oVkn13 { color: blue; }
  .bold-18te3n { font-weight: bold; }
</style>

<section>
  <p class="red-1sPexk">My <span class="bold-18te3n">red</span> text</p>
  <p class="blue-oVkn13 bold-18te3n">My blue text</p>
</section>

kebab-case situation

The kebab-case class names are being transformed to a camelCase version to facilitate their use on Markup and Javascript.

/** style.module.css **/
.success { color: green; }
.error-message {
  color: red;
  text-decoration: line-through;
}
<script>
  import css from './style.module.css';
</script>

<p class={css.success}>My success text</p>
<p class="{css.errorMessage}">My error message</p>

<!-- OR -->

<script>
  import { success, errorMessage } from './style.module.css';
</script>

<p class={success}>My success message</p>
<p class={errorMessage}>My error message</p>

generating

<style>
  .success-3BIYsG { color: green; }
  .error-message-16LSOn {
    color: red;
    text-decoration: line-through;
  }
</style>

<p class="success-3BIYsG">My success messge</p>
<p class="error-message-16LSOn">My error message</p>

Unnamed import

If a css file is being imported without a name, CSS Modules will still apply to the classes of the stylesheet.

/** style.module.css **/
p { font-size: 18px; }
.success { color: green; }
<script>
  import './style.module.css'
</script>

<p class="success">My success message</p>
<p>My another message</p>

generating

<style>
  p { font-size: 18px; }
  .success-vg78j0 { color: green; }
</style>

<p class="success-vg78j0">My success messge</p>
<p>My error message</p>

Directive and Dynamic class

Use the Svelte's builtin class: directive or javascript template to display a class dynamically.
Note: the shorthand directive is NOT working with imported CSS Module identifiers.

<script>
  import { success, error } from './style.module.css';

  let isSuccess = true;
  $: notice = isSuccess ? success : error;
</script>

<button on:click={() => isSuccess = !isSuccess}>Toggle</button>

<!-- Error -->
<p class:success>Success</p>

<!-- Ok -->
<p 
  class:success={isSuccess}
  class:error={!isSuccess}>Notice</p>

<p class={notice}>Notice</p>
<p class={isSuccess ? success : error}>Notice</p>

Preprocessor Modes

The mode can be set globally from the config or locally to override the global setting.

Native

Scopes classes with CSS Modules, anything else is unscoped.

Pros:

  • uses default CSS Modules approach
  • creates unique ID to avoid classname conflicts and unexpected inheritances
  • passes scoped class name to child components

Cons:

  • does not scope non class selectors.
  • forces to write selectors with classes.
  • needs to consider third party plugins with useAsDefaultScoping on – Read more.

Mixed

Scopes non-class selectors with svelte scoping in addition to native (same as preprocessor v1)

<style module="mixed">
  p { font-size: 14px; }
  .red { color: red; }
</style>

<p class="red">My red text</p>

generating

<style>
  p.svelte-teyu13r { font-size: 14px; }
  .red-30_1IC { color: red; }
</style>

<p class="red-30_1IC svelte-teyu13r">My red text</p>

Pros:

  • creates class names with unique ID to avoid conflicts and unexpected inheritances
  • uses svelte scoping on non class selectors
  • passes scoped class name to child components

Cons:

  • adds more weight to tag selectors than class selectors (because of the svelte scoping)
<ul>
 <li>Home</li>
 <li class="active">About</li>
</ul>

<style module="mixed">
 li {
   color: gray; 
 }
 /* this will never be applied */
 .active {
   color: blue;
 }
 /* forcing us to write that instead */
 li.active {
   color: blue;
 }
</style>

<!-- or rewriting the component -->

<ul>
 <li class="item">Home</li>
 <li class="item active">About</li>
</ul>

<style module="mixed">
 .item {
   color: gray; 
 }
 .active {
   color: blue;
 }
</style>

Scoped

Scopes classes with svelte scoping in addition to mixed.

<style module="scoped">
  p { font-size: 14px; }
  .red { color: red; }
</style>

<p class="red">My red text</p>

generating

<style>
  p.svelte-teyu13r { font-size: 14px; }
  .red-30_1IC.svelte-teyu13r { color: red; }
</style>

<p class="red-30_1IC svelte-teyu13r">My red text</p>

Pros:

  • creates class names with unique ID to avoid conflicts and unexpected inheritances
  • scopes every selectors at equal weight

Cons:

  • does not pass scoped classname to child components

Comparative

Svelte scoping Preprocessor Native Preprocessor Mixed Preprocessor Scoped
Scopes classes O O O O
Scopes non class selectors O X O O
Creates unique class ID X O O O
Has equal selector weight O O X O
Passes scoped classname to a child component X O O X

Why CSS Modules over Svelte scoping?

  • On a full svelte application: it is just a question of taste as the default svelte scoping is largely enough. Component styles will never inherit from other styling.

  • On a hybrid project (like using svelte to enhance a web page): the default scoping may actually inherits from a class of the same name belonging to the style of the page. In that case using CSS Modules to create a unique ID and to avoid class inheritance might be advantageous.

Configuration

Rollup

To be used with the plugin rollup-plugin-svelte.

import svelte from 'rollup-plugin-svelte';
import { cssModules } from 'svelte-preprocess-cssmodules';

export default {
  ...
  plugins: [
    svelte({
      preprocess: [
        cssModules(),
      ]
    }),
  ]
  ...
}

Webpack

To be used with the loader svelte-loader.

const { cssModules } = require('svelte-preprocess-cssmodules');

module.exports = {
  ...
  module: {
    rules: [
      {
        test: /\.svelte$/,
        exclude: /node_modules/,
        use: [
          {
            loader: 'svelte-loader',
            options: {
              preprocess: [
                cssModules(),
              ]
            }
          }
        ]
      }
    ]
  }
  ...
}

SvelteKit

As the module distribution is targetting esnext, Node.js 14 or above is required in order to work.

// svelte.config.js

import { cssModules } from 'svelte-preprocess-cssmodules';

const config = {
  ...
  preprocess: [
    cssModules(),
  ]
};

export default config;

Svelte Preprocess

Svelte is running the preprocessors by phases, going through all markup first, followed by script and then style.

The CSS Modules preprocessor is doing all its work on the markup phase via svelte.parse() which requires the compoment to be a valid standard svelte component (using vanilla js and vanilla css). if any other code (such as typescript or sass) is encountered, an error will be thrown.

import { typescript, scss } from 'svelte-preprocess';
import { cssModules } from 'svelte-preprocess-cssmodules';

...
// svelte config:  NOT working!
  preprocess: [
    typescript(), // 2 run second on script phase
    scss(), // 3 run last on style phase
    cssModules(), // 1 run first on markup phase
  ],
...

As it is extremely common for developers to use svelte-preprocess in their application, CSS Modules provides a small utility to easily be incorporated with. linearPreprocess will ensure a linear process with the list of preprocessors.

import { typescript, scss } from 'svelte-preprocess';
import { cssModules, linearPreprocess } from 'svelte-preprocess-cssmodules';

...
// svelte config: OK, processing one after another!
  preprocess: linearPreprocess([
    typescript(), // 1 run first
    scss(), // 2 run second
    cssModules(), // 3 run last
  ]),
...

Options

Pass an object of the following properties

Name Type Default Description
cssVariableHash {String} [hash:base64:6] The hash type (see locatonIdentName)
getLocalIdent Function undefined Generate the classname by specifying a function instead of using the built-in interpolation
hashSeeder {Array} ['style', 'filepath', 'classname'] An array of keys to base the hash on
includeAttributes {Array} [] An array of attributes to parse along with class
includePaths {Array} [] (Any) An array of paths to be processed
localIdentName {String} "[local]-[hash:base64:6]" A rule using any available token
mode native|mixed|scoped native The preprocess mode to use
parseExternalStylesheet {Boolean} false Enable parsing on imported external stylesheet
parseStyleTag {Boolean} true Enable parsing on style tag
useAsDefaultScoping {Boolean} false Replace svelte scoping globally

getLocalIdent

Customize the creation of the classname instead of relying on the built-in function.

function getLocalIdent(
  context: {
    context: string, // the context path
    resourcePath: string, // path + filename
  },
  localIdentName: {
    template: string, // the template rule
    interpolatedName: string, // the built-in generated classname
  },
  className: string, // the classname string
  content: { 
    markup: string, // the markup content
    style: string,  // the style content
  }
): string {
  return `your_generated_classname`;
}

Example of use

# Directory
SvelteApp
└─ src
   ├─ App.svelte
   └─ components
      └─ Button.svelte
<!-- Button.svelte -->
<button class="red">Ok</button>

<style>
  .red { background-color: red; }
</style>
// Preprocess config
...
preprocess: [
  cssModules({
    localIdentName: '[path][name]__[local]',
    getLocalIdent: (context, { interpolatedName }) => {
      return interpolatedName.toLowerCase().replace('src_', '');
      // svelteapp_components_button__red;
    }
  })
],
...

hashSeeder

Set the source of the hash (when using [hash] / [contenthash]).

The list of available keys are:

  • style the content of the style tag (or the imported stylesheet)
  • filepath the path of the component
  • classname the local classname

Example of use: creating a common hash per component

// Preprocess config
...
preprocess: [
  cssModules({
    hashSeeder: ['filepath', 'style'],
  })
],
...
<button class="success">Ok</button>
<button class="cancel">Cancel</button>
<style module>
  .success { background-color: green; }
  .cancel { background-color: gray; }
</style>

generating

<button class="success-yr6RT">Ok</button>
<button class="cancel-yr6RT">Cancel</button>
<style>
  .success-yr6RT { background-color: green; }
  .cancel-yr6RT { background-color: gray; }
</style>

includeAttributes

Add other attributes than class to be parsed by the preprocesser

// Preprocess config
...
preprocess: [
  cssModules({
    includeAttributes: ['data-color', 'classname'],
  })
],
...
<button class="red" data-color="red">Red</button>
<button class="red" classname="blue">Red or Blue</button>
<style module>
  .red { background-color: red; }
  .blue { background-color: blue; }
</style>

generating

<button class="red-yr6RT" data-color="red-yr6RT">Red</button>
<button class="red-yr6RT" classname="blue-aE4qW">Red or Blue</button>
<style>
  .red-yr6RT { background-color: red; }
  .blue-aE4qW { background-color: blue; }
</style>

localIdentName

Inspired by webpack interpolateName, here is the list of tokens:

  • [local] the targeted classname
  • [ext] the extension of the resource
  • [name] the basename of the resource
  • [path] the path of the resource
  • [folder] the folder the resource is in
  • [contenthash] or [hash] (they are the same) the hash of the resource content (by default it's the hex digest of the md5 hash)
  • [<hashType>:contenthash:<digestType>:<length>] optionally one can configure
    • other hashTypes, i. e. sha1, md5, sha256, sha512
    • other digestTypes, i. e. hex, base26, base32, base36, base49, base52, base58, base62, base64
    • and length the length in chars

useAsDefaultScoping

Globally replace the default svelte scoping by the CSS Modules scoping. As a result, the module attribute to <style> becomes unnecessary.

// Preprocess config
...
preprocess: [
  cssModules({
    useAsDefaultScoping: true
  }),
],
...
<h1 class="title">Welcome</h1>
<style>
  .title { color: blue }
</style>

generating

<h1 class="title-erYt1">Welcome</h1>
<style>
  .title-erYt1 { color: blue }
</style>

Potential issue with third party plugins

The preprocessor requires you to add the module attribute to <style> in order to apply CSS Modules to a component. When enabling useAsDefaultScoping the module attribute is not required anymore and CSS Modules will apply to all svelte components of your application, including plugins. If a third party component is relying on svelte scoping and non class selectors, its styling will apply globally and may cause unexpected results.

To prevent any scoping conflict, it is recommended to set the includePaths option to your source folder in order to target your components only.

// Preprocess config
...
preprocess: [
  cssModules({
    useAsDefaultScoping: true,
    includePaths: ['./src'],
  }),
],
...

Migrating from v1

If you want to migrate an existing project to v2 keeping the approach of the 1st version, follow the steps below:

  • Set the mixed mode from the global settings.
    // Preprocess config
    ...
    preprocess: [
     cssModules({
       mode: 'mixed',
     }),
    ],
    ...
  • Remove all $style. prefix from the html markup
  • Add the attribute module to <style> within your components.
    <style module>
    ...
    </style>

Code Example

Rollup Config

export default {
  ...
  plugins: [
    svelte({
      preprocess: [
        cssModules({
          includePaths: ['src'],
          localIdentName: '[hash:base64:10]',
        }),
      ]
    }),
  ]
  ...
}

Svelte Component

<style module>
  .modal {
    position: fixed;
  }
  .wrapper {
    padding: 0.5rem 1rem;
  }
  .body {
    flex: 1 0 0;
  }
  .modal button {
    background-color: white;
  }
  .cancel {
    background-color: #f2f2f2;
  }
</style>

<section class="modal">
  <header class="wrapper">My Modal Title</header>
  <div class="body wrapper">
    <p>Lorem ipsum dolor sit, amet consectetur.</p>
  </div>
  <footer class="wrapper">
    <button>Ok</button>
    <button class="cancel">Cancel</button>
  </footer>
</section>

Final html code generated by svelte

<style>
  ._329TyLUs9c {
    position: fixed;
  }
  .Re123xDTGv {
    padding: 0.5rem 1rem;
  }
  ._1HPUBXtzNG {
    flex: 1 0 0;
  }
  ._329TyLUs9c button {
    background-color: white;
  }
  ._1xhJxRwWs7 {
    background-color: #f2f2f2;
  }
</style>

<section class="_329TyLUs9c">
  <header class="Re123xDTGv">My Modal Title</header>
  <div class="_1HPUBXtzNG Re123xDTGv">
    <p>Lorem ipsum dolor sit, amet consectetur.</p>
  </div>
  <footer class="Re123xDTGv">
    <button>Ok</button>
    <button class="_1xhJxRwWs7">Cancel</button>
  </footer>
</section>

License

MIT