Skip to content

DsfrButton

🌟 Introduction

Le bouton est un élément d’interaction avec l’interface permettant à l’utilisateur d’effectuer une action.

Le DsfrButton est un composant Vue élégant et réutilisable, conçu pour simplifier la création de boutons personnalisés. Il intègre des tailles ajustables, des icônes optionnelles et un gestionnaire de clics, tout en respectant le style de DSFR. Son utilisation est simple, avec une grande flexibilité pour s'adapter à différents contextes.

🏅 La documentation sur l’alerte sur le DSFR

La story sur l’alerte sur le storybook de VueDsfr

📐 Structure

Les boutons sont composés de :

  • Un label - obligatoire, soit en utilisant la prop label soit en utilisant le slot par défaut ;
  • Une icône, pouvant être modifiée (voir les icônes disponibles) - optionnelle.

🛠️ Props

NomTypeDéfautObligatoireDescription
size'sm' | 'md' | 'lg''md'Taille du bouton. Peut être 'sm', 'md', ou 'lg'.
iconstring | objectundefinedIcône à afficher dans le bouton. Peut être un nom ou une configuration d'icône.
labelstringundefinedÉtiquette textuelle du bouton. Si le label est laissé à undefined, le slot par défaut doit contenir du texte !
onClickFunction() => {}Fonction appelée lors du clic sur le bouton.

📡 Évenements

  • click : Émis lorsque le bouton est cliqué.

🧩 Slots

  • default : Emplacement pour le contenu personnalisé du bouton. Inséré dans <button class="fr-btn"><span">.

✨ Les groupes de boutons

Cf. documentation dédiée

📝 Exemples

Un bouton large avec une icône 'maison' à gauche et le texte 'Accueil' :

vue
<script lang="ts" setup>
import { ref } from 'vue'

import DsfrButton from '../DsfrButton.vue'

const nb = ref(0)
const handleClick = () => {
  nb.value++
}
</script>

<template>
  <div class="fr-container fr-my-2w">
    <DsfrButton
      size="lg"
      icon="fr-icon-home-4-fill"
      label="Accueil"
      @click="handleClick()"
    />
    Cliqué {{ nb }} fois
  </div>
</template>

Un petit bouton avec le texte 'Aller plus loin', du contenu supplémentaire dans le slot par défaut, et une icône à droite :

vue
<script lang="ts" setup>
import { ref } from 'vue'

import DsfrButton from '../DsfrButton.vue'

const nb = ref(0)
const handleClick = () => {
  nb.value++
}
</script>

<template>
  <div class="fr-container fr-my-2w">
    <DsfrButton
      size="sm"
      label="Aller plus loin"
      icon="fr-icon-arrow-right-line"
      icon-right
      @click="handleClick()"
    >
      (Contenu <em>supplémentaire</em>)
    </DsfrButton>
    Cliqué {{ nb }} fois
  </div>
</template>

📝 (Presque) toutes les variantes 🌈 de boutons

vue
<script lang="ts" setup>
import DsfrButton from '../DsfrButton.vue'
</script>

