Accessing a child component's HTML element(s) in Vue 3


In Vue 2 accessing using a template ref to access a components element was pretty intuitive, you could just access this.$refs.myComponent.$el but in Vue 3 the concept of Template Refs has been merged with any other Ref that has been exposed and the $el property been removed from the API.

What happened to $el

Vue 2 required that all templates have a single root element

<template>
<!-- This is $el -->
<div>
  ...
</div>
</template>

Vue 3 removed the requirement that templates have a single root element, $el semantically no longer makes sense if you’re returning multiple root elements.

<template>
<!-- Which is $el ? -->
<div>...</div>
<div>...</div>
<div>...</div>
</template>

Of course they could have changed the semantics to $els and returned a list, but with with the merging of Template Refs and Ref it presented an opportunity to simplify the API at the cost of some confusion of those used to Vue 2.

A slight gotcha…

Knowing what that Template Refs are just regular Refs one might deduce that we can access the html of a component with the following

<script setup lang="ts">
import MyComponent from "./MyComponent.vue";
import { onMounted, ref } from "vue";
const childRef = ref<typeof MyComponent>();
const html = ref('');
onMounted(() => {
  html.value = childRef.value;
});
</script>

<template>
<my-component ref="childRef"></my-component>
<pre>{{ html }}</pre>
</template>

However, this won’t work because childRef is the Vue component instance, not its element and we can’t use $el for the reasons discussed above.

Exposing your internals

In order to access a Component instance’s internals one needs to expose them as they are closed by default.

Components using <script setup> are closed by default – i.e. the public instance of the component, which is retrieved via template refs or $parent chains, will not expose any of the bindings declared inside <script setup>.

https://vuejs.org/api/sfc-script-setup.html#defineexpose

Exposing the internals in <script setup> is pretty easy, like defineProps and defineEmits there is a compiler macro by the name defineExpose. In your child component you need to define Template Refs on each element you wish to expose.

<script setup lang="ts">
import { ref } from "vue";
const el = ref<HTMLDivElement>()
defineExpose({ el });
</script>

<template>
<div ref="el">
  ...
</div>
</template>

Note: If you’re using Composition API without <script setup> you can access the expose method through the context:

defineComponent(props, context) {
  const el = ref<HTMLDivElement>();
  context.expose({ el });
}

Now you can access this Template Ref in your parent component directly.

<script setup lang="ts">
import MyComponent from "./MyComponent.vue";
import { onMounted, ref } from "vue";
const myComponent = ref<typeof MyComponent>();
const html = ref('');
onMounted(() => {
  html.value = myComponent.value.el;
});
</script>

<template>
<my-component ref="myComponent"></my-component>
<pre>{{ html }}</pre>
</template>

Note: One might be tempted to write myComponent.value.el.value since it’s a Ref after all… right? Well Yes but actually no. Refs are unwrapped when exposed.

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

const a = 1
const b = ref(2)

defineExpose({ a, b })
</script>

When a parent gets an instance of this component via template refs, the retrieved instance will be of the shape { a: number, b: number } (refs are automatically unwrapped just like on normal instances).

https://vuejs.org/api/sfc-script-setup.html#defineexpose

Accessing multiple elements

As I stated above all you need to do is add multiple Refs to your child component and expose them

<script setup lang="ts">
import { ref } from "vue";
const el1 = ref<HTMLDivElement>()
const el2 = ref<HTMLDivElement>()
defineExpose({ el1, el2 });
</script>

<template>
<div ref="el1">...</div>
<!-- not exposed -->
<div>...</div>
<div ref="el2">...</div>
</template>

Final words

While all of this behaviour is documented but it requires knowing what to look for or having read the relevant sections of the VueJS site and connecting the fact that Template Refs have been merged with standards Refs, components can have multiple root elements, and that Component instances require exposing their internals to parent components.

It took me a fair bit to piece it all together and I hope this saves you some time. Cheers.