Vue componentFree

Changelog Timeline

Vertical changelog timeline for Vue 3 with version badges, dated releases, typed change tags and filter chips. Built with Tailwind CSS, dark mode.

by Vueplayv1.0.0Updated 2 hours ago

Install
Full screen ↗

About this component

A release notes page in timeline form. Each release shows its version badge and date on the left of a vertical line, and its title, optional summary and a list of changes on the right. On phones the date moves above the content and the line runs down the left edge.

Every change is tagged New, Improved, Fixed or Security with its own colour, and the filter chips at the top (toggle buttons with aria-pressed) narrow the list to the types a reader cares about. The newest release is marked Latest and gets the accent colour.

Dates are ISO strings formatted with Intl.DateTimeFormat in UTC, so they never shift a day across time zones; change the locale prop to localise them. The release slot adds content under each entry, such as a link to the full notes.

  • Version badges and formatted dates
  • New, Improved, Fixed and Security tags
  • Filter chips by change type
  • Responsive two-column to single-column layout
  • Light and dark mode

Code

11 files in the repo
<script setup lang="ts">
import { computed, ref } from 'vue'

export type ChangeType = 'new' | 'improved' | 'fixed' | 'security'

export interface Change {
  type: ChangeType
  text: string
}

export interface Release {
  version: string
  /** ISO date, e.g. "2026-09-24". */
  date: string
  title: string
  summary?: string
  changes: Change[]
}

const props = withDefaults(defineProps<{
  releases?: Release[]
  heading?: string
  subheading?: string
  /** Show filter chips for change types. */
  filterable?: boolean
  /** BCP 47 locale used to format dates. */
  locale?: string
}>(), {
  heading: 'Changelog',
  subheading: 'New features, improvements and fixes to Beacon, shipped every two weeks.',
  filterable: true,
  locale: 'en-US',
  releases: () => [
    {
      version: '3.4.0', date: '2026-09-24', title: 'Saved views and a faster search',
      summary: 'Save any combination of filters as a view and share it with your team. Search now returns results as you type.',
      changes: [
        { type: 'new', text: 'Saved views with per-team sharing and a default view per project.' },
        { type: 'improved', text: 'Search is up to 4x faster on workspaces with more than 50,000 items.' },
        { type: 'fixed', text: 'Keyboard shortcuts no longer fire while typing in a comment.' },
      ],
    },
    {
      version: '3.3.2', date: '2026-09-10', title: 'Security and stability',
      changes: [
        { type: 'security', text: 'Session tokens are now rotated after a password change on every device.' },
        { type: 'fixed', text: 'Exports with more than 10,000 rows no longer time out.' },
        { type: 'fixed', text: 'Due dates respect the workspace time zone in email reminders.' },
      ],
    },
    {
      version: '3.3.0', date: '2026-08-27', title: 'Recurring tasks',
      summary: 'Tasks can repeat daily, weekly or on a custom schedule, and each occurrence keeps its own history.',
      changes: [
        { type: 'new', text: 'Recurring tasks with flexible schedules and skip-next.' },
        { type: 'new', text: 'Webhook events for task.recurred and task.skipped.' },
        { type: 'improved', text: 'The calendar view loads a month in a single request.' },
      ],
    },
    {
      version: '3.2.0', date: '2026-08-13', title: 'Dark mode everywhere',
      changes: [
        { type: 'new', text: 'Dark mode for the web app, emails and the embeddable widget.' },
        { type: 'improved', text: 'Higher contrast status colours that pass WCAG AA in both themes.' },
      ],
    },
  ],
})

