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
// button.css.ts
import { css } from "@mincho-js/css";
export const buttonCss = css({
backgroundColor: "blue",
color: "white",
padding: {
Block: 10,
Inline: 20
},
_hover: {
backgroundColor: "darkblue"
}
});
// button.tsx
import { buttonCss } from "./button.css";
export function MyButton() {
return <button className={buttonCss}></button>;
}
The cssVariant()
function is used to define multiple styles. It allows easy creation of components with multiple variants.
Usage Example
// button.css.ts
import { cssVariant } from '@mincho-js/css';
export 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"
}
}
});
// button.tsx
import { buttonVariants } from "./button.css";
interface ButtonProps {
state: "primary" | "secondary" | "danger";
}
export function MyButton({ state }: ButtonProps) {
return <button className={buttonVariants[state]}></button>;
}
The rules()
function is used to define both static and dynamic styles for reusable blocks.
// button.css.ts
import { rules } from '@mincho-js/css';
export const button = rules({
padding: {
Block: 10,
Inline: 20
},
props: ["margin"],
toggles: {
rounded: { borderRadius: 999 }
}
variants: {
color: {
brand: { color: "#FFFFA0" },
accent: { color: "#FFE4B5" }
},
size: {
small: { padding: 12 },
medium: { padding: 16 },
large: { padding: 24 }
}
}
compoundVariants: ({ color, size }) => [
{
condition: [color.brand, size.small],
style: {
fontSize: "16px"
}
}
]
});
// button.tsx
import { button } from "./button.css";
export function MyButton() {
return (<button
className={button(["rounded", { color: "brand", size: "small" }])}
style={button.props({
margin: "20px"
})}
></button>);
}
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;
}
Define it as an object style, similar to css.
Code:
const myRule = rules({
color: "blue",
backgroundColor: "red"
});
Compiled:
.[FILE_NAME]_myRule__[HASH] {
color: blue;
background-color: red;
}
However, it is returned as a function, so you need to run it to use it.
function MyComponent() {
return <div className={myCSS()}></div>;
}
Provides dynamic styles using CSS Variables.
Code:
const myRule = rules({
props: ["color", "background", { size: { targets: ["padding", "margin"] }}]
});
Compiled:
.[FILE_NAME]_myRule__[HASH] {
color: var(--[FILE_NAME]_myRule_color__[HASH]);
background: var(--[FILE_NAME]_myRule_background__[HASH]);
padding: var(--[FILE_NAME]_myRule_size__[HASH]);
margin: var(--[FILE_NAME]_myRule_size__[HASH]);
}
You can also set a default value.
Code:
const myRule = rules({
props: [
"color",
{
background: { base: "red", targets: ["background"] },
size: { base: "3px", targets: ["padding", "margin"] }
}
]
});
Compiled:
.[FILE_NAME]_myRule__[HASH] {
color: var(--[FILE_NAME]_myRule_color__[HASH]);
background: var(--[FILE_NAME]_myRule_background__[HASH], red);
padding: var(--[FILE_NAME]_myRule_size__[HASH], 3px);
margin: var(--[FILE_NAME]_myRule_size__[HASH], 3px);
}
You can think of use cases as those that are statically extracted and those that are dynamically assigned.
Static Usage:
const myCSS = css([
myRule.props({ color: "red", background: "blue", size: "5px" }),
{ borderRadius: 999 }
]);
Compiled:
.[FILE_NAME]_myCSS__[HASH] {
--myCSS_color__[HASH]: red;
--myCSS_background__[HASH]: blue;
--myCSS_size__[HASH]: 5px;
border-radius: 999px;
}
If dynamic case, it is assigned as an inline style.
Dynamic Usage
import { myRule } from "sample.css";
function Sample({ color }) {
return <div style={myRule.props({ color })}>contents...</div>;
}
Stitches's variants
is well enough made.
Code:
const button = rules({
color: "black",
backgroundColor: "white",
borderRadius: 6,
variants: {
color: {
brand: {
color: "#FFFFA0",
backgroundColor: "blueviolet"
},
accent: {
color: "#FFE4B5",
backgroundColor: "slateblue"
}
},
size: {
small: { padding: 12 },
medium: { padding: 16 },
large: { padding: 24 }
},
rounded: {
true: { borderRadius: 999 }
}
}
});
Compiled:
.[FILE_NAME]_button__[HASH] {
color: black;
background-color: white;
border-radius: 6px;
}
.[FILE_NAME]_button_color_brand__[HASH] {
color: #ffffa0;
background-color: blueviolet;
}
.[FILE_NAME]_button_color_accent__[HASH] {
color: #ffe4b5;
background-color: slateblue;
}
.[FILE_NAME]_button_size_small__[HASH] {
padding: 12px;
}
.[FILE_NAME]_button_size_medium__[HASH] {
padding: 16px;
}
.[FILE_NAME]_button_size_large__[HASH] {
padding: 24px;
}
You can use it as if you were using css
.
Usage:
button({
color: "accent",
size: "large",
rounded: true
});
Stitches's boolean variants
are a special case, but the syntax for defining them is awkward.
Therefore, we introduce a specialized syntax.
Code Before:
const button = rules({
// base styles
variants: {
// common variants
rounded: {
true: { borderRadius: 999 }
}
}
});
Code After:
const button = rules({
// base styles
toggles: {
rounded: { borderRadius: 999 }
}
variants: {
// common variants
}
});
Stitches's Compound Variants
is an effective way to set up additional css by leveraging the combination of variations you have already set up.
However, the method of writing the conditions seems quite inconvenient when conditions are complicated.
So we want to improve the UX in this area.
Code Before:
const button = rules({
// base styles
variants: {
color: {
brand: { color: "#FFFFA0" },
accent: { color: "#FFE4B5" }
},
size: {
small: { padding: 12 },
medium: { padding: 16 },
large: { padding: 24 }
}
},
compoundVariants: [
{
variants: {
color: "brand",
size: "small",
},
style: {
fontSize: "16px"
}
}
]
});
It doesn't seem uncomfortable when the conditions are not as demanding as they are now. But if the conditions become complicated, it will be inconvenient to fill out.
Code After:
const button = rules({
// base styles
variants: {
color: {
brand: { color: "#FFFFA0" },
accent: { color: "#FFE4B5" }
},
size: {
small: { padding: 12 },
medium: { padding: 16 },
large: { padding: 24 }
}
},
compoundVariants: ({ color, size }) => [
{
condition: [color.brand, size.small],
style: {
fontSize: "16px"
}
}
]
});
Compiled:
.[FILE_NAME]_button_compound_0__[HASH] {
font-size: 16px;
}
.[FILE_NAME]_button_compound_1__[HASH] {
font-size: 24px;
font-weight: bold;
}
The way of Stitches's Default Variants
is already good to use, so we keep this method in ours.
Code:
const button = rules({
// base styles
variants: {
color: {
brand: {
color: "#FFFFA0",
backgroundColor: "blueviolet"
},
accent: {
color: "#FFE4B5",
backgroundColor: "slateblue"
}
}
},
defaultVariants: {
color: "brand"
}
});
We welcome contributions! Please see our Contributing Guide for more details.
This project is licensed under the MIT License.