Compile-time
Pas de Proxy, pas de Virtual DOM. Le compilateur génère des mises à jour DOM chirurgicales — il sait quel nœud dépend de quelle valeur.
La réactivité est résolue à la compilation. Les composants sont des Web Components natifs avec leur propre Shadow DOM. Réactivité profonde par défaut, et SSR inclus.
Pas de Proxy, pas de Virtual DOM. Le compilateur génère des mises à jour DOM chirurgicales — il sait quel nœud dépend de quelle valeur.
0,98× Svelte 5 sur le benchmark officiel, ~17 Ko brotli (~19 Ko gzip), et ~15 ms pour 1M de mutations profondes. Aucun surcoût de Proxy au runtime.
Chaque composant est un Custom Element avec son Shadow DOM. Pas de runtime propriétaire — juste la plateforme.
Objets et tableaux imbriqués suivis automatiquement.
Render-then-replace, Declarative Shadow DOM.
Transitions intégrées et animations FLIP.
Un routeur côté client, livré avec.
État global via stores $$ et contexte §.
<@module> monte un composant par son nom.
Fragments de markup réutilisables et slots nommés.
<@failed> capture les erreurs et affiche un repli.
Ce compteur est un composant MJS bien vivant. La réactivité est compilée — un clic met à jour le nombre, et rien d'autre.
<script> $count = 0 inc = -> $count++ </script> <button @click={inc}> Clics : {$count} </button>
Chaque exemple ci-dessous est vivant : cliquez, cochez, glissez. Le code à côté est celui qui tourne.
Une liste, des cases, un total : vous changez une donnée, l'écran suit. Rien à recâbler, jamais.
{for todo in $todos}
<@checkbox checked=!{todo}.done>{todo.label}</@checkbox>
{end}
<p class="count">{$doneCount} / {$todos.length}</p>0 sur 3 faites
Deux dollars devant un nom, et la valeur existe pour toute la page. Trois cartes, un seul score, aucun fil à tirer.
<button @click={$$score++}>{$$score}</button>0
score partagé
0
score partagé
0
score partagé
Une lettre qui vole, un décalage par lettre, un dégradé qui glisse : tout tient dans un attribut posé sur la balise. Cliquez pour rejouer.
<script>
$k = 0
letters = 'ModularJS'.split ''
replay = -> $k++
</script>
{key $k}
<h1 class="title">
{for i, l in letters}
<span @in.fly={ y: 40, delay: i * 60, duration: 600, easing: µ.easing.backOut }>{l}</span>
{end}
</h1>
{end}
<button @click={replay()}>Rejouer</button>Treize façons de passer d'un écran à l'autre, réglables par un mot. Choisissez un préréglage, une direction, et regardez la page tourner.
<routes target="vt">
</routes>
<@view vt @viewTransition.cube={ dir: left }>Liste déroulante avec recherche, cases, boutons radio, interrupteur, pastille de couleur, image optimisée : livrés avec le framework, pilotables au clavier, et ils s'envoient dans un formulaire ordinaire.
<div class="screen" --accent={$color}>
<@field label="Pseudo" help="Visible par tous">
<input type="text" name="nick" value=!{$nick} aria-label="Pseudo">
</@field>
<@select name="country" search placeholder="Pays…" search-placeholder="Chercher…" value=!{$country}>
<@option value="fr">🇫🇷 France</@option>
<@option value="be">🇧🇪 Belgique</@option>
<@option value="ch">🇨🇭 Suisse</@option>
<@option value="ca">🇨🇦 Canada</@option>
<@option value="de">🇩🇪 Allemagne</@option>
<@option value="es">🇪🇸 Espagne</@option>
<@option value="it">🇮🇹 Italie</@option>
<@option value="gb">🇬🇧 Royaume-Uni</@option>
<@option value="us">🇺🇸 États-Unis</@option>
<@option value="jp">🇯🇵 Japon</@option>
</@select>
<div class="row">
<@radio name="size" value="s" group=!{$size}>S</@radio>
<@radio name="size" value="m" group=!{$size}>M</@radio>
<@radio name="size" value="l" group=!{$size}>L</@radio>
</div>
<div class="row">
<@checkbox name="news" checked=!{$news}>Recevoir les nouveautés</@checkbox>
<@switch name="night" checked=!{$night}>Mode nuit</@switch>
</div>
<div class="row">
<@color value=!{$color} editable label="Couleur du profil">
<@img src="tuto/girl.jpg" alt="Avatar" widths="96 192" sizes="48px">
</div>
<p class="summary">{$nick} · {$country} · taille {$size.toUpperCase()} · {if $night}nuit{else}jour{end} · {if $news}abonné{else}pas abonné{end}</p>
</div>Visible par tous

Léa · 🇫🇷 France · taille M · nuit · abonné
Supprimer, envoyer, quitter : posez la question dans la balise. Une boîte de dialogue à vos couleurs s'ouvre, et rien ne se passe tant qu'on n'a pas dit oui. La même boîte sait aussi demander un texte, ou annoncer un succès.
<button class="x" @confirm="Supprimer cette photo ?" @click={remove(photo.id)} aria-label="Supprimer">✖</button>result = await µmodal.fire title: 'Votre prénom ?', input: 'text'



