# isaaccss: Inline-Style-as-a-Class CSS engine

An atomic CSS DSL like inline style.

<!-- prettier-ignore -->
```jsx
import { is } from "isaaccss";

const SubmitButton = ({ variant }: { variant: 'primary' | 'secondary' }) => (
  <button class={is`
    background:hsl(var(--H),var(--S),var(--L)) ${variant === "secondary" ? is`--H:30` : is`--H:210`}
    --S:100% --L:50% :hover/--L:60% :active/--L:40%*
    border:3px_solid_hsl(var(--H),var(--S),80%) border-radius:8px color:white
    padding:4px_8px @width>=768px/padding:8px_16px @hover:hover/:hover/scale:1.1
  `}>
    Submit
  </button>
);
```

Or using some short aliases:

<!-- prettier-ignore -->
```jsx
import { is } from "isaaccss";

const SubmitButton = ({ variant }: { variant: 'primary' | 'secondary' }) => (
  <button class={is`
    bg:hsl($H,$S,$L) ${variant === "secondary" ? is`--H:30` : is`--H:210`}
    --S:100% --L:50% :hover/--L:60% :active/--L:40%* 
    b:3px_solid_hsl($H,$S,80%) b-radius:8px c:white
    p:4px_8px @w>=768px/p:8px_16px @hover:hover/:hover/scale:1.1
  `}>
    Submit
  </button>
);
```

[Playground](https://luncheon.github.io/isaaccss/playground/)

## Installation

```
npm i -D isaaccss
```

## Usage

Import `is` from `"isaaccss"` and write styles in `is`-tagged template.

```js
import { is } from "isaaccss";

document.body.className = is`m:0 @screen&w>=640px/m:1rem`;
```

The above code with default config generates the following JS and CSS:

```js
document.body.className = `#a #b`;
```

```css
.\#a:not(#\ ) {
  margin: 0;
}

@media screen and (width>=640px) {
  .\#b:not(#\ ) {
    margin: 1rem;
  }
}
```

- `#` of `#a`, `#b`: prefix to avoid conflicts with other libraries and your CSS. Customizable.
- `:not(#\ )`: selector to increase ID-specificity. Ensures greater specificity than other libraries or your CSS.

## Syntax

```
[@media/][@^container/][selectors/]property:value[!][;property:value[!]...][?][*[*...]]
 ~~~~~~~  ~~~~~~~~~~~~  ~~~~~~~~~~ ~~~~~~~~ ~~~~~ ~  ~~~~~~~~~~~~~~~~~~~~~  ~  ~~~~~~~
   (1)        (2)           (3)      (4)     (5) (6)          (7)          (8)   (9)
```

