React

#Lien direct vers cette section

Installez le paquet (npm install @ariane-ui/core) puis importez-le une fois à un point d'entrée global — React ne rend pas les Custom Elements utilisables sans que leur classe soit enregistrée.

import '@ariane-ui/core';

function Pagination() {
    return (
        <ar-pagination
            current={1}
            total={10}
            onar-pagination-page-changed={(event) => console.log(event.detail.to)}
        />
    );
}

Événements

#Lien direct vers cette section

Depuis React 19, les Custom Elements sont supportés nativement : un événement personnalisé s'écoute via un préfixe on sensible à la casse (onar-pagination-page-changed écoute l'événement ar-pagination-page-changed). Sur React 18 et antérieur, ce support natif n'existe pas — passez par une ref et addEventListener :

// React 18 et antérieur : pas de support natif des Custom Elements, passe par une ref.
useEffect(() => {
    const el = ref.current;
    const onChanged = (event) => console.log(event.detail.to);
    el.addEventListener('ar-pagination-page-changed', onChanged);
    return () => el.removeEventListener('ar-pagination-page-changed', onChanged);
}, []);

Propriétés et attributs

#Lien direct vers cette section

React 19 décide pour chaque prop : si le nom existe sur l'élément, il affecte la propriété ; sinon il pose un attribut (true donne un attribut vide ; false, undefined ou une fonction retirent l'attribut). Un nom en camelCase (isDateDisabled, closeOnBackdrop) est donc une propriété, et un nom en kebab-case (close-on-backdrop) un attribut.

import '@ariane-ui/core';

const isSunday = (date) => date.getDay() === 0;

function Booking() {
    return (
        <>
            {/* Noms camelCase : React 19 affecte la propriété */}
            <ar-datepicker label="Date de départ" isDateDisabled={isSunday} />
            <ar-dialog closeOnBackdrop />

            {/* Noms kebab-case : React 19 pose l'attribut */}
            <ar-dialog close-on-backdrop />
        </>
    );
}

Pour une valeur simple, les deux formes donnent le même résultat : chaque attribut d'Ariane est réfléchi, donc un sélecteur CSS comme [path="e1"] ou un querySelector fonctionne quelle que soit la forme employée.

Une fonction, un objet ou un booléen se passe directement dans une prop camelCase, à condition que le composant soit défini avant le rendu — ce que garantit l'import ci-dessus. Si la classe est enregistrée plus tard (autoloader CDN, import dynamique), le nom n'existe pas encore sur l'élément : React n'écrit alors qu'un attribut (que le navigateur met en minuscules, donc sans effet), ignore une fonction et convertit un objet en texte. Dans ce cas, et sur React 18 et antérieur (où toute prop est un attribut), passez par une ref :

// Propriété JS complexe : passe par une ref (toujours sur React 18 et antérieur ; avec React 19,
// seulement si le composant n'est pas encore défini au moment du rendu).
useEffect(() => {
    if (datepickerRef.current) {
        datepickerRef.current.isDateDisabled = (date) => date.getDay() === 0;
    }
}, []);

Typage TypeScript

#Lien direct vers cette section

Importez une fois, n'importe où dans le projet, le sous-chemin qui ne contient que des types. Il déclare le JSX des balises ar-* : propriétés, attributs en kebab-case et props d'événement onar-…, avec le type de detail.

// src/ariane.d.ts (ou n'importe quel module du projet)
import '@ariane-ui/core/react';
<ar-pagination
    current={page}
    total={10}
    onar-pagination-page-changed={(event) => setPage(event.detail.to)} // detail typé
/>

Les types portent sur les balises par défaut (ar-*) : avec un préfixe personnalisé, ils ne s'appliquent pas. addEventListener('ar-…') est typé dès que le paquet est importé, sans ce sous-chemin.

Rendu serveur et hydratation

#Lien direct vers cette section

Avec un rendu serveur suivi de hydrateRoot (par exemple dans Next.js), deux comportements sont à connaître. Ils ne concernent pas un rendu uniquement côté client.

  • Props posées tardivement. Sur un élément rendu par le serveur, onar-… et les propriétés JS ne sont posés qu'au premier rendu qui modifie la prop. Écoutez alors par ref, et posez les propriétés dans un useEffect :
// Rendu serveur puis hydratation : écoutez par ref plutôt que par onar-…
useEffect(() => {
    const el = ref.current;
    if (!el) return;
    const onChanged = (event) => setPage(event.detail.to);
    el.addEventListener('ar-pagination-page-changed', onChanged);
    return () => el.removeEventListener('ar-pagination-page-changed', onChanged);
}, []);
  • ar-charcounter défini avant l'hydratation. Le composant rattache le champ le premier, et l'événement natif change, à la perte de focus, déclenche un onChange de React en plus (même valeur que le précédent, une fois par saisie). Sans conséquence pour un gestionnaire qui ne fait que mettre à jour un état ; à prendre en compte s'il envoie une requête, sauvegarde ou mesure. Pour l'éviter, définissez les composants après l'hydratation :
// Définir les composants après l'hydratation
useEffect(() => {
    import('@ariane-ui/core');
}, []);

Pour plus d'information sur ce sujet, voir la documentation officielle de React sur les Custom Elements.

Next.js

#Lien direct vers cette section

Ariane ne prend pas officiellement en charge le rendu serveur (voir Compatibilité) : le HTML produit par Next.js contient les balises ar-* et leur contenu, mais pas leur shadow DOM. Les composants se définissent dans le navigateur, une fois le JavaScript chargé. Next.js étant un framework React, les événements, les propriétés, le typage et l'hydratation sont décrits dans la section React. Ne sont reprises ici que les particularités de l'App Router.

Charger la librairie

#Lien direct vers cette section

Chargez la librairie dans un Client Component monté dans le layout, par un import() dans un useEffect. Un import dans un Server Component ne définit rien dans le navigateur.

// components/ArianeLoader.tsx
'use client';

import { useEffect } from 'react';

export function ArianeLoader() {
    useEffect(() => {
        void import('@ariane-ui/core');
    }, []);
    return null;
}

// app/layout.tsx : <body><ArianeLoader />{children}</body>

Avec @ariane-ui/core/headless, enregistrez les classes dans le même effet. Gardez le customElements.get : en développement, StrictMode exécute l'effet deux fois.

'use client';

import { useEffect } from 'react';
import { ArAlert } from '@ariane-ui/core/headless';

export function ArianeLoader() {
    useEffect(() => {
        // En développement, StrictMode exécute l'effet deux fois : sans cette garde,
        // le second define() lève « has already been used with this registry ».
        if (!customElements.get('acme-alert')) customElements.define('acme-alert', ArAlert);
    }, []);
    return null;
}

Pour plus d'information sur ce sujet, voir la documentation officielle de Next.js sur les Server et Client Components.

Vue

#Lien direct vers cette section

Déclarez les tags ar-* comme éléments personnalisés dans la configuration Vite, pour que Vue ne tente pas de les résoudre comme ses propres composants :

// vite.config.ts — indique à Vue de ne pas traiter les tags ar-* comme des
// composants Vue
export default defineConfig({
    plugins: [
        vue({
            template: {
                compilerOptions: {
                    isCustomElement: (tag) => tag.startsWith('ar-'),
                },
            },
        }),
    ],
});

Dans le template :

<ar-pagination :current="1" :total="10" @ar-pagination-page-changed="onChanged" />
<ar-datepicker :isDateDisabled.prop="isSunday" />

Et le script associé :

function onChanged(event) {
    console.log(event.detail.to);
}

function isSunday(date) {
    return date.getDay() === 0;
}

Propriétés et attributs

#Lien direct vers cette section

Vue décide pour chaque liaison : si la clé existe sur l'élément, il affecte la propriété ; sinon il pose un attribut. Un attribut statique (path="e1") suit la même règle. Le modificateur .prop force la propriété, .attr (ou le préfixe ^) force l'attribut.

Un nom en kebab-case n'existe pas sur l'élément : :is-date-disabled="fn" devient un attribut texte, et la propriété reste undefined, sans erreur ni avertissement. Écrivez le nom en camelCase. .prop ne convertit pas le nom : :is-date-disabled.prop viserait une clé qui n'existe pas, il faut :isDateDisabled.prop ou ajouter .camel.

<!-- Attribut texte : la propriété reste undefined, sans erreur -->
<ar-datepicker :is-date-disabled="isSunday" />

<!-- Propriété -->
<ar-datepicker :isDateDisabled.prop="isSunday" />
<ar-datepicker :is-date-disabled.camel.prop="isSunday" />

Pour une fonction ou un objet, préférez .prop : il reste fiable même si le composant n'est pas encore défini au moment du rendu (chargement asynchrone, cas courant avec l'autoloader CDN), alors que la liaison sans modificateur ne devient une propriété que si la classe est déjà enregistrée. C'est aussi l'approche recommandée par Vue pour ce cas.

