useTina()
The main composable for consuming TinaCMS data with reactive, real-time updates.
Type signature
function useTina<T extends Record<string, unknown>>(
props: UseTinaOptions<T> | Ref<any> | ComputedRef<any>
): UseTinaReturn<T>
You can pass either an options object or a ref directly, including the whole ref returned by useAsyncData/useFetch (see the example below).
Parameter: props (as UseTinaOptions<T>)
| Property | Type | Required | Description |
|---|---|---|---|
data | T | No | The initial data, from SSR, an API call, or a Tina query. |
query | string | No | The Tina GraphQL query string, needed for the visual-editing handshake. |
variables | Record<string, unknown> | No | GraphQL variables for the query. |
Return value
| Property | Type | Description |
|---|---|---|
data | Ref<T> | Reactive data, automatically updated as the form changes in Tina Admin. |
isEditing | Ref<boolean> | true when the page is open in editing/preview mode. |
isInIframe | Ref<boolean> | true when the page is loaded inside the Tina Admin iframe. |
quickEditEnabled | Ref<boolean> | true while quick-edit (click-to-edit) mode is enabled from Tina Admin. |
bridgeError | Ref<Error | null> | The last error caught while talking to Tina Admin, or null if nothing failed. Shared with useTinaBridge(). |
refresh | () => Promise<void> | Forces refreshNuxtData(). Call it after a real save event. |
tinaField(object, property?, index?)
A helper that builds a data-tina-field attribute, so Tina Admin knows which DOM element represents which field.
<h1 :data-tina-field="tinaField(post, 'title')">{{ post.title }}</h1>
<!-- for items inside a list, include the index -->
<span
v-for="(tag, idx) in post.tags"
:key="idx"
:data-tina-field="tinaField(post, 'tags', idx)"
>{{ tag }}</span>
object must come from the data returned by useTina(), not the raw data before processing. That's where tinaField() finds the _content_source metadata it needs.
Usage example
useTina() accepts the full server response ref, not just the unwrapped data. It needs the query and variables inside it to send the handshake to Tina Admin. The easiest approach: write a server route that calls tina/__generated__/client, then pass the useAsyncData result ref straight into useTina().
// server/api/posts/[slug].get.ts
import client from '../../../tina/__generated__/client'
export default defineEventHandler(async (event) => {
const relativePath = `${getRouterParam(event, 'slug')}.md`
const res = await client.queries.post({ relativePath })
return { data: res.data, query: res.query, variables: res.variables }
})
<script setup lang="ts">
const route = useRoute()
const { data: response } = await useAsyncData(`post-${route.params.slug}`, () =>
$fetch(`/api/posts/${route.params.slug}`),
)
const { data, isEditing, isInIframe } = useTina(response)
const post = computed(() => data.value?.post ?? {})
</script>
<template>
<article>
<div v-if="isInIframe" class="edit-badge">Tina Contextual Editing Active</div>
<h1 :data-tina-field="tinaField(post, 'title')">{{ post.title }}</h1>
<p :data-tina-field="tinaField(post, 'description')">{{ post.description }}</p>
</article>
</template>