Vue’s component model makes it straightforward to encapsulate common UI patterns like modal dialogs. A well-designed dialog component accepts configuration through props, exposes slots for flexible content, and communicates state changes back to the parent using events. The implementation described here mimics the behavior of popular UI libraries while keeping the codebase light and fully under your control.
Component Foundation
Start with a single-file component that renders a fixed overlay and a centered dialog box. The structure separates header, body, and footer sections while applying basic styling for the backdrop and positioning.
DialogBox.vue – Initial Template
<template>
<div class="modal-overlay" v-show="showDialog" @click.self="onOverlayClick">
<div class="modal-container" :style="containerStyle">
<header class="modal-header">
<span class="modal-heading">{{ heading }}</span>
<button class="close-btn" @click="requestClose">
<i class="icon-close"></i>
</button>
</header>
<section class="modal-body">
<slot></slot>
</section>
<footer class="modal-footer" v-if="$slots.footer">
<slot name="footer"></slot>
</footer>
</div>
</div>
</template>
<script>
export default {
name: 'DialogBox',
props: {
heading: {
type: String,
default: 'Notice'
},
dialogWidth: {
type: String,
default: '420px'
},
offsetTop: {
type: String,
default: '12vh'
},
showDialog: {
type: Boolean,
default: false
}
},
computed: {
containerStyle() {
return {
width: this.dialogWidth,
marginTop: this.offsetTop
}
}
},
methods: {
onOverlayClick() {
this.$emit('close')
},
requestClose() {
this.$emit('close')
}
}
}
</script>
<style scoped>
.modal-overlay {
position: fixed;
inset: 0;
background-color: rgba(0, 0, 0, 0.45);
display: flex;
justify-content: center;
overflow: auto;
z-index: 2000;
}
.modal-container {
background: #fff;
border-radius: 6px;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.15);
margin-bottom: 40px;
align-self: flex-start;
display: flex;
flex-direction: column;
max-width: 90vw;
}
.modal-header {
padding: 18px 20px 10px;
display: flex;
justify-content: space-between;
align-items: center;
}
.modal-heading {
font-size: 16px;
font-weight: 600;
color: #303133;
}
.close-btn {
background: none;
border: none;
font-size: 18px;
cursor: pointer;
color: #909399;
}
.modal-body {
padding: 20px;
color: #606266;
font-size: 14px;
word-break: break-word;
}
.modal-footer {
padding: 10px 20px 20px;
text-align: right;
}
</style>
Controlling Visibility with .sync
The parent controls the dialog’s visibility using a boolean value. Instead of a simple prop, Vue’s .sync modifier provides a concise two-way binding pattern. The child emits update:showDialog whenever it needs to close, keeping the parent’s state in sync.
Parent Usage
<template>
<div>
<button @click="open = true">Open Dialog</button>
<DialogBox
heading="Confirm Action"
:show-dialog.sync="open"
@close="open = false"
>
<p>Are you sure you want to proceed?</p>
<template #footer>
<button @click="open = false">Cancel</button>
<button class="primary" @click="handleConfirm">Confirm</button>
</template>
</DialogBox>
</div>
</template>
<script>
import DialogBox from './DialogBox.vue'
export default {
components: { DialogBox },
data() {
return {
open: false
}
},
methods: {
handleConfirm() {
// perform action
this.open = false
}
}
}
</script>
Inside the child, the .sync modifier expands to v-bind:showDialog="open" and v-on:update:showDialog="open = $event". The child calls $emit('update:showDialog', false) to notify the parent. The v-show="showDialog" directive handles actual visibility while keeping the DOM ready for transitions.
Customizing Content with Slots
The body area uses an anonymous slot, allowing anything from plain text to complex markup to be passed directly between the component tags. For the footer, a named slot (footer) is used so the parent can optional provide action buttons; if no footer slot is supplied, the footer section remains completely hidden.
Example – Body with custom list
<DialogBox heading="Item list" :show-dialog.sync="listVisible">
<ul>
<li>Apple</li>
<li>Banana</li>
<li>Cherry</li>
</ul>
<template #footer>
<button @click="listVisible = false">Close</button>
</template>
</DialogBox>
Dynamic Dimensions
The component accepts dialogWidth and offsetTop as props, allowing callers to adjust the box width and its distance from the top of the viewport without touching the component’s internal styles. The computed property containerStyle binds these values to the inline style.
Transition Enhancement
Adding a <transition> wrapper around the overlay enables smooth fade/animation when toggling visibility. Simply wrap the content with <transition name="fade"> and define corresponding CSS classes.
<transition name="fade">
<div class="modal-overlay" v-show="showDialog" ...>
...
</div>
</transition>
.fade-enter-active, .fade-leave-active {
transition: opacity 0.25s;
}
.fade-enter-from, .fade-leave-to {
opacity: 0;
}
The resulting component behaves predictably: it opens and closes through a reactive boolean, lets consumers inject arbitrary markup, and respects custom sizing constraints. This pattern forms a solid foundation that can be extended with additional features like before-close hooks, nested dialogs, or teleport to body for better stacking context management.