Marx is a classless CSS framework: beautiful defaults for raw, semantic HTML
with zero classes. Shipped as the npm package marx-css and a CDN stylesheet.
There is no JavaScript and no build output beyond CSS.
src/ authored partials (edit here)
marx.css entry: declares @layer order + @imports each partial into a layer
_tokens.css marx.base — the --marx-* theming API (light-dark() dark mode)
_preflight.css marx.base — cross-browser reset
_base.css marx.content — body, layout, dialog/popover, details
_typography.css marx.content — headings, text, links, code, mark, kbd
_tables.css marx.content
_buttons.css marx.forms
_form.css marx.forms
css/ BUILD OUTPUT, committed — marx.css, marx.min.css, .map
index.html HTML5 test page (open in a browser to eyeball changes)
docs/ GitHub Pages site (docs/index.html), uses css/marx.min.css
npm run build # bundle src/ -> css/marx.css + css/marx.min.css (Lightning CSS)
npm run lint # oxlint
npm run format # oxfmt --write . (scope to changed files)
npm test # runs the test script- Cascade layers: everything lives in
@layer marx.base, marx.content, marx.forms. Layered rules lose to any unlayered author CSS, so users override Marx for free. - Zero specificity: every selector is wrapped in
:where(...). Pseudo-elements (::placeholder,::file-selector-button) can't sit inside:where()— keep them bare, attached to a:where()subject. - Tokens only: never hardcode a color/size — reference a
--marx-*custom property from_tokens.css. Need a value that has no token? Add the token, don't inline it. - Logical properties:
margin-block-start,padding-inline,inline-size,text-align: start— never physicalleft/right/top/bottom. - One-directional rhythm: block elements space with
margin-block-startonly, nevermargin-block-end. Preflight zeroes margins, so don't re-zero. - Focus:
:focus-visible { outline: 2px solid var(--marx-ring); outline-offset: 2px }. Neveroutline: none. - Motion: wrap every
transition/animationin@media (prefers-reduced-motion: no-preference). - No
!important(one sanctioned exception: preflight's[hidden]). - No vendor prefixes — Lightning CSS (browserslist
defaults) adds them.
Changesets-driven, OIDC publish (no npm token in CI):
npm run changesetand commit the generated.changeset/*.md.- Merge to
main. The changesets action opens/updates a Version Packages PR. - Merge that PR — CI publishes to npm via OIDC and tags the release.
- After editing anything in
src/, runnpm run build—css/artifacts are committed and consumed by CDN users; a stalecss/ships broken styles. - Keep
css/paths stable (css/marx.min.css). CDN URLs anddocs/are pinned to them; renaming breaks every consumer. - Run
formatscoped to your changes — don't let it churn unrelated files.