const types: { value: ChangeType, label: string, badge: string, dot: string }[] = [
  { value: 'new', label: 'New', badge: 'bg-orange-50 text-orange-700 ring-orange-600/20 dark:bg-orange-400/10 dark:text-orange-300 dark:ring-orange-400/25', dot: 'bg-orange-500' },
  { value: 'improved', label: 'Improved', badge: 'bg-sky-50 text-sky-700 ring-sky-600/20 dark:bg-sky-400/10 dark:text-sky-300 dark:ring-sky-400/25', dot: 'bg-sky-500' },
  { value: 'fixed', label: 'Fixed', badge: 'bg-emerald-50 text-emerald-700 ring-emerald-600/20 dark:bg-emerald-400/10 dark:text-emerald-300 dark:ring-emerald-400/25', dot: 'bg-emerald-500' },
  { value: 'security', label: 'Security', badge: 'bg-rose-50 text-rose-700 ring-rose-600/20 dark:bg-rose-400/10 dark:text-rose-300 dark:ring-rose-400/25', dot: 'bg-rose-500' },
]
const typeInfo = Object.fromEntries(types.map(t => [t.value, t])) as Record<ChangeType, (typeof types)[number]>

const active = ref<Set<ChangeType>>(new Set())
function toggleType(t: ChangeType) {
  const next = new Set(active.value)
  if (next.has(t)) next.delete(t)
  else next.add(t)
  active.value = next
}

const visible = computed(() => props.releases
  .map((release, index) => ({
    release,
    latest: index === 0,
    changes: active.value.size ? release.changes.filter(c => active.value.has(c.type)) : release.changes,
  }))
  .filter(r => r.changes.length > 0))

const formatter = computed(() => new Intl.DateTimeFormat(props.locale, { year: 'numeric', month: 'short', day: 'numeric', timeZone: 'UTC' }))
const formatDate = (iso: string) => formatter.value.format(new Date(`${iso}T00:00:00Z`))
</script>

