An atomic CSS DSL like inline style.
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:
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>
);
npm i -D isaaccss
Import is
from "isaaccss"
and write styles in is
-tagged template.
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:
document.body.className = `#a #b`;
.\#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.
[@media/][@^container/][selectors/]property:value[!][;property:value[!]...][?][*[*...]]
~~~~~~~ ~~~~~~~~~~~~ ~~~~~~~~~~ ~~~~~~~~ ~~~~~ ~ ~~~~~~~~~~~~~~~~~~~~~ ~ ~~~~~~~
(1) (2) (3) (4) (5) (6) (7) (8) (9)
- Optional
@media/
indicates 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 } }
- Optional
@^container/
indicates 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 } }
- Optional
selectors/
indicates additional selectors- Pseudo-classes
e.g.:hover/
,:has(>:checked)/
- Pseudo-elements
e.g.::before/
,::part(foo)/
- Child combinator
e.g.>div/
- Adjacent sibling combinator
e.g.+div/
- 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 }
- Pseudo-classes
- Required
property
indicates the property name- Must be one of the known properties or a custom property
- Required
value
indicates the property value$bar
will be replaced withvar(--bar)
- Custom property set libraries, such as Open Props, can help with design themes
- Optional
!
indicates!important
- Multiple
property:value[!]
can be specified, delimited by semicolons;
- Optional trailing
?
generates unnamed@layer{}
- For example, add
?
to the components in a component library, so that applications using it can override the properties
- For example, add
- Optional trailing
*
increases ID-specificity, more than one can be specified- For example, add
*
to the preferred style between:hover
and:active
- For example, add
- An underscore
_
will be replaced with a whitespace\_
will be replaced with_
)
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.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
instead of rollup-plugin-css-only
.
// 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()],
};
Class names are not compressed in Vite dev server, but in Vite build.
// 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.
}),
],
};
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 for the default aliases.
- 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
: no need'unsafe-inline'
or'nonce-a682b15c'
forstyle-src
- Unlike Tailwind CSS and Windi CSS:
- 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
- This is a class name description rule, not a predefined property set, therefore:
- Unlike Linaria:
- Short aliases can be used
- Atomic styles are reused, preventing CSS file size bloating