Next.js starter your AI actually understands. Ship internal tools in days not weeks. Pre-order $199 $499 → [Get it now]

How to expose methods in Vue 3 with defineExpose

In Vue 3 <script setup>, all bindings are private by default — parent components cannot access child component methods or data using template refs unless you explicitly expose them with defineExpose. As the creator of CoreUI with Vue development experience since 2014, I use defineExpose in CoreUI Vue components that need to provide imperative APIs, such as dialog open() and close() methods or form reset() and validate() methods. Without defineExpose, a parent’s childRef.value.open() call fails silently because the method is not exposed. This is intentional — <script setup> components are encapsulated by default, and defineExpose is an explicit contract of the public API.

Expose methods from a child component so the parent can call them.

<!-- Modal.vue -->
<template>
  <div v-if="isOpen" class="modal">
    <div class="modal-content">
      <slot></slot>
      <button @click="close">Close</button>
    </div>
  </div>
</template>

<script setup>
import { ref } from 'vue'

const isOpen = ref(false)

function open() {
  isOpen.value = true
}

function close() {
  isOpen.value = false
}

function toggle() {
  isOpen.value = !isOpen.value
}

// Expose the public API to parent components
defineExpose({ open, close, toggle })
</script>
<!-- Parent.vue -->
<template>
  <button @click="openModal">Open Modal</button>
  <Modal ref="modalRef">
    <p>Modal content here</p>
  </Modal>
</template>

<script setup>
import { ref } from 'vue'
import Modal from './Modal.vue'

const modalRef = ref(null)

function openModal() {
  modalRef.value.open() // Works because open() is exposed
}
</script>

defineExpose({ open, close, toggle }) makes exactly those three methods available on the component’s template ref. The internal isOpen state remains private — the parent can only call open(), close(), or toggle(), not access isOpen directly.

Exposing Reactive Data

Expose reactive state alongside methods.

<!-- FormComponent.vue -->
<script setup>
import { ref, computed } from 'vue'

const fields = ref({ name: '', email: '' })
const isDirty = ref(false)

const isValid = computed(() =>
  fields.value.name.trim() !== '' && fields.value.email.includes('@')
)

function reset() {
  fields.value = { name: '', email: '' }
  isDirty.value = false
}

function validate() {
  return isValid.value
}

// Expose both reactive data and methods
defineExpose({
  isDirty,  // reactive - parent reads isDirty.value
  isValid,  // computed - parent reads isValid.value
  reset,
  validate
})
</script>
<!-- Parent.vue -->
<script setup>
import { ref } from 'vue'
import FormComponent from './FormComponent.vue'

const formRef = ref(null)

function handleSave() {
  if (formRef.value.validate()) {
    // save...
  }
}

function handleReset() {
  formRef.value.reset()
}
</script>

Exposed refs and computed properties retain their reactivity on the parent side. The parent can read formRef.value.isDirty.value and it will be reactive. However, the parent cannot assign to isDirty directly — the child controls its own state.

Best Practice Note

In CoreUI Vue components we use defineExpose sparingly — only for imperative APIs that genuinely need to be called from outside the component, such as focus management, scrolling, or animation triggers. For most data communication, props and emits are the idiomatic Vue approach. Overusing defineExpose breaks component encapsulation and makes components harder to refactor. See how to define emits in Vue 3 with defineEmits for the event-based alternative that is often more appropriate.


Speed up your responsive apps and websites with fully-featured, ready-to-use open-source admin panel templates—free to use and built for efficiency.


About the Author