Footer

Customize the footer content and links

Learn how to customize the footer to match your branding and include relevant links.

The main footer is located at app/components/footer/MainFooter.vue:

app/components/footer/MainFooter.vue
<script setup lang="ts">
import ReportBugForm from '@/components/feedback/ReportBugForm.vue'
import RequestFeatureForm from '@/components/feedback/RequestFeatureForm.vue'

const userStore = useUserStore()
const { isAuthenticated } = storeToRefs(userStore)

const showRequestFeatureForm = ref(false)
const showReportBugForm = ref(false)

const handleClickRequestFeature = () => {
  showRequestFeatureForm.value = true
}

const handleClickReportBug = () => {
  showReportBugForm.value = true
}
</script>

<template>
  <footer
    class="base-container text-muted-foreground flex flex-col lg:flex-row text-center gap-4 items-center justify-center py-4 text-sm"
  >
    <!-- Copyright and legal links -->
    <div class="flex flex-col md:flex-row items-center gap-2">
      <span> © {{ new Date().getFullYear() }} {{ $config.public.siteName }}. All rights reserved. </span>
      <span class="hidden md:inline opacity-50"> | </span>
      <div class="flex items-center gap-2">
        <NuxtLink to="/legal/privacy">
          <span>{{ $t('common.privacyPolicy') }}</span>
        </NuxtLink>
        <span class="opacity-50"> | </span>
        <NuxtLink to="/legal/terms">
          <span>{{ $t('common.termsAndConditions') }}</span>
        </NuxtLink>
      </div>
    </div>

    <!-- Feedback buttons (only for authenticated users) -->
    <div v-if="isAuthenticated" class="hidden lg:flex gap-2">
      <Button variant="outline" size="sm" @click="handleClickReportBug">
        {{ $t('reportBug.button') }}
      </Button>
      <Button variant="outline" size="sm" @click="handleClickRequestFeature">
        {{ $t('requestFeature.button') }}
      </Button>
    </div>

    <!-- Feedback dialogs -->
    <RequestFeatureForm v-if="showRequestFeatureForm" v-model="showRequestFeatureForm" />
    <ReportBugForm v-if="showReportBugForm" v-model="showReportBugForm" />
  </footer>
</template>

Add more footer links:

<template>
  <footer class="base-container py-8">
    <div class="grid grid-cols-1 md:grid-cols-4 gap-8">
      <!-- Company -->
      <div>
        <h3 class="font-semibold mb-4">Company</h3>
        <ul class="space-y-2 text-sm">
          <li><NuxtLink to="/about">About</NuxtLink></li>
          <li><NuxtLink to="/blog">Blog</NuxtLink></li>
          <li><NuxtLink to="/careers">Careers</NuxtLink></li>
        </ul>
      </div>

      <!-- Product -->
      <div>
        <h3 class="font-semibold mb-4">Product</h3>
        <ul class="space-y-2 text-sm">
          <li><NuxtLink to="/pricing">Pricing</NuxtLink></li>
          <li><NuxtLink to="/docs">Documentation</NuxtLink></li>
          <li><NuxtLink to="/changelog">Changelog</NuxtLink></li>
        </ul>
      </div>

      <!-- Support -->
      <div>
        <h3 class="font-semibold mb-4">Support</h3>
        <ul class="space-y-2 text-sm">
          <li><NuxtLink to="/support">Contact</NuxtLink></li>
          <li><NuxtLink to="/faq">FAQ</NuxtLink></li>
          <li><NuxtLink to="/status">Status</NuxtLink></li>
        </ul>
      </div>

      <!-- Legal -->
      <div>
        <h3 class="font-semibold mb-4">Legal</h3>
        <ul class="space-y-2 text-sm">
          <li><NuxtLink to="/legal/privacy">Privacy</NuxtLink></li>
          <li><NuxtLink to="/legal/terms">Terms</NuxtLink></li>
          <li><NuxtLink to="/legal/cookies">Cookies</NuxtLink></li>
        </ul>
      </div>
    </div>

    <!-- Copyright -->
    <div class="mt-8 pt-8 border-t text-center text-sm text-muted-foreground">
      © {{ new Date().getFullYear() }} {{ $config.public.siteName }}. All rights reserved.
    </div>
  </footer>
</template>
<script setup>
const socialLinks = [
  { icon: 'simple-icons:twitter', url: 'https://twitter.com/yourcompany', label: 'Twitter' },
  { icon: 'simple-icons:github', url: 'https://github.com/yourcompany', label: 'GitHub' },
  { icon: 'simple-icons:linkedin', url: 'https://linkedin.com/company/yourcompany', label: 'LinkedIn' },
  { icon: 'simple-icons:discord', url: 'https://discord.gg/yourserver', label: 'Discord' },
]
</script>

<template>
  <footer>
    <!-- ... other content -->

    <!-- Social links -->
    <div class="flex items-center justify-center gap-4">
      <a
        v-for="social in socialLinks"
        :key="social.label"
        :href="social.url"
        target="_blank"
        rel="noopener noreferrer"
        class="text-muted-foreground hover:text-foreground transition-colors"
        :aria-label="social.label"
      >
        <Icon :name="social.icon" class="size-5" />
      </a>
    </div>
  </footer>
</template>

Newsletter signup

Add a newsletter signup form to the footer:

<script setup>
const email = ref('')
const isSubmitting = ref(false)