1. Optional `@media/` indicates [media queries](https://developer.mozilla.org/docs/Web/CSS/Media_Queries/Using_media_queries)
   - `@foo/d:none` generates `@media foo { .\#a { display: none } }`  
     (`.\@foo\/d\:none` is compressed into `.\#a`)
   - Tokens are parenthesized where necessary  
     e.g. `@screen&w>=640px/m:0` -> `@media screen and (width >= 640px) { .\#a { margin: 0 } }`
2. Optional `@^container/` indicates [container queries](https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Container_Queries)
   - `@^w>=200/m:1rem` generates `@container (w>=200) { .\#a { margin:1rem } }`
   - `@^card(w>=200)/m-l:1rem` generates `@container card (w>=200) { .\#a { margin-left: 1rem } }`
3. Optional `selectors/` indicates additional selectors
   - [Pseudo-classes](https://developer.mozilla.org/docs/Web/CSS/Pseudo-classes)  
     e.g. `:hover/`, `:has(>:checked)/`
   - [Pseudo-elements](https://developer.mozilla.org/docs/Web/CSS/Pseudo-elements)  
     e.g. `::before/`, `::part(foo)/`
   - [Child combinator](https://developer.mozilla.org/docs/Web/CSS/Child_combinator)  
     e.g. `>div/`
   - [Adjacent sibling combinator](https://developer.mozilla.org/docs/Web/CSS/Adjacent_sibling_combinator)  
     e.g. `+div/`
   - [General sibling combinator](https://developer.mozilla.org/docs/Web/CSS/General_sibling_combinator)  
     e.g. `~div/`
   - Combination of the above  
     e.g. `:hover>input+label::before/`
   - If there are ampersands `&`, they becomes that class  
     e.g. `&+&/m-l:1rem` -> `.\&\+\& + .\&\+\& { margin-left: 1rem }`
4. Required `property` indicates the property name
   - Must be one of the [known properties](https://github.com/known-css/known-css-properties/blob/master/data/all.json) or a [custom property](https://developer.mozilla.org/docs/Web/CSS/--*)
5. Required `value` indicates the property value
   - `$bar` will be replaced with `var(--bar)`
     - Custom property set libraries, such as [Open Props](https://open-props.style/), can help with design themes
6. Optional `!` indicates [`!important`](https://developer.mozilla.org/en-US/docs/Web/CSS/important)
7. Multiple `property:value[!]` can be specified, delimited by semicolons `;`
8. Optional trailing `?` generates unnamed [`@layer{}`](https://developer.mozilla.org/docs/Web/CSS/@layer)
   - For example, add `?` to the components in a component library, so that applications using it can override the properties
9. Optional trailing `*` increases ID-[specificity](https://developer.mozilla.org/docs/Web/CSS/Specificity), more than one can be specified
   - For example, add `*` to the preferred style between `:hover` and `:active`

- An underscore `_` will be replaced with a whitespace ` ` and can be escaped with a backslash (`\_` will be replaced with `_`)

## Setup

### [esbuild](https://esbuild.github.io/)

```js
import esbuild from "esbuild";
import isaaccss from "isaaccss/esbuild";

esbuild.build({
  entryPoints: ["src/index.ts"],
  outdir: "dist",
  bundle: true,
  // Inject `isaaccss.inject`.
  inject: [isaaccss.inject],
  plugins: [
    isaaccss.plugin({
      // Optional filename filter. Default is following.
      filter: /\.[cm][jt]x?$/,

      // Optional isaaccss config. See `Configuration Example` section below.
      pretty: true,
      compress: { prefix: "~" },
      aliases: [],
      postcss: { plugins: [] },
    }),
  ],
});
```

### [Rollup](https://rollupjs.org/)

```js
// rollup.config.js
import isaaccss from "isaaccss/rollup";

/** @type {import("rollup").RollupOptions} */
export default {
  input: "src/index.js",
  output: { file: "dist/index.js" },
  plugins: [
    isaaccss({
      // Optional include filter. By default, all bundled scripts are included.
      include: ["**/*.js"],

      // Optional exclude filter. By default, `**/node_modules/**` are excluded.
      exclude: ["**/node_modules/**"],

      // Optional output filename.
      // Default is the output script filename with extension ".css".
      output: "dist/index.css",

      // Optional isaaccss config. See `Configuration Example` section below.
      pretty: true,
      compress: { prefix: "~" },
      aliases: [],
      postcss: { plugins: [] },
    }),
  ],
};
```

When you want to merge other CSS files with isaaccss CSS, use [`rollup-plugin-import-css`](https://github.com/jleeson/rollup-plugin-import-css) instead of [`rollup-plugin-css-only`](https://github.com/thgh/rollup-plugin-css-only).

```js
// rollup.config.js
import css from "rollup-plugin-import-css";
import isaaccss from "isaaccss/rollup";

/** @type {import("rollup").RollupOptions} */
export default {
  input: "src/index.js",
  output: { file: "dist/index.js" },
  plugins: [css(), isaaccss()],
};
```

### [Vite](https://vitejs.dev/)

Class names are not compressed in Vite dev server, but in Vite build.

```js
// vite.config.js
import isaaccssPlugin from "isaaccss/vite";

/** @type {import("vite").UserConfig} */
export default {
  plugins: [
    isaaccssPlugin({
      // Options are same as for Rollup isaaccss plugin above.
    }),
  ],
};
```

## Configuration Example

```js
import { defaultAliases } from "isaaccss/aliases";
import OpenProps from "open-props";
import postcssJitProps from "postcss-jit-props";

export default {
  // Whether to pretty-print. Default is `false`.
  pretty: true,

  // Class name compression setting in boolean or `{ prefix: string }`. Pass `false` to turn off.
  // Default is `{ prefix: "#" }`; class names are "#a", "#b", ..., "#aa", "#ab", ...
  compress: { prefix: "~" },

  // Aliases. If specified, the default aliases are removed.
  aliases: [
    // If you want to extend the default, pass the `defaultAliases` imported from "isaaccss".
    defaultAliases,

    // Custom aliases. For example:
    {
      // Alias format of `media`, `container` and `selector` is one of the following:
      // - { "search": replacementString }
      // - [/pattern/g, replacementStringOrFunction]
      // - Array of the above
      media: {
        dark: "prefers-color-scheme:dark",
        light: "prefers-color-scheme:light",
        sm: "640px", // use breakpoints like `@w<sm/d:none`
        md: "768px",
      },
      container: {},
      selector: {
        "::a": "::after",
        "::b": "::before",
        ":f": ":focus",
        ":h": ":hover",
      },

      // Alias format of `property` is one of the following:
      // - { "search": replacementStringOrStringArray }
      // - [/pattern/g, replacementStringOrStringArray]
      // - Array of the above
      property: {
        items: "align-items",
        justify: "justify-content",
      },

      // Alias format of `value` is one of the following:
      // - [propertyNameOrPattern, { "search": replacementString }] or
      // - [propertyNameOrPattern, [/pattern/g, replacementStringOrFunction]]
      // - Array of the above
      value: [
        [
          "box-shadow",
          {
            sm: "0 1px 2px hsla($shadow-hsl / 0.1)",
            md: "0 1px 2px hsla($shadow-hsl / 0.1),0 3px 6px hsla($shadow-hsl / 0.1)",
          },
        ],
        [
          // an example of rem-based pixel unit
          // `m:[1]`->{margin:.0625rem} `m-l:[16]`->{margin-left:1rem}
          /^margin|^padding|^font-size$/,
          [/\[(-?\d*\.?\d+)\]/g, (_, $1) => `${+$1 / 16}rem`.replace(/^0\./, ".")],
        ],
      ],
    },
  ],

  // Optional PostCSS config. The only field is `plugins`.
  // Following configuration is an example of using Open Props (e.g. `color:$blue-1`):
  postcss: {
    plugins: [postcssJitProps(OpenProps)],
  },
};
```

See [src/aliases/default.ts](https://github.com/luncheon/isaaccss/blob/main/src/aliases/default.ts) for the default aliases.

## Intention

- Like inline styles and other atomic CSS frameworks:
  - Predictable changes
  - No pondering class names
  - Correct separation of concerns: markup and its style are strongly coupled and should be maintained together. Putting them into separate files is a bad idea.
- Unlike inline styles:
  - Media queries, container queries and selectors (combinators, pseudo-class, pseudo-elements) can be described
  - Specificity can be adjusted
  - Short aliases can be used
  - [`Content-Security-Policy`](https://developer.mozilla.org/docs/Web/HTTP/Headers/Content-Security-Policy): no need `'unsafe-inline'` or `'nonce-a682b15c'` for [`style-src`](https://developer.mozilla.org/docs/Web/HTTP/Headers/Content-Security-Policy/style-src)
- Unlike [Tailwind CSS](https://tailwindcss.com/) and [Windi CSS](https://windicss.org/):
  - This is a class name description rule, not a predefined property set, therefore:
    - Less to remember
    - Simple and flexible: any media, any container, any selector, any property and any value can be described as is
  - High specificity (ID-specificity = 1) by default to override styles from other CSS libraries
  - Specificity can be adjusted
  - Class names can be compressed into prefixed short names such as `#a`, `#b`, ..., `#aa`, ...
  - Invalid class names can be detected
- Unlike [Linaria](https://linaria.dev/):
  - Short aliases can be used
  - Atomic styles are reused, preventing CSS file size bloating

## License

[WTFPL](http://www.wtfpl.net/)
