2022 / 08 / 01
A confirmation dialog component with Vue 3 and Tailwind CSS

Let's write a reusable confirmation dialog with Vue and Tailwind CSS.

frontend
vue 3
typescript
tailwindcss
demo

Prerequisites

We’ll use the <BaseModal> component we wrote in:
A basic modal component with Vue 3 and Tailwind CSS.

Desired API

Let’s suppose we already have a <ConfirmationDialog> component, and we’d like to use it like this:

Dialog 1

<script setup lang="ts">
import { ref } from 'vue'
import ConfirmationDialog from '@/components/modals/ConfirmationDialog.vue'

const showDialog = ref(false)

function handleResult(value: boolean) {
  showDialog.value = false
  // Do something with `value`
  console.log('value', value)
}
</script>

<template>
  <ConfirmationDialog :show="showDialog1" @result="handleResult" />
</template>

Here is where you can think about and define the API you want to provide for users of your component.

Is it better to have a @result event that gets passed a boolean?
Or would it better to have two type of events emitted like @confirm and @cancel?

That depends on your taste. For now I’ll go with the simple approach of having a single event that we can handle on the host.

Customizability

Ideally we’d like to have a default implementation that’s good enough for the majority of use cases; but then we’d like to have the option to provide alternative markup for the different parts that compose our component.

This is the perfect use case for slots in Vue.
Let’s say we want to customize the title and body, maybe with something like this:

Dialog 2

<script setup lang="ts">
import { ref } from 'vue'
import ConfirmationDialog from '@/components/modals/ConfirmationDialog.vue'

const showDialog = ref(false)

function handleResult(value: boolean) {
  showDialog.value = false
  // Do something with `value`
  console.log('value', value)
}
</script>

<template>
  <ConfirmationDialog :show="showDialog" @result="handleResult">
    <template #title="{ emitResult }">
      <div class="flex justify-between">
        <div class="text-xl font-semibold tracking-wide">
          Cool confirmation
        </div>
        <CloseIcon
          class="w-6 cursor-pointer text-pink-600"
          @click="emitResult(false)"
        />
      </div>
    </template>

    <template #body>
      <div class="py-4 text-sm">Are you super duper sure about THIS?</div>
    </template>
  </ConfirmationDialog>
</template>

Again, if you use a named slot for the body or the default slot comes down to taste and the type of API you want to provide.

In my case I will provide named slots for all the parts, and a default slot to override the whole component.

What about customizing the dialog buttons?
Another easy one:

Dialog 3

<script setup lang="ts">
import { ref } from 'vue'
import ConfirmationDialog from '@/components/modals/ConfirmationDialog.vue'

const showDialog = ref(false)

function handleResult(value: boolean) {
  showDialog.value = false
  // Do something with `value`
  console.log('value', value)
}
</script>

<template>
  <ConfirmationDialog :show="showDialog3" @result="handleResult">
    <template #actions="{ emitResult }">
      <div class="flex gap-3">
        <button
          class="border border-red-400 bg-gray-200 px-3 font-semibold uppercase"
          @click="emitResult(false)"
        >
          No way!
        </button>

        <button
          class="border border-cyan-400 bg-gray-200 px-3 font-semibold uppercase"
          @click="emitResult(true)"
        >
          Yes, please
        </button>
      </div>
    </template>
  </ConfirmationDialog>
</template>
A first level of customizability could be achieved by passing props to change the title or the confirmation question, but even in this case I'd just provide slots for those parts if needed. For example: ```html
Please confirm
``` In this way you can change the title and still keep the default styling.

But, what if we want to completely override the design of the confirmation dialog?
WDYT? :thinking:

Dialog 4

<script setup lang="ts">
import { ref } from 'vue'
import ConfirmationDialog from '@/components/modals/ConfirmationDialog.vue'

const showDialog = ref(false)

function handleResult(value: boolean) {
  showDialog.value = false
  // Do something with `value`
  console.log('value', value)
}
</script>

<template>
  <ConfirmationDialog
    v-slot="{ emitResult }"
    :show="showDialog"
    @result="handleResult"
  >
    <div class="bg-black p-3 text-lg font-bold text-white">
      A very custom title
    </div>

    <div class="max-w-sm p-4">
      <div class="text-sm">
        Do you REALLY agree to the terms of the agreement presented in here?
      </div>

      <div class="flex justify-between pt-5">
        <button
          class="bg-pink-600 px-4 py-2 text-sm font-bold text-white"
          @click="emitResult(false)"
        >
          NOPE NOPE NOPE
        </button>

        <button
          class="bg-sky-600 px-4 py-2 text-sm font-bold text-white"
          @click="emitResult(true)"
        >
          YEP
        </button>
      </div>
    </div>
  </ConfirmationDialog>
</template>

So many possibilities! :thinking: :dizzy:

Source code

Let me show you the implementation I used for the four demos above.
src/components/modals/ConfirmationDialog.vue:

<script setup lang="ts">
import BaseModal from '@/components/modals/BaseModal.vue'

defineProps<{
  show: boolean
}>()

const emit = defineEmits<{
  (e: 'result', value: boolean): void
}>()

// We might want to delegate the process of emitting
// the result to somewhere else, so we define a function
// we can pass through scoped slots
function emitResult(value: boolean) {
  emit('result', value)
}
</script>

<template>
  <BaseModal :show="show">
    <!-- Default slot, when we want to override the whole component -->
    <slot :emit-result="emitResult">
      <div class="p-4">
        <!-- Title slot, we pass the `emitResult` in case
        we add a close button or something to it -->
        <slot name="title" :emit-result="emitResult">
          <div class="text-lg font-medium">Please confirm</div>
        </slot>

        <!-- Body slot to customize the content -->
        <slot name="body">
          <div class="py-2 text-sm">Are you sure?</div>
        </slot>

        <!-- Actions slot, to customize the dialog buttons -->
        <slot name="actions" :emit-result="emitResult">
          <div class="flex justify-end gap-2">
            <button
              class="bg-indigo-200 px-3 py-1 font-medium"
              @click="$emit('result', false)"
            >
              Cancel
            </button>

            <button
              class="bg-indigo-200 px-3 py-1 font-medium"
              @click="$emit('result', true)"
            >
              Ok
            </button>
          </div>
        </slot>
      </div>
    </slot>
  </BaseModal>
</template>

That’s it! :tada: