Skip to content

FloatButton

Composant FloatButton pour placer un bouton flottant dans le coin de l'écran avec support des slots, badges, tooltips et accessibilité complète selon les normes W3C.

Exemples d'utilisation

FloatButton de base

Bouton flottant simple

Aperçu du bouton flottant

vue
<script setup>
import { PlusIcon } from '@heroicons/vue/24/outline'
</script>

<template>
  <SuFloatButton 
    :icon="'PlusIcon'"
    label="Ajouter un élément"
    position="right"
  />
</template>

Avec badge

Bouton avec badge de notification

Bouton avec badge "3"

vue
<script setup>
import { ChatBubbleLeftIcon } from '@heroicons/vue/24/outline'
</script>

<template>
  <SuFloatButton 
    :icon="'ChatBubbleLeftIcon'"
    label="Messages"
    badge="3"
    position="right"
  />
</template>

Comme lien externe

Bouton flottant comme lien

Lien vers l'aide externe

vue
<script setup>
import { QuestionMarkCircleIcon } from '@heroicons/vue/24/outline'
</script>

<template>
  <SuFloatButton 
    :icon="'QuestionMarkCircleIcon'"
    label="Centre d'aide"
    href="https://help.example.com"
    target="_blank"
    position="left"
    tooltip-position="right"
  />
</template>

Avec ButtonGroup

Bouton avec groupe d'actions

Bouton principal avec actions secondaires

vue
<script setup>
import { PlusIcon, CogIcon, HeartIcon, ShareIcon } from '@heroicons/vue/24/outline'
</script>

<template>
  <SuFloatButton 
    :icon="'PlusIcon'"
    label="Actions rapides"
    position="right"
  >
    <SuButtonGroup gap="sm" variant="secondary" size="sm">
      <SuButton :icon="'CogIcon'" iconDisplay="only" aria-label="Paramètres" />
      <SuButton :icon="'HeartIcon'" iconDisplay="only" aria-label="Favoris" />
      <SuButton :icon="'ShareIcon'" iconDisplay="only" aria-label="Partager" />
    </SuButtonGroup>
  </SuFloatButton>
</template>

Avec LinkGroup

Bouton avec groupe de liens

Bouton d'aide avec liens de support

vue
<script setup>
import { QuestionMarkCircleIcon, PhoneIcon, EnvelopeIcon, ChatBubbleLeftIcon } from '@heroicons/vue/24/outline'
</script>

<template>
  <SuFloatButton 
    :icon="'QuestionMarkCircleIcon'"
    label="Besoin d'aide ?"
    position="left"
    tooltip-position="right"
  >
    <SuLinkGroup gap="sm" variant="secondary" size="sm">
      <SuLink href="/help" :icon="'QuestionMarkCircleIcon'" iconDisplay="left">Centre d'aide</SuLink>
      <SuLink href="tel:+33123456789" :icon="'PhoneIcon'" iconDisplay="left">Téléphone</SuLink>
      <SuLink href="mailto:support@example.com" :icon="'EnvelopeIcon'" iconDisplay="left">Email</SuLink>
      <SuLink href="/chat" :icon="'ChatBubbleLeftIcon'" iconDisplay="left">Chat</SuLink>
    </SuLinkGroup>
  </SuFloatButton>
</template>

Positions

Positions disponibles

Boutons dans les deux coins

vue
<template>
  <!-- Bouton à droite -->
  <SuFloatButton 
    :icon="'PlusIcon'"
    label="Ajouter"
    position="right"
  />
  
  <!-- Bouton à gauche -->
  <SuFloatButton 
    :icon="'CogIcon'"
    label="Paramètres"
    position="left"
    tooltip-position="right"
    variant="secondary"
  />
</template>

Tailles

Tailles disponibles

Différentes tailles

vue
<template>
  <SuFloatButton size="sm" :icon="'PlusIcon'" label="Small" />
  <SuFloatButton size="md" :icon="'CogIcon'" label="Medium" />
  <SuFloatButton size="lg" :icon="'HeartIcon'" label="Large" />
</template>

Offset personnalisé

Position personnalisée avec offset

Bouton avec offset personnalisé

vue
<template>
  <SuFloatButton 
    :icon="'PlusIcon'"
    label="Position personnalisée"
    position="right"
    :offset="{ x: 60, y: 60 }"
  />
</template>

API

Props

PropTypeDefaultDescription
position'left' | 'right''right'Position du bouton flottant
offset{ x?: number, y?: number }{ x: 24, y: 24 }Décalage personnalisé en pixels
size'sm' | 'md' | 'lg''md'Taille du bouton flottant
iconComponentundefinedIcône du bouton
labelstringundefinedLabel du bouton (affiché dans le tooltip)
hrefstringundefinedURL de destination (transforme en lien)
target'_blank' | '_self' | '_parent' | '_top'undefinedCible du lien
relstringundefinedRelation du lien
badgestringundefinedTexte du badge affiché en haut à droite
variant'primary' | 'secondary' | 'outline' | 'ghost''primary'Variante du bouton
disabledbooleanfalseDésactive le bouton
loadingbooleanfalseÉtat de chargement
zIndexnumber1000Z-index du bouton
hideOnScrollbooleanfalseMasquer lors du scroll vers le bas
showTooltipbooleantrueAfficher le tooltip au survol
tooltipPosition'top' | 'bottom' | 'left' | 'right''left'Position du tooltip

Attributs d'accessibilité

PropTypeDefaultDescription
ariaLabelstringundefinedLabel accessible (utilise label par défaut)
ariaDescribedBystringundefinedID de l'élément de description
rolestringundefinedRôle ARIA personnalisé
tabIndexnumber0Ordre de tabulation

Events

EventTypeDescription
@click(event: MouseEvent) => voidÉmis lors du clic sur le bouton
@focus(event: FocusEvent) => voidÉmis lors du focus sur le bouton
@blur(event: FocusEvent) => voidÉmis lors de la perte de focus

Slots

SlotDescription
defaultContenu affiché au-dessus du bouton (LinkGroup ou ButtonGroup)

Fonctionnalités avancées

🎯 Positionnement intelligent

Le composant gère automatiquement :

  • Position fixe : Reste visible lors du scroll
  • Offset personnalisé : Ajustement précis de la position
  • Zones sûres : Support des encoches iPhone (safe-area-inset)
  • Responsive : Ajustement automatique sur mobile

🏷️ Badge de notification

Le badge offre un feedback visuel pour :

  • Notifications : Nombre de messages non lus
  • Alertes : Indicateurs d'état
  • Compteurs : Éléments en attente
  • Animation : Pulsation pour attirer l'attention

💬 Tooltip intelligent

Le tooltip s'adapte automatiquement :

  • Position : S'ajuste selon la position du bouton
  • Contenu : Utilise le label comme texte
  • Accessibilité : Lié via aria-describedby
  • Responsive : Taille adaptée sur mobile

📱 Comportement responsive

  • Mobile : Offsets réduits automatiquement (16px au lieu de 24px)
  • Zones sûres : Support des encoches et barres de navigation
  • Tooltip : Taille réduite sur petits écrans
  • Slot : Largeur maximale adaptée à l'écran

Accessibilité

Le composant FloatButton respecte les normes WCAG 2.1 AA :

✅ Fonctionnalités d'accessibilité

  • Navigation au clavier : Support des touches Entrée et Espace
  • Focus visible : Indicateur de focus clair et contrasté
  • Labels accessibles : aria-label automatique ou personnalisé
  • Tooltips liés : Association via aria-describedby
  • Badges accessibles : Annonce vocale du contenu du badge
  • Contraste : Ratios conformes WCAG AA (4.5:1 minimum)
  • Tailles minimales : Respecte les tailles minimales de cible tactile (44px)
  • Mode sombre : Contraste adapté automatiquement
  • Contraste élevé : Support de prefers-contrast: high
  • Réduction d'animation : Respect de prefers-reduced-motion

🎯 Bonnes pratiques

vue
<template>
  <!-- Bouton flottant avec label accessible -->
  <SuFloatButton 
    :icon="'PlusIcon'"
    label="Ajouter un nouvel élément"
    position="right"
  />

  <!-- Lien externe avec sécurité -->
  <SuFloatButton 
    :icon="'QuestionMarkCircleIcon'"
    label="Ouvrir le centre d'aide"
    href="https://help.example.com"
    target="_blank"
    position="left"
    tooltip-position="right"
  />

  <!-- Avec badge et description -->
  <SuFloatButton 
    :icon="'ChatBubbleLeftIcon'"
    label="Messages non lus"
    badge="5"
    aria-describedby="messages-help"
  />
  <div id="messages-help" class="sr-only">
    Vous avez 5 messages non lus
  </div>

  <!-- Avec slot et rôle approprié -->
  <SuFloatButton 
    :icon="'CogIcon'"
    label="Menu d'actions"
    role="button"
    aria-haspopup="true"
  >
    <ButtonGroup role="menu" aria-label="Actions rapides">
	    <Button role="menuitem">Action 1</Button>
	    <Button role="menuitem">Action 2</Button>
    </ButtonGroup>
  </SuFloatButton>
</template>
ToucheAction
TabNaviguer vers/depuis le bouton flottant
EntréeActiver le bouton ou suivre le lien
EspaceActiver le bouton
ÉchapFermer le slot (si ouvert)

Exemples d'usage avancés

Chat flottant avec actions

vue
<script setup>
import { ref } from 'vue'
import { ChatBubbleLeftIcon, PhoneIcon, EnvelopeIcon, QuestionMarkCircleIcon } from '@heroicons/vue/24/outline'

const unreadMessages = ref(3)

const handleChatClick = () => {
  // Ouvrir la fenêtre de chat
  console.log('Ouvrir le chat')
}
</script>

<template>
  <SuFloatButton 
    :icon="'ChatBubbleLeftIcon'"
    label="Support client"
    :badge="unreadMessages.toString()"
    position="right"
    @click="handleChatClick"
  >
    <LinkGroup gap="sm" variant="muted" size="sm">
      <Link href="/help" :icon="'QuestionMarkCircleIcon'" iconDisplay="left">
        Centre d'aide
      </Link>
      <Link href="tel:+33123456789" :icon="'PhoneIcon'" iconDisplay="left">
        Téléphone
      </Link>
      <Link href="mailto:support@example.com" :icon="'EnvelopeIcon'" iconDisplay="left">
        Email
      </Link>
    </LinkGroup>
  </SuFloatButton>
</template>

Actions rapides avec toolbar

vue
<script setup>
import { ref } from 'vue'
import { PlusIcon, CogIcon, HeartIcon, ShareIcon, BookmarkIcon } from '@heroicons/vue/24/outline'

const showActions = ref(false)

const toggleActions = () => {
  showActions.value = !showActions.value
}

const handleAction = (action: string) => {
  console.log(`Action: ${action}`)
  showActions.value = false
}
</script>

<template>
  <SuFloatButton 
    :icon="'PlusIcon'"
    label="Actions rapides"
    position="right"
    :aria-expanded="showActions"
    @click="toggleActions"
  >
    <ButtonGroup 
      v-if="showActions"
      gap="sm" 
      variant="secondary" 
      size="sm"
      role="toolbar"
      aria-label="Actions rapides"
    >
      <Button 
        :icon="'CogIcon'" 
        iconDisplay="only" 
        aria-label="Paramètres"
        @click="handleAction('settings')"
      />
      <Button 
        :icon="'HeartIcon'" 
        iconDisplay="only" 
        aria-label="Ajouter aux favoris"
        @click="handleAction('favorite')"
      />
      <Button 
        :icon="'ShareIcon'" 
        iconDisplay="only" 
        aria-label="Partager"
        @click="handleAction('share')"
      />
      <Button 
        :icon="BookmarkIcon" 
        iconDisplay="only" 
        aria-label="Marquer"
        @click="handleAction('bookmark')"
      />
    </ButtonGroup>
  </SuFloatButton>
</template>

Bouton de retour en haut

vue
<script setup>
import { ref, onMounted, onUnmounted } from 'vue'
import { ArrowUpIcon } from '@heroicons/vue/24/outline'

const showBackToTop = ref(false)

const handleScroll = () => {
  showBackToTop.value = window.scrollY > 300
}

const scrollToTop = () => {
  window.scrollTo({
    top: 0,
    behavior: 'smooth'
  })
}

onMounted(() => {
  window.addEventListener('scroll', handleScroll, { passive: true })
})

onUnmounted(() => {
  window.removeEventListener('scroll', handleScroll)
})
</script>

<template>
  <SuFloatButton 
    v-if="showBackToTop"
    :icon="ArrowUpIcon"
    label="Retour en haut"
    position="right"
    variant="outline"
    :offset="{ x: 24, y: 100 }"
    @click="scrollToTop"
  />
</template>

Support client complet

vue
<script setup>
import { ref } from 'vue'
import { 
  ChatBubbleLeftIcon, 
  PhoneIcon, 
  EnvelopeIcon, 
  QuestionMarkCircleIcon,
  DocumentTextIcon,
  VideoCameraIcon
} from '@heroicons/vue/24/outline'

const supportOptions = [
  { 
    href: '/help', 
    icon: QuestionMarkCircleIcon, 
    label: 'Centre d\'aide',
    description: 'Documentation et FAQ'
  },
  { 
    href: '/docs', 
    icon: DocumentTextIcon, 
    label: 'Documentation',
    description: 'Guides techniques'
  },
  { 
    href: '/video-tutorials', 
    icon: VideoCameraIcon, 
    label: 'Tutoriels vidéo',
    description: 'Apprentissage visuel'
  },
  { 
    href: 'tel:+33123456789', 
    icon: PhoneIcon, 
    label: 'Téléphone',
    description: 'Support direct'
  },
  { 
    href: 'mailto:support@example.com', 
    icon: EnvelopeIcon, 
    label: 'Email',
    description: 'Support par email'
  }
]

const unreadTickets = ref(2)
</script>

<template>
  <SuFloatButton 
    :icon="'ChatBubbleLeftIcon'"
    label="Support client"
    :badge="unreadTickets > 0 ? unreadTickets.toString() : undefined"
    position="left"
    tooltipPosition="right"
  >
    <div class="support-menu">
      <div class="support-header">
        <h4>Comment pouvons-nous vous aider ?</h4>
      </div>
      
      <LinkGroup gap="sm" variant="muted" size="sm">
        <Link 
          v-for="option in supportOptions"
          :key="option.href"
          :href="option.href"
          :icon="option.icon"
          iconDisplay="left"
          :title="option.description"
        >
          {{ option.label }}
        </Link>
      </LinkGroup>
    </div>
  </SuFloatButton>
</template>

<style scoped>
.support-menu {
  display: flex;
  flex-direction: column;
  gap: 0.75rem;
  min-width: 200px;
}

.support-header {
  padding-bottom: 0.5rem;
  border-bottom: 1px solid #e5e7eb;
}

.support-header h4 {
  margin: 0;
  font-size: 0.875rem;
  font-weight: 600;
  color: #374151;
}
</style>

Cas d'usage courants

💬 Support et aide

  • Chat en direct : Accès rapide au support client
  • Centre d'aide : Lien vers la documentation
  • Contact : Téléphone, email, formulaire

➕ Actions principales

  • Ajouter : Créer un nouvel élément
  • Composer : Rédiger un message, article
  • Créer : Nouveau projet, document

🔧 Outils et paramètres

  • Paramètres : Accès rapide aux réglages
  • Outils : Fonctionnalités avancées
  • Raccourcis : Actions fréquentes

📱 Navigation mobile

  • Menu : Navigation principale sur mobile
  • Retour en haut : Scroll vers le haut de page
  • Partage : Partage rapide de contenu

Bonnes pratiques

✅ À faire

  • Utiliser un label descriptif pour l'accessibilité
  • Limiter à 1-2 boutons flottants par page
  • Placer les actions principales à droite (plus accessible)
  • Utiliser des badges pour les notifications importantes
  • Tester sur mobile et avec les lecteurs d'écran

❌ À éviter

  • Masquer des fonctionnalités essentielles dans le slot
  • Utiliser trop de boutons flottants (pollution visuelle)
  • Oublier le label pour l'accessibilité
  • Placer le bouton sur du contenu important
  • Utiliser des animations excessives

Performance

🚀 Optimisations intégrées

  • Événements passifs : Scroll listeners optimisés
  • Transitions CSS : Animations hardware-accelerated
  • Lazy rendering : Le slot n'est rendu que si nécessaire
  • Cleanup automatique : Suppression des event listeners

📊 Impact minimal

  • CSS généré : ~2KB gzippé
  • JavaScript : Logique légère et optimisée
  • Rendu : Pas d'impact sur les performances de la page
  • Accessibilité : Aucun compromis sur les performances

Publié sous licence MIT.