Un nombre se lie plutôt qu'il ne s'écrit : un attribut statique (total="10") est affecté à la propriété comme une chaîne, alors que :total="10" donne un nombre.

<!-- Nombre lié : la propriété vaut 10 -->
<ar-pagination :current="page" :total="10" />

<!-- Texte : la propriété vaut la chaîne "10" -->
<ar-pagination current="1" total="10" />

Chaque attribut d'Ariane est réfléchi : un sélecteur CSS comme [path="e1"] ou un querySelector fonctionne quelle que soit la forme employée. Les attributs ARIA se posent tels quels : par exemple role="alert" sur une ar-alert pour une annonce immédiate.

v-model

#Lien direct vers cette section

v-model fonctionne sur ar-datepicker : il lit et écrit value (une date au format yyyy-MM-dd). Le composant émet input et change sur l'hôte une fois value à jour. Sur un champ suivi par ar-charcounter, le v-model du champ lui-même met à jour le compteur, y compris pour un préremplissage ou un vidage fait par le code.

<ar-datepicker v-model="birthDate" name="birth" label="Date de naissance" />

<!-- Sans v-model -->
<ar-datepicker
    :value="birthDate"
    @ar-datepicker-input-change="(event) => (birthDate = event.detail.value ?? '')"
/>

L'événement ar-datepicker-input-change porte aussi valid (la date respecte-t-elle min et max) et valueAsDate, que v-model ne donne pas.

Typage TypeScript

#Lien direct vers cette section

Importez une fois, n'importe où dans le projet, le sous-chemin qui ne contient que des types. Il déclare les balises ar-* (GlobalComponents) avec leurs propriétés et leurs événements, dont le detail est typé. Dans un gabarit, @ar-dialog-hide est typé par onArDialogHide.