async function handleNewsletterSignup() {
  if (!email.value) return

  isSubmitting.value = true

  try {
    await $fetch('/api/newsletter/subscribe', {
      method: 'POST',
      body: { email: email.value },
    })

    toast.success('Subscribed successfully!')
    email.value = ''
  } catch (error) {
    toast.error('Failed to subscribe')
  } finally {
    isSubmitting.value = false
  }
}
</script>

<template>
  <footer>
    <div class="max-w-md mx-auto mb-8">
      <h3 class="font-semibold mb-2">Subscribe to our newsletter</h3>
      <p class="text-sm text-muted-foreground mb-4">Get the latest updates and news delivered to your inbox.</p>
      <form @submit.prevent="handleNewsletterSignup" class="flex gap-2">
        <Input v-model="email" type="email" placeholder="Enter your email" required />
        <Button type="submit" :disabled="isSubmitting"> Subscribe </Button>
      </form>
    </div>

    <!-- ... rest of footer -->
  </footer>
</template>
<template>
  <footer class="py-6 text-center text-sm text-muted-foreground">
    <p>© {{ new Date().getFullYear() }} {{ $config.public.siteName }}</p>
    <div class="flex items-center justify-center gap-4 mt-2">
      <NuxtLink to="/legal/privacy">Privacy</NuxtLink>
      <span></span>
      <NuxtLink to="/legal/terms">Terms</NuxtLink>
      <span></span>
      <NuxtLink to="/support">Support</NuxtLink>
    </div>
  </footer>
</template>
<template>
  <footer class="bg-muted">
    <div class="base-container py-12">
      <div class="grid grid-cols-2 md:grid-cols-4 gap-8">
        <!-- Columns -->
      </div>
    </div>
    <div class="border-t py-6">
      <div class="base-container text-center text-sm text-muted-foreground">
        © {{ new Date().getFullYear() }} {{ $config.public.siteName }}
      </div>
    </div>
  </footer>
</template>
<template>
  <footer class="bg-muted">
    <div class="base-container py-12">
      <div class="grid grid-cols-1 md:grid-cols-5 gap-8">
        <!-- Logo and description -->
        <div class="md:col-span-2">
          <AppLogo />
          <p class="mt-4 text-sm text-muted-foreground">Build amazing web applications with our modern boilerplate.</p>
        </div>

        <!-- Links columns -->
        <div v-for="section in footerSections" :key="section.title">
          <h3 class="font-semibold mb-4">{{ section.title }}</h3>
          <ul class="space-y-2 text-sm">
            <li v-for="link in section.links" :key="link.to">
              <NuxtLink :to="link.to">{{ link.label }}</NuxtLink>
            </li>
          </ul>
        </div>
      </div>
    </div>
  </footer>
</template>

Show different content based on authentication

<script setup>
const userStore = useUserStore()
const { isAuthenticated } = storeToRefs(userStore)
</script>

<template>
  <footer>
    <!-- Content for authenticated users -->
    <div v-if="isAuthenticated">
      <Button @click="showFeedbackForm">Report Bug</Button>
    </div>

    <!-- Content for guests -->
    <div v-else>
      <Button as-child>
        <NuxtLink to="/auth/register">Sign Up</NuxtLink>
      </Button>
    </div>
  </footer>
</template>
app/layouts/default.vue
<script setup>
const route = useRoute()

const hideFooter = computed(() => {
  const pagesWithoutFooter = ['/auth/login', '/auth/register', '/checkout']
  return pagesWithoutFooter.some(page => route.path.startsWith(page))
})
</script>

<template>
  <div>
    <MainHeader />
    <slot />
    <MainFooter v-if="!hideFooter" />
  </div>
</template>

Keep footer at bottom of page:

app/layouts/default.vue
<template>
  <div class="min-h-screen flex flex-col">
    <MainHeader />
    <main class="flex-1">
      <slot />
    </main>
    <MainFooter />
  </div>
</template>
<template>
  <footer class="bg-primary text-primary-foreground">
    <!-- Footer content -->
  </footer>
</template>
<template>
  <footer class="bg-gradient-to-r from-primary to-secondary text-white">
    <!-- Footer content -->
  </footer>
</template>

Create legal pages linked from the footer:

app/pages/legal/privacy.vue
<script setup>
useSeoMeta({
  title: 'Privacy Policy',
  description: 'Our privacy policy',
})
</script>

<template>
  <div class="base-container py-12 max-w-4xl">
    <h1 class="text-4xl font-bold mb-8">Privacy policy</h1>
    <div class="prose dark:prose-invert max-w-none">
      <!-- Privacy policy content -->
      <p>Last updated: {{ new Date().toLocaleDateString() }}</p>
      <!-- ... -->
    </div>
  </div>
</template>

Internationalization

Use i18n for footer text:

<script setup>
const { t } = useI18n()
</script>

<template>
  <footer>
    <span>© {{ new Date().getFullYear() }} {{ t('footer.copyright') }}</span>
    <NuxtLink to="/legal/privacy">
      {{ t('footer.privacy') }}
    </NuxtLink>
  </footer>
</template>

Add translations in i18n/locales/en.json:

{
  "footer": {
    "copyright": "All rights reserved",
    "privacy": "Privacy Policy",
    "terms": "Terms of Service"
  }
}

Best practices

Keep it accessible

  • Use semantic HTML (<footer> tag)
  • Ensure sufficient color contrast
  • Make links keyboard navigable

Mobile-friendly

  • Stack columns on mobile
  • Use appropriate font sizes
  • Ensure touch targets are large enough

Performance

  • Lazy-load newsletter forms
  • Optimize social media icons
  • Don't load heavy scripts in footer

Reference

Keep your footer clean and organized. Users typically look there for legal information, contact details, and social links.