Vue templateFree

Blog Article

Long-form Vue 3 article template with hand-tuned Tailwind typography, a scroll-aware table of contents, code blocks, callouts, author box and related posts.

by Vueplayv1.0.0Updated 2 hours ago

Install
Full screen ↗

About this template

A reading page for serious long-form writing: engineering deep dives, guides and essays. It pairs a generous article column with a sticky table of contents that highlights the section you are reading, a reading progress bar in the header and a share rail on wide screens.

The typography is written with plain Tailwind utilities rather than the typography plugin. All the paragraph, heading, list, quote and inline-code styles live in src/prose.ts, so adjusting the rhythm of the whole article means editing one small file. Code blocks use a tiny dependency-free highlighter and have a copy button.

On phones the table of contents becomes a collapsible panel that shows the current section, and the diagram, code and callouts all adapt to narrow widths. Replace the article body in ArticleBody.vue and the entries in src/data.ts to publish your own post.

  • Article hero, body, author box and related posts
  • Sticky table of contents with scroll-spy highlighting
  • Reading progress bar and share rail
  • Code block with syntax colours and copy button, note and warning callouts
  • Utility-class typography in one file, light and dark mode

Code

21 files in the repo
<script setup lang="ts">
import { onBeforeUnmount, onMounted, ref } from 'vue'
import { toc } from './data'
import ArticleBody from './components/ArticleBody.vue'
import ArticleHero from './components/ArticleHero.vue'
import AuthorBox from './components/AuthorBox.vue'
import RelatedPosts from './components/RelatedPosts.vue'
import SiteHeader from './components/SiteHeader.vue'
import TableOfContents from './components/TableOfContents.vue'

const HEADER_OFFSET = 96

const activeId = ref(toc[0]!.id)
const progress = ref(0)
const articleEl = ref<HTMLElement>()

let frame = 0
function update() {
  frame = 0
  // The active section is the last heading that has scrolled past the header.
  let current = toc[0]!.id
  for (const item of toc) {
    const el = document.getElementById(item.id)
    if (el && el.getBoundingClientRect().top <= HEADER_OFFSET + 24) current = item.id
  }
  // At the very bottom, highlight the final section even if it is short.
  if (window.innerHeight + window.scrollY >= document.documentElement.scrollHeight - 4) current = toc[toc.length - 1]!.id
  activeId.value = current

  const rect = articleEl.value?.getBoundingClientRect()
  if (rect) {
    const total = rect.height - window.innerHeight + HEADER_OFFSET
    progress.value = Math.min(1, Math.max(0, (HEADER_OFFSET - rect.top) / Math.max(total, 1)))
  }
}
function onScroll() {
  if (!frame) frame = requestAnimationFrame(update)
}

onMounted(() => {
  update()
  window.addEventListener('scroll', onScroll, { passive: true })
  window.addEventListener('resize', onScroll)
})
onBeforeUnmount(() => {
  window.removeEventListener('scroll', onScroll)
  window.removeEventListener('resize', onScroll)
  cancelAnimationFrame(frame)
})

function navigate(id: string) {
  if (id === 'top') {
    window.scrollTo({ top: 0, behavior: 'smooth' })
    return
  }
  const el = document.getElementById(id)
  if (!el) return
  window.scrollTo({ top: el.getBoundingClientRect().top + window.scrollY - HEADER_OFFSET + 8, behavior: 'smooth' })
  history.replaceState(null, '', `#${id}`)
  el.focus({ preventScroll: true })
}

const linkCopied = ref(false)
async function copyLink() {
  try {
    await navigator.clipboard.writeText(window.location.href)
    linkCopied.value = true
    setTimeout(() => { linkCopied.value = false }, 1800)
  }
  catch {
    linkCopied.value = false
  }
}

const tags = ['Sync', 'Offline-first', 'Distributed systems', 'Mobile']
</script>