<template>
  <div class="fr-container fr-my-2w">
    <div class="flex  gap-4  flex-end  w-full">
      <div class="flex  flex-col  gap-4  flex-end  w-full">
        <DsfrButton
          label="Bouton primaire"
          primary
        />
        <DsfrButton
          label="Bouton secondaire"
          secondary
        />
        <DsfrButton
          label="Bouton tertiaire"
          tertiary
        />
        <DsfrButton
          label="Bouton tertiaire sans bordure"
          tertiary
          no-outline
        />
      </div>
      <div class="flex  flex-col  gap-4  flex-end  w-full">
        <DsfrButton
          label="Petit bouton primaire"
          size="sm"
          primary
        />
        <DsfrButton
          label="Petit bouton secondaire"
          size="sm"
          secondary
        />
        <DsfrButton
          label="Petit bouton tertiaire"
          size="sm"
          tertiary
        />
        <DsfrButton
          label="Petit bouton tertiaire sans bordure"
          size="sm"
          tertiary
          no-outline
        />
      </div>
    </div>
    <div class="flex  flex-col  gap-4  flex-end  w-full">
      <DsfrButton
        label="Gros bouton primaire"
        size="lg"
        primary
      />
      <DsfrButton
        label="Gros bouton secondaire"
        size="lg"
        secondary
      />
      <DsfrButton
        label="Gros bouton tertiaire"
        size="lg"
        tertiary
      />
      <DsfrButton
        label="Gros bouton tertiaire sans bordure"
        size="lg"
        tertiary
        no-outline
      />
      <div class="flex  flex-col  gap-4  flex-end  w-full">
        <DsfrButton
          label="Bouton primaire avec icône DSFR"
          icon="fr-icon-moon-line"
          primary
        />
        <DsfrButton
          label="Bouton secondaire avec icône DSFR"
          icon="fr-icon-moon-line"
          secondary
        />
        <DsfrButton
          label="Bouton tertiaire avec icône DSFR"
          icon="fr-icon-moon-line"
          tertiary
        />
        <DsfrButton
          label="Bouton tertiaire sans bordure avec icône DSFR"
          icon="fr-icon-moon-line"
          tertiary
          no-outline
        />
      </div>
      <div class="flex  flex-col  gap-4  flex-end  w-full">
        <DsfrButton
          label="Bouton primaire avec icône oh-vue-icon"
          icon="ri-moon-line"
          primary
        />
        <DsfrButton
          label="Bouton secondaire avec icône oh-vue-icon animée (spin)"
          :icon="{ name: 'ri-refresh-line', animation: 'spin' }"
          secondary
        />
        <DsfrButton
          label="Bouton tertiaire avec icône oh-vue-icon animée (spin-pulse)"
          :icon="{ name: 'ri-moon-line', animation: 'spin-pulse' }"
          tertiary
        />
        <DsfrButton
          label="Bouton 3re ss bordure avec icône OVI colorée avec couleur DSFR"
          title="Bouton tertiaire sans bordure avec icône oh-vue-icon colorée avec une couleur du DSFR"
          :icon="{ name: 'ri-moon-line', fill: 'var(--success-425-625)' }"
          tertiary
          no-outline
        />
      </div>
      <div class="flex  gap-4  flex-end  w-full">
        <div class="flex  flex-col  gap-4  flex-end  w-full">
          <DsfrButton
            label="Bouton primaire icône seulement avec icône DSFR"
            icon="fr-icon-moon-line"
            icon-only
            primary
          />
          <DsfrButton
            label="Bouton secondaire avec icône DSFR"
            icon="fr-icon-moon-line"
            icon-only
            secondary
          />
          <DsfrButton
            label="Bouton tertiaire avec icône DSFR animée"
            icon="fr-icon-moon-line"
            icon-only
            tertiary
          />
          <DsfrButton
            label="Bouton 3re ss bordure avec icône OVI colorée avec couleur DSFR"
            title="Bouton tertiaire sans bordure avec icône DSFR colorée avec une couleur du DSFR"
            icon="fr-icon-moon-line"
            icon-only
            tertiary
            no-outline
          />
        </div>
        <div class="flex  flex-col  gap-4  flex-end  w-full">
          <DsfrButton
            label="Bouton primaire icône seulement avec icône oh-vue-icon"
            icon="ri-moon-line"
            icon-only
            primary
          />
          <DsfrButton
            label="Bouton secondaire avec icône oh-vue-icon animée (spin)"
            :icon="{ name: 'ri-refresh-line', animation: 'spin' }"
            icon-only
            secondary
          />
          <DsfrButton
            label="Bouton tertiaire avec icône oh-vue-icon animée (spin-pulse)"
            :icon="{ name: 'ri-moon-line', animation: 'spin-pulse' }"
            icon-only
            tertiary
          />
          <DsfrButton
            label="Bouton 3re ss bordure avec icône OVI colorée avec couleur DSFR"
            title="Bouton tertiaire sans bordure avec icône oh-vue-icon colorée avec une couleur du DSFR"
            :icon="{ name: 'ri-moon-line', fill: 'var(--success-425-625)' }"
            icon-only
            tertiary
            no-outline
          />
        </div>
      </div>
    </div>
  </div>