Un succès, une alerte, un message : une ligne, et une notification glisse dans le coin de l'écran avec sa barre de vie, façon Steam. Le son s'ajoute en un mot.
µmodal.notify 'Succès débloqué : première notification', type: 'success', sound: trueen bas à droite, avec le son
Les couleurs vivent à part du code : un bloc de valeurs, posé sur une carte, une section ou le site entier. Le plus proche gagne, et tout ce qui est dedans suit — les composants imbriqués aussi.
<theme name="gold">
$$accent: #d4af37
$$card-surface: #2a2410
</theme><@showcase-card theme="gold">Léa
Designer
Marc
Développeur
Nour
Chef de projet
Vous
Visiteur
un attribut, rien d'autre
Déclarez vos écrans, posez une zone : les liens changent la vue sans recharger, l'adresse suit, le bouton Précédent marche.
<routes target="app">
/ showcase-page-home
/photos showcase-page-photos
/profil showcase-page-profile
/* showcase-page-home
</routes>
<div class="screen">
<div class="phone">
<nav class="tabs">
<a href="#/" @class{µurl.path == '/'}="on">Accueil</a>
<a href="#/photos" @class{µurl.path == '/photos'}="on">Photos</a>
<a href="#/profil" @class{µurl.path == '/profil'}="on">Profil</a>
</nav>
<div class="view">
<@view app @viewTransition.none>
</div>
</div>
<p class="hint">cliquez : l'adresse change, la page ne se recharge pas</p>
</div>
<style>
:host
display: block
width: 100%
.screen
display: flex
flex-direction: column
align-items: center
justify-content: center
gap: 16px
width: 100%
height: 300px
border-radius: 14px
background: $$screen
box-shadow: inset 0 0 40px color-mix(in srgb, $$accent 8%, transparent)
.phone
width: 260px
margin: 0 auto
border-radius: 22px
border: 1px solid $$border
background: $$panel
overflow: hidden
.tabs
display: flex
a
flex: 1
text-align: center
padding: 10px 0
color: $$muted
text-decoration: none
font-size: 13px
&.on
color: $$fg
box-shadow: inset 0 -2px 0 $$accent
.view
height: 220px
padding: 14px
margin-top: 4px
overflow: hidden
position: relative
.hint
margin: 0
color: $$muted
font-size: 13px
</style><h3>Bonjour !</h3>
<p>Trois écrans, un seul fichier de routes.</p>
<style>
:host
display: block
padding: 16px
color: $$fg
animation: enter .35s ease-out
h3
margin: 0 0 8px
color: $$accent
p
color: $$muted
font-size: 13px
@keyframes enter
from
opacity: 0
transform: translateX(24px)
to
opacity: 1
transform: none
</style><h3>Photos</h3>
<div class="grid">
<img src="µasset('tuto/16/16-5/retrowave.webp')" alt="">
<img src="µasset('tuto/16/16-2/deepsea.webp')" alt="">
<img src="µasset('tuto/16/16-5/halloween.webp')" alt="">
<img src="µasset('tuto/16/16-5/spaaaaace.webp')" alt="">
</div>
<style>
:host
display: block
padding: 16px
color: $$fg
animation: enter .35s ease-out
h3
margin: 0 0 8px
color: $$accent
.grid
display: grid
grid-template-columns: 1fr 1fr
gap: 8px
img
width: 100%
aspect-ratio: 4 / 3
object-fit: cover
border-radius: 8px
@keyframes enter
from
opacity: 0
transform: translateX(24px)
to
opacity: 1
transform: none
</style><h3>Profil</h3>
<div class="avatar">L</div>
<p>Léa — Designer</p>
<style>
:host
display: block
padding: 16px
color: $$fg
animation: enter .35s ease-out
h3
margin: 0 0 8px
color: $$accent
p
color: $$muted
font-size: 13px
.avatar
display: flex
align-items: center
justify-content: center
width: 48px
height: 48px
margin: 8px 0
border-radius: 50%
background: $$accent
color: #06111f
font-weight: 700
font-size: 18px
@keyframes enter
from
opacity: 0
transform: translateX(24px)
to
opacity: 1
transform: none
</style>| Critère | MJS | Svelte | React | Solid |
|---|---|---|---|---|
| Approche | compile-time | compile-time | VDOM | compile-time |
| Runtime | ~17 Ko | ~10 Ko | ~45 Ko | ~7 Ko |
| Réactivité profonde sans Proxy | ✓ | partiel | non | manuel |
| Web Components natifs | ✓ | non | non | non |
| SSR | ✓ | ✓ | ✓ | ✓ |
Chiffres indicatifs, donnés pour être honnêtes, pas promotionnels.
Audité par Fable 5.1 (Claude, Anthropic)
Méga-banc de référence : la même application météo — recherche de ville, prévisions, persistance, accessibilité — écrite dans 14 frameworks, rendue côté client sans prérendu, comme les frameworks construits.
Les 14 versions mesurées le 18/09/2026 au soir, même machine, même session : première visite puis seconde visite
La version MJS inline dans son index.html le seul CSS critique et diffère le reste en une feuille liée, comme la construction d'Angular le fait pour la sienne ; les autres versions servent leurs feuilles telles que le banc les fournit. Le banc ne compte pas index.html dans le poids.
Audité par Fable 5.1 (Claude, Anthropic)
Une commande pour récupérer le framework.
npm install modularjs-framework