<template>
  <div id="top" class="min-h-screen bg-white text-gray-900 dark:bg-gray-950 dark:text-white">
    <SiteHeader :progress="progress" />

    <main>
      <ArticleHero />

      <div class="mx-auto grid max-w-6xl gap-10 px-4 pt-12 pb-20 sm:px-6 sm:pt-16 lg:grid-cols-[minmax(0,1fr)_minmax(0,42rem)_minmax(0,1fr)] lg:gap-12">
        <!-- Share rail -->
        <aside class="hidden lg:block" aria-label="Share">
          <div class="sticky top-24 flex flex-col items-end gap-2">
            <p class="text-xs font-semibold tracking-wider text-gray-500 uppercase dark:text-gray-400">Share</p>
            <button
              type="button"
              class="grid size-10 place-items-center rounded-full border border-gray-200 text-gray-600 transition hover:border-teal-500 hover:text-teal-700 focus-visible:outline-2 focus-visible:outline-teal-500 dark:border-white/10 dark:text-gray-400 dark:hover:text-teal-300"
              :aria-label="linkCopied ? 'Link copied' : 'Copy link to article'"
              @click="copyLink"
            >
              <svg v-if="!linkCopied" viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" class="size-4.5" aria-hidden="true"><path d="M8.5 11.5a3.5 3.5 0 0 0 5 0l2.5-2.5a3.5 3.5 0 0 0-5-5l-1 1M11.5 8.5a3.5 3.5 0 0 0-5 0L4 11a3.5 3.5 0 0 0 5 5l1-1" /></svg>
              <svg v-else viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="size-4.5 text-teal-600 dark:text-teal-400" aria-hidden="true"><path d="m4 10.5 4 4 8-9" /></svg>
            </button>
            <a href="#" class="grid size-10 place-items-center rounded-full border border-gray-200 text-gray-600 transition hover:border-teal-500 hover:text-teal-700 focus-visible:outline-2 focus-visible:outline-teal-500 dark:border-white/10 dark:text-gray-400 dark:hover:text-teal-300" aria-label="Share by email">
              <svg viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linejoin="round" class="size-4.5" aria-hidden="true"><rect x="3" y="4.5" width="14" height="11" rx="2" /><path d="m3.5 5.5 6.5 5 6.5-5" /></svg>
            </a>
          </div>
        </aside>

        <div class="min-w-0">
          <TableOfContents class="mb-10 lg:hidden" variant="panel" :items="toc" :active-id="activeId" @navigate="navigate" />
          <article ref="articleEl" class="[&_h2]:outline-none">
            <ArticleBody />
          </article>

          <ul class="mt-12 flex flex-wrap gap-2" aria-label="Tags">
            <li v-for="tag in tags" :key="tag">
              <a href="#" class="inline-block rounded-full bg-gray-100 px-3 py-1 text-sm text-gray-700 hover:bg-teal-50 hover:text-teal-800 dark:bg-white/5 dark:text-gray-300 dark:hover:bg-teal-400/10 dark:hover:text-teal-200">#{{ tag }}</a>
            </li>
          </ul>

          <AuthorBox />
        </div>

        <aside class="hidden lg:block">
          <TableOfContents variant="rail" :items="toc" :active-id="activeId" @navigate="navigate" />
        </aside>
      </div>

      <RelatedPosts />
    </main>

    <footer class="border-t border-gray-200 dark:border-white/10">
      <div class="mx-auto flex max-w-6xl flex-col gap-3 px-4 py-8 text-sm text-gray-500 sm:flex-row sm:justify-between sm:px-6 dark:text-gray-400">
        <p>© 2026 Kestrel Software. All rights reserved.</p>
        <p>Set in your system fonts. Built with Vue and Tailwind CSS.</p>
      </div>
    </footer>
  </div>
</template>
All blogs →
Preview of Blog IndexFree

Template · Blogs

Blog Index

Vue 3 blog home template with a featured post, drawn cover art, category chips, live search, pagination and a newsletter signup. Tailwind CSS with dark mode.

by Vueplay
Preview of Docs Layout$15

Template · Documentation

Docs Layout

Premium Vue 3 documentation template: searchable sidebar, content pages, scroll-aware table of contents, tabbed code blocks with copy, callouts and a mobile drawer.

by Vueplay
Preview of 404 Not Found PageFree

Template · Error pages

404 Not Found Page

Friendly Vue 3 404 page with a drawn compass illustration, live search suggestions and helpful links, plus a compact 500 error variant. Tailwind CSS.

by Vueplay
Preview of Admin Dashboard$29

Template · Dashboards

Admin Dashboard

A complete Vue 3 admin dashboard template with a collapsible sidebar, hand-drawn SVG charts, a sortable customers table and settings. Tailwind CSS, dark mode.

by Vueplay

Pro feature

Upgrade to Pro

Upgrade to unlock more of Vueplay. Cancel any time.

  • Custom domains with HTTPS
  • Private projects
  • No "Made with Vueplay" badge
  • GitHub sync

$120/year

Compare plansBilling settings