<template>
  <section class="px-4 py-16 sm:px-6 sm:py-20">
    <div class="mx-auto max-w-4xl">
      <header class="max-w-2xl">
        <h2 class="text-3xl font-bold tracking-tight text-gray-900 sm:text-4xl dark:text-white">{{ heading }}</h2>
        <p class="mt-3 text-lg text-pretty text-gray-600 dark:text-gray-400">{{ subheading }}</p>
      </header>

      <div v-if="filterable" class="mt-8 flex flex-wrap items-center gap-2" role="group" aria-label="Filter by change type">
        <button
          v-for="t in types"
          :key="t.value"
          type="button"
          :aria-pressed="active.has(t.value)"
          class="inline-flex items-center gap-2 rounded-full px-3 py-1.5 text-sm font-medium ring-1 transition focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-orange-600 dark:focus-visible:outline-orange-400"
          :class="active.has(t.value)
            ? 'bg-gray-900 text-white ring-gray-900 dark:bg-white dark:text-gray-900 dark:ring-white'
            : 'bg-white text-gray-700 ring-gray-200 hover:bg-gray-50 dark:bg-gray-900 dark:text-gray-300 dark:ring-white/10 dark:hover:bg-gray-800'"
          @click="toggleType(t.value)"
        >
          <span class="size-2 rounded-full" :class="t.dot" aria-hidden="true" />
          {{ t.label }}
        </button>
        <button
          v-if="active.size"
          type="button"
          class="rounded-full px-3 py-1.5 text-sm font-medium text-gray-500 hover:text-gray-900 focus-visible:outline-2 focus-visible:outline-orange-600 dark:text-gray-400 dark:hover:text-white"
          @click="active = new Set()"
        >Clear</button>
      </div>

      <ol class="relative mt-10 space-y-12 before:absolute before:top-2 before:bottom-2 before:left-[5px] before:w-px before:bg-gray-200 md:before:left-[11.5rem] dark:before:bg-white/10">
        <li v-for="{ release, latest, changes } in visible" :key="release.version" class="relative grid gap-3 pl-8 md:grid-cols-[10rem_1fr] md:gap-12 md:pl-0">
          <span
            aria-hidden="true"
            class="absolute top-1.5 left-0 size-[11px] rounded-full ring-4 md:left-[calc(11.5rem-5px)]"
            :class="latest ? 'bg-orange-500 ring-orange-100 dark:ring-orange-500/20' : 'bg-gray-300 ring-white dark:bg-gray-600 dark:ring-gray-950'"
          />
          <div class="flex items-center gap-3 md:flex-col md:items-end md:gap-2 md:pt-0.5 md:text-right">
            <span
              class="rounded-md px-2 py-0.5 font-mono text-xs font-semibold ring-1"
              :class="latest
                ? 'bg-orange-600 text-white ring-orange-600 dark:bg-orange-500 dark:text-gray-950 dark:ring-orange-500'
                : 'bg-gray-50 text-gray-700 ring-gray-200 dark:bg-white/5 dark:text-gray-300 dark:ring-white/10'"
            >v{{ release.version }}</span>
            <time :datetime="release.date" class="text-sm text-gray-500 dark:text-gray-400">{{ formatDate(release.date) }}</time>
          </div>

          <article class="md:pl-4">
            <h3 class="flex flex-wrap items-center gap-2 text-lg font-semibold text-gray-900 dark:text-white">
              {{ release.title }}
              <span v-if="latest" class="rounded-full bg-orange-50 px-2 py-0.5 text-xs font-semibold text-orange-700 ring-1 ring-orange-600/20 dark:bg-orange-400/10 dark:text-orange-300 dark:ring-orange-400/25">Latest</span>
            </h3>
            <p v-if="release.summary" class="mt-2 text-base/7 text-pretty text-gray-600 dark:text-gray-400">{{ release.summary }}</p>
            <ul class="mt-4 space-y-2.5">
              <li v-for="change in changes" :key="change.text" class="flex items-start gap-3 text-sm/6 text-gray-700 dark:text-gray-300">
                <span class="mt-0.5 w-20 shrink-0 rounded-md px-2 py-0.5 text-center text-xs font-semibold ring-1" :class="typeInfo[change.type].badge">
                  {{ typeInfo[change.type].label }}
                </span>
                <span>{{ change.text }}</span>
              </li>
            </ul>
            <slot name="release" :release="release" />
          </article>
        </li>
      </ol>

      <p v-if="!visible.length" class="mt-10 text-sm text-gray-500 dark:text-gray-400">No changes match the selected filters.</p>
    </div>
  </section>
</template>

Props

PropTypeDefaultDescription
releasesRelease[]4 sample releasesReleases, newest first. Each has version, ISO date, title, optional summary and changes ({ type: 'new' | 'improved' | 'fixed' | 'security', text }).
headingstring'Changelog'Section heading.
subheadingstring'New features, improvements and fixes…'Text under the heading.
filterablebooleantrueShow filter chips for change types.
localestring'en-US'Locale used to format release dates.

Slots

SlotDescription
releaseScoped slot ({ release }) rendered under each release's change list.
All data display →
Preview of BoardFree

Component · Data display

Board

A flexible board component built for smooth panning, zooming with a grid layout. It supports both mouse and touch input, making it a great fit for whiteboards, diagram tools, mind maps, and workflow editors. Designed for performance and responsiveness, it delivers a fluid experience on desktop and mobile with intuitive gestures and real-time interaction on a scalable canvas.

Preview of Kanban Board$12

Component · Data display

Kanban Board

Vue 3 kanban board with native drag and drop, keyboard moves, WIP limits and inline add card. v-model board state, Tailwind CSS, dark mode.

by Vueplay
Preview of Stats CardsFree

Component · Data display

Stats Cards

Vue 3 KPI stat cards with trend deltas and tiny inline SVG sparklines, no chart library needed. Tailwind CSS, dark mode, typed props.

by Vueplay
Preview of Team GridFree

Component · Data display

Team Grid

Vue 3 team section with a responsive grid of member cards: initials avatars, role, short bio, location and social links. 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