// src/env.d.ts (ou n'importe quel module du projet)
import '@ariane-ui/core/vue';
  • Par défaut, vue-tsc signale une valeur du mauvais type ou hors de l'union (variant="danger") et type l'événement reçu par un gestionnaire.
  • Avec strictTemplates, un nom d'événement mal orthographié est aussi signalé. Une propriété au nom mal orthographié ne l'est pas, dans les deux modes.
  • Avec strictTemplates, v-model sur une balise Ariane est signalé (vue-tsc le traite comme celui d'un composant Vue) : utilisez :value et l'événement.

Les types portent sur les balises par défaut (ar-*) : avec un préfixe personnalisé, ils ne s'appliquent pas.

Thème sombre

#Lien direct vers cette section

Le thème sombre manuel passe par data-theme="dark" sur :root. Les règles du thème sont dans une couche CSS (ariane.theme) : un color-scheme posé sur :root sans couche, comme celui du style.css d'un projet Vite, l'emporte et neutralise data-theme. Ne l'appliquez que lorsque data-theme est absent :

/* Le template Vite pose :root { color-scheme: light dark; } hors couche : */
:root:not([data-theme]) {
    color-scheme: light dark;
}

Pour plus d'information sur ce sujet, voir la documentation officielle de Vue sur les Custom Elements.

Angular

#Lien direct vers cette section

Ajoutez CUSTOM_ELEMENTS_SCHEMA aux schemas du composant (standalone) qui utilise des composants Ariane, pour qu'Angular accepte des tags inconnus de son compilateur de template :

import { Component, CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';

@Component({
    selector: 'app-root',
    standalone: true,
    schemas: [CUSTOM_ELEMENTS_SCHEMA],
    templateUrl: './app.component.html',
})
export class AppComponent {
    onChanged(event: CustomEvent<{ from: number; to: number }>) {
        console.log(event.detail.to);
    }

    isSunday = (date: Date) => date.getDay() === 0;
}

Dans le template :

<ar-pagination [current]="1" [total]="10" (ar-pagination-page-changed)="onChanged($event)">
</ar-pagination>
<ar-datepicker [isDateDisabled]="isSunday"></ar-datepicker>

Le binding [prop] affecte toujours une propriété JS (jamais un attribut), donc les valeurs complexes passent sans configuration additionnelle.

Événements et typage

#Lien direct vers cette section

Dès que le paquet est importé quelque part dans le projet (le import '@ariane-ui/core' de main.ts suffit), $event est typé : (ar-pagination-page-changed) reçoit un CustomEvent dont le detail est connu, sans cast. Les noms d'événements restent en kebab-case, tels que documentés.

<ar-pagination
    [total]="10"
    (ar-pagination-page-changed)="last = $event.detail.to"
></ar-pagination>

Sous CUSTOM_ELEMENTS_SCHEMA, Angular ne vérifie pas les liaisons de propriétés :[curent]="3" (faute de frappe) ou [total]="'dix'" compilent sans erreur. C'est une limite d'Angular, qu'aucun fichier de types ne lève.

Formulaires

#Lien direct vers cette section

ar-datepicker est un champ de formulaire : ajoutez ngDefaultControl pour que ngModel, formControl et [formField] (signal forms) le reconnaissent. Sans lui, Angular lève NG01203: No value accessor à l'exécution. Le composant émet input et change sur l'hôte une fois value à jour, ce que l'accesseur par défaut écoute ; la valeur échangée est une date yyyy-MM-dd.

<form #f="ngForm">
    <ar-datepicker label="Date" name="date" ngDefaultControl [(ngModel)]="date" required>
    </ar-datepicker>
</form>

<ar-datepicker label="Date" ngDefaultControl [formControl]="dateControl"></ar-datepicker>
  • Le modèle suit la valeur validée : à la sélection dans le calendrier, ou à la validation d'une saisie (Entrée, perte de focus), pas à chaque frappe.
  • setValue(), reset() et la réinitialisation du formulaire natif sont répercutés dans le champ.
  • Un ar-charcounter suit le champ qu'il observe, y compris lorsque la valeur est écrite par programme (ngModel, setValue()).

État ouvert/fermé

#Lien direct vers cette section

Un composant peut se fermer seul (Échap, clic en dehors, bouton de fermeture) sans que l'état d'Angular le sache. Si [open] est lié à un signal resté à true, un nouveau clic sur « Ouvrir » réécrit true : Angular ne réaffecte pas une liaison inchangée, et le composant ne se rouvre pas. Resynchronisez l'état sur l'événement de fermeture. Le motif vaut pour ar-dialog, ar-collapse, ar-dropdown et ar-datepicker (ar-*-hidden).

<button (click)="opened.set(true)">Ouvrir</button>
<ar-dialog [open]="opened()" (ar-dialog-hidden)="opened.set(false)">…</ar-dialog>

Shadow DOM et budget

#Lien direct vers cette section

Un composant Angular en ViewEncapsulation.ShadowDom est un shadow DOM applicatif : les tokens du thème du document le traversent, pas les règles ::part() (voir Dans un shadow DOM applicatif). Importer la feuille du thème dans le styleUrl du composant fonctionne, mais le ng build de production échoue sur le budget anyComponentStyle (8 kB en erreur par défaut) : la palette seule en pèse plus de 25. Adoptez plutôt une feuille partagée, ou relevez le budget dans angular.json :

"budgets": [
    { "type": "anyComponentStyle", "maximumWarning": "30kB", "maximumError": "60kB" }
]

Pour plus d'information sur ce sujet, voir la documentation officielle d'Angular sur les Custom Elements.

Svelte

#Lien direct vers cette section

Avec Svelte 5, importez le paquet, puis utilisez les composants directement dans le markup. Svelte ne type pas les événements d'un élément personnalisé : annotez le paramètre du gestionnaire. HTMLElementEventMap['ar-pagination-page-changed'] donne un detail typé, dès que le paquet est importé.

<script lang="ts">
    import '@ariane-ui/core';

    let page = $state(1);

    function onChanged(event: HTMLElementEventMap['ar-pagination-page-changed']) {
        page = event.detail.to;
    }
</script>

<ar-pagination current={page} total={10} onar-pagination-page-changed={onChanged} />
<ar-datepicker isDateDisabled={(date) => date.getDay() === 0} />

Si le composant est défini après le rendu (autoloader CDN, import dynamique), une fonction (isDateDisabled) ou un booléen en camelCase (closeOnBackdrop={true}) sont posés comme attributs en minuscules et perdus. Affectez-les alors en propriété une fois le composant défini :

<script lang="ts">
    import { onMount } from 'svelte';

    let picker: HTMLElement & { isDateDisabled?: (date: Date) => boolean };

    onMount(async () => {
        await customElements.whenDefined('ar-datepicker');
        picker.isDateDisabled = (date) => date.getDay() === 0;
    });
</script>

<ar-datepicker bind:this={picker} />

bind:value est refusé sur un élément personnalisé. ar-datepicker émet input et change sur l'hôte une fois value à jour : liez value et écoutez l'événement.

<ar-datepicker
    name="date"
    value={date}
    oninput={(event) => (date = event.currentTarget.value)}
/>

Pour plus d'information sur ce sujet, voir la documentation officielle de Svelte sur les Custom Elements.