The @mincho-js/css package provides framework-agnostic APIs for CSS-in-JS styling.
🌱 Easy adoption from Vanilla Extract.
💉 Vanilla Extract with the power of a preprocessor — Features inspired by Sass, Less, Stylus, etc.
🤸 Syntax optimized for TypeScript — Carefully designed as if it were native to TypeScript.
🌐 Works with any front-end framework — or even without one.
🔒 Still type-safe styles for TypeScript.
This package has vanilla extract as a peer dependency, so we install them together.
You'll also need to set up bundler integration.
npm install @mincho-js/css @vanilla-extract/css
# or
yarn add @mincho-js/css @vanilla-extract/css
# or
pnpm install @mincho-js/css @vanilla-extract/css
Define styles in a file named .css.ts
:
// styles.css.ts
import { css } from "@mincho-js/css";
export const container = css({
padding: 10
});
css()
returns a class name, which you can import and use in your app:
// app.ts
import { container } from "./styles.css.ts";
document.write(`
<section class="${container}">
...
</section>
`);
The css()
function takes a style object and generates a unique class name for the given styles.
Usage Example
import { css } from "@mincho-js/css";
const buttonStyle = css({
backgroundColor: "blue",
color: "white",
padding: {
Block: 10,
Inline: 20
},
_hover: {
backgroundColor: "darkblue"
}
});
The cssVariant()
function is used to define conditional styles. It allows easy creation of components with multiple variants.
Usage Example
import { cssVariant } from '@mincho-js/css';
const buttonVariants = cssVariant({
primary: {
backgroundColor: "blue",
color: "white"
},
secondary: {
backgroundColor: "gray",
color: "black",
"%primary &":{
color: "white"
}
},
danger: {
backgroundColor: "red",
color: {
base: "white",
"@media (prefers-color-scheme: dark)": "black"
}
}
});
Some features are already implemented in Vanilla Extract, but we're assuming a first-time reader.
Instead, we've attached an emoji to make it easier to distinguish.
- Vanilla Extract: 🧁
- Mincho: 🍦
We need to have a hash value to solve the problem of overlapping class names.
Vanilla Extract's style()
is already doing a good job.
Code:
const myCss = css({
color: "blue",
backgroundColor: "#EEEEEE"
});
Compiled:
.[FILE_NAME]_myCSS__[HASH] {
color: blue;
background-color: #EEEEEE;
}
Identifiers can be changed with settings.
Unitless Properties is convenient because it reduces unnecessary string representations.
Code:
export const myCss = css({
// cast to pixels
padding: 10,
marginTop: 25,
// unitless properties
flexGrow: 1,
opacity: 0.5
});
Compiled:
.[FILE_NAME]_myCSS__[HASH] {
padding: 10px;
margin-top: 25px;
flex-grow: 1;
opacity: 0.5;
}
Vendor Prefixes is convenient because it reduces unnecessary string representations.
Code:
export const myCss = css({
WebkitTapHighlightColor: "rgba(0, 0, 0, 0)"
});
Compiled:
.[FILE_NAME]_myCSS__[HASH] {
-webkit-tap-highlight-color: rgba(0, 0, 0, 0);
}
Fallback Styles is convenient because it reduces unnecessary properties.
Code:
export const myCss = css({
// In Firefox and IE the "overflow: overlay" will be
// ignored and the "overflow: auto" will be applied
overflow: ["auto", "overlay"]
});
Compiled:
.[FILE_NAME]_myCSS__[HASH] {
overflow: auto;
overflow: overlay;
}
Inspired by the Less's Merge properties, this feature allows you to composition long split values.
- If they end in
$
, they are joined by a comma - if they end in
_
, they are joined by a whitespace
Code:
export const myCss = css({
boxShadow$: ["inset 0 0 10px #555", "0 0 20px black"],
transform_: ["scale(2)", "rotate(15deg)"]
});
Compiled:
.[FILE_NAME]_myCSS__[HASH] {
boxShadow: inset 0 0 10px #555, 0 0 20px black;
transform: scale(2) rotate(15deg);
}
For use with Fallback Styles, use a double array.
It's automatically composited.
Code:
export const myCss = css({
transform_: [
// Apply to all
"scale(2)",
// Fallback style
["rotate(28.64deg)", "rotate(0.5rad)"]
]
});
Compiled:
.[FILE_NAME]_myCSS__[HASH] {
transform: scale(2) rotate(28.64deg);
transform: scale(2) rotate(0.5rad);
}
Inspired by the Tailwind's Important modifier, If !
is at the end of the value, treat it as !important
.
Code:
export const myCss = css({
color: "red!"
});
Compiled:
.[FILE_NAME]_myCSS__[HASH] {
color: red !important;
}
Unlike Vanilla Extract's CSS Variables, it is supported at the top level.
Inspired by the SASS Variable, You can use $
like you would a variable.
The conversion to prefix and kebab-case
happens automatically.
Code:
export const myCss = css({
$myCssVariable: "purple",
color: "$myCssVariable",
backgroundColor: "$myOtherVariable(red)"
});
Compiled:
.[FILE_NAME]_myCSS__[HASH] {
--my-css-variable: purple;
color: var(--my-css-variable);
background-color: var(--my-other-variable, red);
}
Simple Pseudo Selectors is convenient because these are the elements you typically use with "&", so keep it.
Inspired by the Panda CSS's Conditional Styles, _
is used as :
.
However, no other classes or attributes are added, it's a simple conversion.
camelCase
also convert to kebab-case
.
Code:
export const myCss = css({
_hover: {
color: "pink"
},
_firstOfType: {
color: "blue"
},
__before: {
content: ""
}
});
Compiled:
.[FILE_NAME]_myCSS__[HASH]:hover {
color: pink;
}
.[FILE_NAME]_myCSS__[HASH]:first-of-type {
color: blue;
}
.[FILE_NAME]_myCSS__[HASH]::before {
content: "";
}
Allow toplevel attribute selector
to prevent deep nesting.
It would be nice to be able to autocomplete HTML attributes.
If the start is [
without &
treat it as attribute selectors
.
It is a continuation of Simple Pseudo Selectors.
Code:
export const myCss = css({
"[disabled]": {
color: "red"
},
`[href^="https://"][href$=".org"]`: {
color: "blue"
}
});
Compiled:
.[FILE_NAME]_myCSS__[HASH][disabled] {
color: red;
}
.[FILE_NAME]_myCSS__[HASH][href^="https://"][href$=".org"] {
color: blue;
}
Unlike Vanilla Extract's Complex Selectors, it is supported at the top level.
I want to reduce nesting as much as possible.
Exception values for all properties are treated as complex selectors.
Code:
export const myCss = css({
"&:hover:not(:active)": {
border: "2px solid aquamarine"
},
"nav li > &": {
textDecoration: "underline"
}
});
Compiled:
.[FILE_NAME]_myCSS__[HASH]:hover:not(:active) {
border: 2px solid aquamarine;
}
nav li > .[FILE_NAME]_myCSS__[HASH] {
text-decoration: underline;
}
[!WARNING] Constraints like circular reference still apply.
That it inherits all of Vanilla Extract's constraints.
const invalid = css({
// ❌ ERROR: Targetting `a[href]`
"& a[href]": {...},
// ❌ ERROR: Targetting `.otherClass`
"& ~ div > .otherClass": {...}
});
// Also Invalid example:
export const child = css({});
export const parent = css({
// ❌ ERROR: Targetting `child` from `parent`
[`& ${child}`]: {...}
});
// Valid example:
export const parent = css({});
export const child = css({
[`${parent} &`]: {...}
});
As above, Circular reference is the same.
export const child = css({
background: "blue",
get selectors() {
return {
[`${parent} &`]: {
color: 'red'
}
};
}
});
export const parent = css({
background: "yellow",
selectors: {
[`&:has(${child})`]: {
padding: 10
}
}
});
Allows nesting, like Vanilla Extract's Media Queries, and also allows top-levels.
Code:
export const myCss = css({
// Nested
"@media": {
"screen and (min-width: 768px)": {
padding: 10
},
"(prefers-reduced-motion)": {
transitionProperty: "color"
}
},
// Top level
"@supports (display: grid)": {
display: "grid"
}
});
Compiled:
@media screen and (min-width: 768px) {
.[FILE_NAME]_myCSS__[HASH] {
padding: 10px;
}
}
@media (prefers-reduced-motion) {
.[FILE_NAME]_myCSS__[HASH] {
transition-property: color;
}
}
@supports (display: grid) {
.[FILE_NAME]_myCSS__[HASH] {
display: grid;
}
}
Inspired by the Griffel's Keyframes, Makes @keyframes
or @font-face
writable inline.
fontFamily$
is used as special case of the Merge Values
rule.
Code:
export const myCss = css({
// Keyframes
animationName: {
"0%": { transform: "rotate(0deg)" },
"100%": { transform: "rotate(360deg)" }
},
animationDuration: "3s",
// Fontface
fontFamily: {
src: "local('Comic Sans MS')"
},
// Fontface with multiple
fontfamily$: [{ src: "local('Noto Sans')" }, { src: "local('Gentium')" }]
});
Compiled:
@keyframes [FILE_NAME]_myCSSKeyframes__[HASH] {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
@font-face {
src: local("Comic Sans MS");
font-family: "[FILE_NAME]_myCSSFontFace1__[HASH]";
}
@font-face {
src: local("Noto Sans");
font-family: "[FILE_NAME]_myCSSFontFace2__[HASH]";
}
@font-face {
src: local("Gentium");
font-family: "[FILE_NAME]_myCSSFontFace3__[HASH]";
}
.[FILE_NAME]_myCSS__[HASH] {
animation-name: [FILE_NAME]_myCSSKeyframes__[HASH];
animation-duration: 3s;
font-family: [FILE_NAME]_myCSSFontFace1__[HASH];
font-family: [FILE_NAME]_myCSSFontFace2__[HASH], [FILE_NAME]_myCSSFontFace3__[HASH];
}
Inspired by the SCSS's nested properties, this feature allows nesting for property names.
Reduce redundancy and make your context stand out.
Uppercase it to distinguish it from Property based condition
.
Vendor Prefixes
exists only in Top level, while Nested Properties
exists only in nesting, so you can tell them apart.
Code:
export const myCss = css({
transition: {
Property: "font-size",
Duration: "4s",
Delay: "2s"
}
});
Compiled:
.[FILE_NAME]_myCSS__[HASH] {
transition-property: font-size;
transition-duration: 4s;
transition-delay: 2s;
}
Inspired by the Panda CSS, You can apply properties based on selectors or at-rules.
The default properties refer to base
.
export const myCss = css({
color: {
base: "red",
_hover: "green",
"[disabled]": "blue",
"nav li > &": "black",
"@media (prefers-color-scheme: dark)": "white"
}
});
Compiled:
.[FILE_NAME]_myCSS__[HASH] {
color: red;
}
.[FILE_NAME]_myCSS__[HASH]:hover {
color: green;
}
.[FILE_NAME]_myCSS__[HASH][disabled] {
color: blue;
}
nav li > .[FILE_NAME]_myCSS__[HASH] {
color: black;
}
@media (prefers-color-scheme: dark) {
.[FILE_NAME]_myCSS__[HASH] {
color: red;
}
}
Inspired by the SCSS's nested selectors, this feature allows nesting for selectors.
It works with Simple Pseudo Selectors and Complex Selectors.
export const myCss = css({
"nav li > &": {
color: "red",
_hover: {
color: "green"
},
"&:hover:not(:active)": {
color: "blue"
},
":root[dir=rtl] &": {
color: "black"
}
}
});
Compiled:
nav li > .[FILE_NAME]_myCSS__[HASH] {
color: red;
}
nav li > .[FILE_NAME]_myCSS__[HASH]:hover {
color: green;
}
nav li > .[FILE_NAME]_myCSS__[HASH][disabled]:hover:not(:active) {
color: blue;
}
:root[dir=rtl] nav li > .[FILE_NAME]_myCSS__[HASH] {
color: black;
}
Like Nested Selectors
, but they are hoisted and combined into a AND
rule.
Depending on the Ar-Rules
keyword, the combining syntax is slightly different.
(Unlike @media
, @supports
, and @container
, @layer
is displayed like parent.child
.)
Code:
export const myCss = css({
"nav li > &": {
color: "red",
"@media (prefers-color-scheme: dark)": {
"@media": {
"(prefers-reduced-motion)": {
color: "green"
},
"(min-width: 900px)": {
color: "blue"
}
}
},
"@layer framework": {
"@layer": {
"layout": {
color: "black"
},
"utilities": {
color: "white"
}
}
}
}
});
Compiled:
nav li > .[FILE_NAME]_myCSS__[HASH] {
color: red;
}
@media (prefers-color-scheme: dark) and (prefers-reduced-motion) {
nav li > .[FILE_NAME]_myCSS__[HASH] {
color: green;
}
}
@media (prefers-color-scheme: dark) and (min-width: 900px) {
nav li > .[FILE_NAME]_myCSS__[HASH] {
color: blue;
}
}
@layer framework.layout {
nav li > .[FILE_NAME]_myCSS__[HASH] {
color: blue;
}
}
@layer framework.utilities {
nav li > .[FILE_NAME]_myCSS__[HASH] {
color: blue;
}
}
It can be used with Property based condition
.
Code:
export const myCss = css({
"nav li > &": {
color: {
base: "red",
"@media (prefers-color-scheme: dark)": {
"@media (prefers-reduced-motion)": "green",
"@media (min-width: 900px)": "blue"
},
"@layer framework": {
"@layer": {
"layout": "black",
"utilities": "white"
}
}
}
}
});
Inspired by the Stylus's property lookup, this feature can be used to refer to a property value.
Code:
export const myCss = css({
width: "50px",
height: "@width",
margin: "calc(@width / 2)"
});
Compiled:
.[FILE_NAME]_myCSS__[HASH] {
width: 50px
height: 50px;
margin: calc(50px / 2);
}
When used alone, like "@flexGrow"
, you can use the literal value it refers to.
Code:
export const myCss = css({
flexGrow: 1,
flexShrink: "@flexGrow"
});
Compiled:
.[FILE_NAME]_myCSS__[HASH] {
flex-grow: 1;
flex-shrink: 1;
}
Inspired by the JSS plugin nested, this feature can be reference a local rule.
Use the %
symbol.
Code:
export const myCss = cssVariant({
primary: {
color: "red",
":has(%secondary)": {
color: "blue",
}
},
secondary: {
color: "black",
"%primary &":{
color: "white"
}
}
});
Compiled:
.[FILE_NAME]_myCSS_primary__[HASH] {
color: red;
}
.[FILE_NAME]_myCSS_primary__[HASH]:has(.[FILE_NAME]_myCSS_secondary__[HASH]) {
color: blue;
}
.[FILE_NAME]_myCSS_secondary__[HASH] {
color: black;
}
.[FILE_NAME]_myCSS_primary__[HASH] .[FILE_NAME]_myCSS_secondary__[HASH] {
color: white;
}
Vanilla Extract's composition is well enough made, so keep it.
Code:
const base = css({ padding: 12 });
const primary = css([base, { background: "blue" }]);
const secondary = css([base, { background: "aqua" }]);
Compiled:
.[FILE_NAME]_base__[HASH] {
padding: 12px;
}
.[FILE_NAME]_base__[HASH] {
background: blue;
}
.[FILE_NAME]_base__[HASH] {
background: aqua;
}
We welcome contributions! Please see our Contributing Guide for more details.
This project is licensed under the MIT License.