Monsoon v0.2.0
Made by Alexandre Kohler Star

Getting started

Arbitrary values without a build step

When a scale has no step that fits, write the value itself: w-[37px], bg-[#0ea5e9], grid-cols-[200px_1fr]. The stylesheet cannot contain values you invent, so they come from one of two places, neither of them a build step: a small script that writes the rules in the browser (6.0 kB with brotli), or the purge CLI, which compiles them into the purged file.

8 min read

Try it

This page loads the script, so these boxes are styled by classes that are not in the stylesheet. Switch the preview to dark to see the dark: value apply.

Live result
bg-[#0ea5e9]
oklch()
bg-red
border-[3px]

h-[calc(2.5rem+6px)] and text-[13px]

index.html
<div class="grid grid-cols-[repeat(auto-fit,minmax(9rem,1fr))] gap-3">
  <div class="rounded-[14px] bg-[#0ea5e9] p-4 text-white font-[650]">...</div>
  <div class="rounded-[14px] bg-[oklch(0.72_0.16_150)] p-4">...</div>
  <div class="rounded-[14px] border-[3px] border-[#a855f7] dark:text-[#e9d5ff]">...</div>
</div>

The syntax

You writeYou getNotes
w-[37px]width: 37pxAny utility prefix that takes a value, followed by the value in brackets.
p-[3px_7px]padding: 3px 7pxAn underscore is a space. Write \_ for a real underscore.
h-[calc(100vh-4rem)]height: calc(100vh - 4rem)The spaces that calc(), min(), max() and clamp() need around + and - are added for you.
bg-[url(/img/hero_1.png)]background-image: url(/img/hero_1.png)Underscores inside url() stay underscores.
bg-(--brand)background-color: var(--brand)Parentheses take a custom property name: a short way to write bg-[var(--brand)].
[mask-type:luminance]mask-type: luminanceAn arbitrary property: any property and value, in brackets.
bg-[red]/50color-mix(in oklab, red 50%, transparent)An opacity modifier, on the families that take a color.
-mt-[3px]margin-top: -3pxA negative value, on margins, positions, translate, rotate, z-index and a few more.
content-['x']content: 'x'Use it with before: and after:.
text-[length:var(--size)]font-size: var(--size)A type hint for values that could mean two things.

One prefix, two meanings

A few prefixes write a color or a size depending on the value, exactly like their static classes do (border-2 and border-primary). The value decides:

ClassCSSWhy
border-[3px]border-width: 3pxA length is a width.
border-[red]border-color: redA color, a color function or a color keyword is a color.
text-[13px]font-size: 13pxA length is a font size.
text-[#eee]color: #eeeA color is the text color.
font-[650]font-weight: 650A number is a weight, anything else is a family.
bg-[url(/a.png)]background-image: url(/a.png)Images and gradients are a background image.
bg-[cover], bg-[center_top]background-size, background-positionSize and position keywords.
shadow-[0_2px_8px_red]box-shadow (it composes with ring)A whole shadow. shadow-[red] sets only the shadow color.

When a value is ambiguous, for example a custom property, add a hint before a colon: color:, length:, size:, position:, image:, number:, weight: or family-name:. So bg-[length:200px_100px] is a background size and border-(color:--edge) is a border color.

Variants

Every prefix of the static classes works on arbitrary values, on every family, and they can be stacked. The order on the page is the same as in the stylesheet: plain values first, then states, then breakpoints from small to large, then container and media queries.

variants.html
<div class="hover:bg-[#0369a1] md:w-[37rem] dark:text-[#eee]">
<div class="@container"><div class="grid @md:grid-cols-[1fr_2fr]">...</div></div>
<nav class="print:hidden rtl:mr-[3px]">
<p class="md:hover:underline-offset-[6px]">
<span class="before:content-['#'] before:mr-[.25em]">

These are the rules the last line writes:

generated.css
.before\:content-\[\'\#\'\]::before{content:'#'}
@media (min-width: 768px){.md\:hover\:underline-offset-\[6px\]:hover{text-underline-offset:6px}}

Besides the static variants, arbitrary values also take before:, after:, placeholder:, selection:, marker:, file:, backdrop:, first-line: and first-letter: (pseudo elements are always written last), plus visited:, checked:, invalid:, required:, empty:, target:, open:, group-focus: and peer-focus:.

The dark: prefix uses the same rule as the rest of Monsoon: the nearest themed ancestor wins, at any depth.

Runtime or compiled

Both modes use the same parser, from src/arbitrary/, so a class means exactly the same thing in development and in production.

Load the script. monsoon.js includes it next to the interactive layer; monsoon-arbitrary.js is the engine alone, 6.0 kB with brotli.

index.html
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/monsooncss@0.2.0">
<script src="https://cdn.jsdelivr.net/npm/monsooncss@0.2.0/dist/monsoon-arbitrary.js" defer></script>
  • On load it reads the class attributes of the page, and a MutationObserver catches every element added later and every class change, so it works with any library that renders HTML.
  • Each new class is compiled once and cached. Rules go into one constructed stylesheet with adoptedStyleSheets, or a <style id="monsoon-arbitrary"> element in older browsers.
  • Rules are inserted in the order of the static stylesheet, so w-[1px] md:w-[2px] behaves the way you expect whatever order the classes appear in.
  • Classes without brackets or parentheses are skipped with one quick check, so static classes cost nothing.

The engine has a small API on Monsoon.arbitrary (or MonsoonArbitrary for the standalone file): scan(root), add(className), css() to read the generated rules, stop() and reset().

Arbitrary rules come after every static utility. A value you wrote on purpose wins over a scale step on the same element: with p-[3px] md:p-4, the arbitrary padding wins at every width. Put both on the arbitrary side (md:p-[1rem]) when you need a breakpoint to override it.

Safety

Class attributes can come from user content, so the parser only accepts values that cannot leave their declaration. A class is ignored, and nothing is written, when its value:

  • contains ;, {, }, @, !, < or >, so it cannot end a declaration, open a rule or an at-rule, add !important or close a <style> element;
  • contains a backslash other than \_, so nothing can hide behind a CSS escape;
  • contains a comment, a control character, expression( or javascript:;
  • has unbalanced brackets, parentheses or quotes;
  • is longer than 300 characters, or uses an unknown prefix or variant.

Selectors are escaped with the same rules as CSS.escape, and property names of arbitrary properties must be plain lowercase names or custom properties. The test suite feeds the parser a list of injection attempts and checks that none of them produces a rule.

Terminalrejected classes
bg-[red;color:blue]          ignored: ";"
w-[1px}]                     ignored: "}" and unbalanced
content-['</style>']         ignored: "<" and ">"
bg-[\7d]                     ignored: escape
bg-[red_!important]          ignored: "!"

Every family

Each prefix maps to the same properties as the static classes with that prefix. The table is generated from the parser, so it is always complete.

GroupExampleCSSTakes
Layoutinset-[3px]inset: 3pxnegative
inset-x-[3px]left: 3px; right: 3pxnegative
inset-y-[3px]top: 3px; bottom: 3pxnegative
top-[3px]top: 3pxnegative
right-[3px]right: 3pxnegative
bottom-[3px]bottom: 3pxnegative
left-[3px]left: 3pxnegative
start-[3px]inset-inline-start: 3pxnegative
end-[3px]inset-inline-end: 3pxnegative
z-[999]z-index: 999negative
aspect-[4/3]aspect-ratio: 4/3
columns-[18rem]columns: 18rem
object-[25%_75%]object-position: 25% 75%
Flexbox and gridbasis-[37%]flex-basis: 37%
flex-[2_1_0%]flex: 2 1 0%
grow-[2]flex-grow: 2
shrink-[0]flex-shrink: 0
order-[13]order: 13negative
grid-cols-[200px_1fr]grid-template-columns: 200px 1fr
grid-rows-[auto_1fr]grid-template-rows: auto 1fr
col-[2/4]grid-column: 2/4
col-span-[5]grid-column: span 5 / span 5
col-start-[2]grid-column-start: 2
col-end-[-1]grid-column-end: -1
row-[1/3]grid-row: 1/3
row-span-[3]grid-row: span 3 / span 3
row-start-[2]grid-row-start: 2
row-end-[4]grid-row-end: 4
auto-cols-[12rem]grid-auto-columns: 12rem
auto-rows-[minmax(4rem,auto)]grid-auto-rows: minmax(4rem,auto)
gap-[3px]gap: 3px
gap-x-[3px]column-gap: 3px
gap-y-[3px]row-gap: 3px
Spacingp-[3px]padding: 3px
px-[3px]padding-inline: 3px
py-[3px]padding-block: 3px
pt-[3px]padding-top: 3px
pr-[3px]padding-right: 3px
pb-[3px]padding-bottom: 3px
pl-[3px]padding-left: 3px
ps-[3px]padding-inline-start: 3px
pe-[3px]padding-inline-end: 3px
m-[3px]margin: 3pxnegative
mx-[3px]margin-inline: 3pxnegative
my-[3px]margin-block: 3pxnegative
mt-[3px]margin-top: 3pxnegative
mr-[3px]margin-right: 3pxnegative
mb-[3px]margin-bottom: 3pxnegative
ml-[3px]margin-left: 3pxnegative
ms-[3px]margin-inline-start: 3pxnegative
me-[3px]margin-inline-end: 3pxnegative
space-x-[3px]margin-inline-end: 3pxnegative
space-y-[3px]margin-block-end: 3pxnegative
scroll-m-[3px]scroll-margin: 3pxnegative
scroll-mt-[3px]scroll-margin-top: 3pxnegative
scroll-p-[3px]scroll-padding: 3px
scroll-pt-[3px]scroll-padding-top: 3px
Sizingw-[3px]width: 3px
h-[3px]height: 3px
size-[3px]width: 3px; height: 3px
min-w-[3px]min-width: 3px
max-w-[3px]max-width: 3px
min-h-[3px]min-height: 3px
max-h-[3px]max-height: 3px
Typographytext-[13px]font-size: 13pxcolor
font-[650]font-weight: 650
leading-[1.1]line-height: 1.1
tracking-[.02em]letter-spacing: .02emnegative
indent-[3px]text-indent: 3pxnegative
decoration-[3px]text-decoration-thickness: 3pxcolor
underline-offset-[6px]text-underline-offset: 6px
line-clamp-[7]overflow: hidden; display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 7
list-[square]list-style-type: square
align-[-2px]vertical-align: -2px
content-['x']content: 'x'
placeholder-[#0ea5e9]color: #0ea5e9color
Borders, outline and ringborder-[3px]border-width: 3pxcolor
border-x-[3px]border-left-width: 3px; border-right-width: 3pxcolor
border-y-[3px]border-top-width: 3px; border-bottom-width: 3pxcolor
border-t-[3px]border-top-width: 3pxcolor
border-r-[3px]border-right-width: 3pxcolor
border-b-[3px]border-bottom-width: 3pxcolor
border-l-[3px]border-left-width: 3pxcolor
border-s-[3px]border-inline-start-width: 3pxcolor
border-e-[3px]border-inline-end-width: 3pxcolor
rounded-[3px]border-radius: 3px
rounded-t-[3px]border-top-left-radius: 3px; border-top-right-radius: 3px
rounded-r-[3px]border-top-right-radius: 3px; border-bottom-right-radius: 3px
rounded-b-[3px]border-bottom-right-radius: 3px; border-bottom-left-radius: 3px
rounded-l-[3px]border-top-left-radius: 3px; border-bottom-left-radius: 3px
rounded-tl-[3px]border-top-left-radius: 3px
rounded-tr-[3px]border-top-right-radius: 3px
rounded-br-[3px]border-bottom-right-radius: 3px
rounded-bl-[3px]border-bottom-left-radius: 3px
divide-x-[3px]border-inline-style: solid; border-inline-start-width: 0; border-inline-end-width: 3px
divide-y-[3px]border-top-width: 0; border-bottom-style: solid; border-bottom-width: 3px
divide-[#0ea5e9]border-color: #0ea5e9color
outline-[3px]outline-style: solid; outline-width: 3pxcolor
outline-offset-[3px]outline-offset: 3px
ring-[3px]--m-ring-offset-shadow: var(--m-ring-inset,) 0 0 0 var(--m-ring-offset-width) var(--m-ring-offset-color); --m-ring-shadow: var(--m-ring-inset,) 0 0 0 calc(3px + var(--m-ring-offset-width)) var(--m-ring-color); box-shadow: var(--m-ring-offset-shadow), var(--m-ring-shadow), var(--m-shadow)color
ring-offset-[3px]--m-ring-offset-width: 3pxcolor
Colors and backgroundsbg-[#0ea5e9]background-color: #0ea5e9color
from-[#0ea5e9]--m-gradient-from: #0ea5e9; --m-gradient-stops: var(--m-gradient-from), var(--m-gradient-via, var(--m-gradient-to, transparent)), var(--m-gradient-to, transparent)color
via-[#0ea5e9]--m-gradient-via: #0ea5e9color
to-[#0ea5e9]--m-gradient-to: #0ea5e9color
fill-[#0ea5e9]fill: #0ea5e9color
stroke-[1.5]stroke-width: 1.5color
caret-[#0ea5e9]caret-color: #0ea5e9color
accent-[#0ea5e9]accent-color: #0ea5e9color
Effects and filtersshadow-[0_2px_8px_rgb(0_0_0/.2)]--m-shadow: 0 2px 8px rgb(0 0 0/.2); box-shadow: var(--m-ring-offset-shadow), var(--m-ring-shadow), var(--m-shadow)color
opacity-[.35]opacity: .35
mix-blend-[multiply]mix-blend-mode: multiply
blur-[3px]filter: blur(3px)
brightness-[1.2]filter: brightness(1.2)
contrast-[1.1]filter: contrast(1.1)
saturate-[1.4]filter: saturate(1.4)
grayscale-[.5]filter: grayscale(.5)
invert-[.8]filter: invert(.8)
sepia-[.3]filter: sepia(.3)
hue-rotate-[90deg]filter: hue-rotate(90deg)negative
drop-shadow-[0_2px_4px_red]filter: drop-shadow(0 2px 4px red)
backdrop-blur-[3px]-webkit-backdrop-filter: blur(3px); backdrop-filter: blur(3px)
backdrop-brightness-[3px]-webkit-backdrop-filter: brightness(3px); backdrop-filter: brightness(3px)
Transitions and transformstransition-[width]transition-property: width; transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); transition-duration: 150ms
duration-[250]transition-duration: 250ms
delay-[75]transition-delay: 75ms
ease-[linear]transition-timing-function: linear
animate-[spin_2s_linear_infinite]animation: spin 2s linear infinite
scale-[1.15]scale: 1.15
scale-x-[1.15]scale: 1.15 1
scale-y-[1.15]scale: 1 1.15
rotate-[17deg]rotate: 17degnegative
translate-x-[3px]--m-tx: 3px; translate: var(--m-tx) var(--m-ty)negative
translate-y-[3px]--m-ty: 3px; translate: var(--m-tx) var(--m-ty)negative
origin-[30%_70%]transform-origin: 30% 70%
Interactivitycursor-[grab]cursor: grab
will-change-[scroll-position]will-change: scroll-position
Morescroll-mx-[3px]scroll-margin-inline: 3pxnegative
scroll-px-[3px]scroll-padding-inline: 3px
scroll-my-[3px]scroll-margin-block: 3pxnegative
scroll-py-[3px]scroll-padding-block: 3px
scroll-mr-[3px]scroll-margin-right: 3pxnegative
scroll-pr-[3px]scroll-padding-right: 3px
scroll-mb-[3px]scroll-margin-bottom: 3pxnegative
scroll-pb-[3px]scroll-padding-bottom: 3px
scroll-ml-[3px]scroll-margin-left: 3pxnegative
scroll-pl-[3px]scroll-padding-left: 3px
scroll-ms-[3px]scroll-margin-inline-start: 3px
scroll-ps-[3px]scroll-padding-inline-start: 3px
scroll-me-[3px]scroll-margin-inline-end: 3px
scroll-pe-[3px]scroll-padding-inline-end: 3px
backdrop-contrast-[3px]-webkit-backdrop-filter: contrast(3px); backdrop-filter: contrast(3px)
backdrop-saturate-[3px]-webkit-backdrop-filter: saturate(3px); backdrop-filter: saturate(3px)
backdrop-grayscale-[3px]-webkit-backdrop-filter: grayscale(3px); backdrop-filter: grayscale(3px)
backdrop-invert-[3px]-webkit-backdrop-filter: invert(3px); backdrop-filter: invert(3px)
backdrop-sepia-[3px]-webkit-backdrop-filter: sepia(3px); backdrop-filter: sepia(3px)
backdrop-hue-rotate-[3px]-webkit-backdrop-filter: hue-rotate(3px); backdrop-filter: hue-rotate(3px)
backdrop-drop-shadow-[3px]-webkit-backdrop-filter: drop-shadow(3px); backdrop-filter: drop-shadow(3px)

Bare color names

Every palette hue also works without a step: bg-red, text-blue, border-emerald, ring-violet, from-sky. These are static classes in the stylesheet, no script needed. They use step 500 in light mode and step 400 in dark mode, and follow the nearest themed ancestor like every semantic color.

Live result
bg-redbg-orangebg-emeraldbg-skybg-violetborder-pink

Frequently asked questions

Do I need the script in production?

No. Run npx monsooncss purge and the arbitrary classes are compiled into the purged stylesheet. Keep the script for prototypes, CMS content and pages you never purge.

Does it slow the page down?

The engine only looks at class names that contain a bracket or a parenthesis and compiles each distinct class once, so the work grows with the number of different arbitrary classes, not with the size of the page. The whole interactive layer with the engine is 53.8 kB with brotli.

Can a user break my page with a class attribute?

Not through the engine: values that could end a declaration or open a rule are rejected before any CSS is written, see Safety above.

No recent searches

Try , , or .

Recent

Loading the search index

No results for ""

Check the spelling, or search a class name such as px-4.