</template>

⚙️ Code source du composant

vue
<script lang="ts" setup>
import { computed, ref } from 'vue'
import { OhVueIcon as VIcon } from 'oh-vue-icons'

import type { DsfrButtonProps } from './DsfrButton.types'

export type { DsfrButtonProps }

const props = withDefaults(defineProps<DsfrButtonProps>(), {
  size: 'md',
  icon: undefined,
  label: undefined,
  onClick: () => undefined,
})

const sm = computed(() => ['sm', 'small'].includes(props.size))
const md = computed(() => ['md', 'medium'].includes(props.size))
const lg = computed(() => ['lg', 'large'].includes(props.size))

const btn = ref<{ focus: () => void } | null>(null)
const focus = () => {
  btn.value?.focus()
}
defineExpose({ focus })

const dsfrIcon = computed(() => typeof props.icon === 'string' && props.icon.startsWith('fr-icon-'))
const defaultScale = computed(() => props.iconOnly ? 1.25 : 0.8325)
const iconProps = computed(() => typeof props.icon === 'string'
  ? { scale: defaultScale.value, name: props.icon }
  : { scale: defaultScale.value, ...props.icon },
)
</script>

<template>
  <button
    ref="btn"
    class="fr-btn"
    :class="{
      'fr-btn--secondary': secondary && !tertiary,
      'fr-btn--tertiary': tertiary && !secondary && !noOutline,
      'fr-btn--tertiary-no-outline': tertiary && !secondary && noOutline,
      'fr-btn--sm': sm,
      'fr-btn--md': md,
      'fr-btn--lg': lg,
      'fr-btn--icon-right': !iconOnly && dsfrIcon && iconRight,
      'fr-btn--icon-left': !iconOnly && dsfrIcon && !iconRight,
      'inline-flex': !dsfrIcon,
      reverse: iconRight && !dsfrIcon,
      'justify-center': !dsfrIcon && iconOnly,
      [icon as string]: dsfrIcon,
    }"
    :title="iconOnly ? label : undefined"
    :disabled="disabled"
    :aria-disabled="disabled"
    :style="(!dsfrIcon && iconOnly) ? { 'padding-inline': '0.5rem' } : {}"
    @click="onClick ? onClick($event) : () => {}"
  >
    <VIcon
      v-if="icon && !dsfrIcon"
      v-bind="iconProps"
    />
    <span v-if="!iconOnly">
      {{ label }}
      <!-- @slot Slot par défaut pour le contenu du bouton. Sera dans `<button class="fr-btn"><span">` -->
      <slot />
    </span>
  </button>
</template>

<style scoped>
.inline-flex {
  display: inline-flex;
  align-items: center;
  gap: 0.5rem;
}

.reverse {
  flex-direction: row-reverse;
}
</style>
ts
import type { ButtonHTMLAttributes } from 'vue'
import type { OhVueIcon as VIcon } from 'oh-vue-icons'

export type DsfrButtonProps = {
  disabled?: boolean
  label?: string
  secondary?: boolean
  tertiary?: boolean
  iconRight?: boolean
  iconOnly?: boolean
  noOutline?: boolean
  size?: 'sm' | 'small' | 'lg' | 'large' | 'md' | 'medium' | '' | undefined
  icon?: string | InstanceType<typeof VIcon>['$props']
  onClick?: ($event: MouseEvent) => void
}

export type DsfrButtonGroupProps = {
  buttons?: (DsfrButtonProps & ButtonHTMLAttributes)[]
  reverse?: boolean
  equisized?: boolean
  iconRight?: boolean
  align?: 'right' | 'center' | '' | undefined
  inlineLayoutWhen?: 'always' | 'never' | 'sm' | 'small' | 'lg' | 'large' | 'md' | 'medium' | '' | true | undefined
  size?: 'sm' | 'small' | 'lg' | 'large' | 'md' | 'medium' | undefined
}

Et voilà ! Notre DsfrButton est prêt à illuminer votre interface avec style et fonctionnalité. N'oubliez pas d'appuyer sur ces boutons avec panache ! 🚀