Skip to main content
version-badge-pattern Version badge UI showing build version, git commit, and changelog in a tooltip. Use when adding version visibility for support or debugging. Works with React, Vue, Svelte, and JS.
跳到安装 Skills Marketplace 发现并探索由社区构建的 Agent Skills
用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
直接命令不会经过审查 Prompt;运行前请先检查来源。
npx skills add https://github.com/laurigates/claude-plugins --skill version-badge-pattern命令会保持在同一行。复制前请横向滚动并检查完整内容。
想先保存到本地?可下载 SkillsMP 当前能够提供的文件。
下载 Zip 下载中... 同仓库更多 Skills Install, configure and troubleshoot MCP servers. Use when adding/enabling servers, editing .mcp.json, fixing OAuth, or when a server runs stale code after an upstream fix.
FinOps snapshot — org billing, workflow stats, cache usage. Use when you want a high-level view of CI spending or workflow health before diving deeper.
GitHub Actions billing, workflow efficiency, and waste analysis at org or repo level. Use when investigating CI/CD costs, wasted runs, or optimizing triggers.
name version-badge-pattern description Version badge UI showing build version, git commit, and changelog in a tooltip. Use when adding version visibility for support or debugging. Works with React, Vue, Svelte, and JS. user-invocable false allowed-tools Bash, Read, Write, Edit, Grep, Glob, TodoWrite created "2025-02-03T00:00:00.000Z" modified "2026-05-09T00:00:00.000Z" reviewed "2026-02-08T00:00:00.000Z"
Version Badge Pattern
A reusable UI pattern for displaying application version with build metadata and recent changes.
When to Use This Skill
Use this skill when... Use alternative when...
Adding version display to app header/footer Just need version in package.json
Want tooltip with changelog info Only need static version text
Need accessible, keyboard-navigable version info Building a non-interactive display
Implementing across React/Vue/Svelte Using server-rendered only (no JS)
Pattern Overview ┌──────────────────────────────────────┐
│ App Header v1.43.0|004ddd9 ← Trigger (always visible)
└──────────────────────────────────────┘
│
▼ (on hover/focus)
┌─────────────────────────┐
│ Build Information │
│ Version: 1.43.0 │
│ Commit: 004ddd97e8... │
│ Built: Dec 11, 10:00 │
│ Branch: main │
│─────────────────────────│
│ Recent Changes │
│ v1.43.0 │
│ ✨ New feature X │
│ 🐛 Fixed bug Y │
└─────────────────────────┘
Data Flow CHANGELOG.md → parse-changelog.mjs → ENV_VAR → Component
package.json version ─────────────────────┘
git commit SHA ───────────────────────────┘
Build Script Create scripts/parse-changelog.mjs:
#!/usr/bin/env node
import { readFileSync, existsSync } from 'fs' ;
import { fileURLToPath } from 'url' ;
import { dirname, join } from 'path' ;
const __dirname = dirname (fileURLToPath (import .meta .url ));
const CHANGELOG_PATH = join (__dirname, '..' , 'CHANGELOG.md' );
const MAX_VERSIONS = 2 ;
const MAX_FEATURES = 3 ;
const MAX_OTHER = 2 ;
const CHANGE_TYPES = {
feat : { icon : 'sparkles' , label : 'Feature' },
fix : { icon : 'bug' , label : 'Bug Fix' },
perf : { icon : 'zap' , label : 'Performance' },
breaking : { icon : 'warning' , label : 'Breaking' },
refactor : { icon : 'recycle' , label : 'Refactor' },
docs : { icon : 'book' , label : 'Documentation' },
};
function parseChangelog ( ) {
if (!existsSync (CHANGELOG_PATH )) {
console .log (JSON .stringify ([]));
return ;
}
const content = readFileSync (CHANGELOG_PATH , 'utf-8' );
const lines = content.split ('\n' );
const versions = [];
let currentVersion = null ;
for (const line of lines) {
const versionMatch = line.match (/^## \[?(\d+\.\d+\.\d+)\]?/ );
if (versionMatch) {
if (currentVersion) {
versions.push (currentVersion);
}
if (versions.length >= MAX_VERSIONS ) break ;
currentVersion = {
version : versionMatch[1 ],
features : [],
fixes : [],
other : [],
};
continue ;
}
if (!currentVersion) continue ;
const changeMatch = line.match (/^\* \*\*(\w+):\*?\*? (.+)$/ );
if (changeMatch) {
const [, type, description] = changeMatch;
const changeType = CHANGE_TYPES [type.toLowerCase ()] || CHANGE_TYPES .refactor ;
const entry = {
type : type.toLowerCase (),
icon : changeType.icon ,
description : description.trim (),
};
if (type.toLowerCase () === 'feat' && currentVersion.features .length < MAX_FEATURES ) {
currentVersion.features .push (entry);
} else if (type.toLowerCase () === 'fix' && currentVersion.fixes .length < MAX_OTHER ) {
currentVersion.fixes .push (entry);
} else if (currentVersion.other .length < MAX_OTHER ) {
currentVersion.other .push (entry);
}
}
}
if (currentVersion) {
versions.push (currentVersion);
}
console .log (JSON .stringify (versions.slice (0 , MAX_VERSIONS )));
}
parseChangelog ();
React + Tailwind + shadcn/ui Implementation
Component: components/version-badge.tsx 'use client' ;
import { useMemo } from 'react' ;
import {
Tooltip ,
TooltipContent ,
TooltipProvider ,
TooltipTrigger ,
} from '@/components/ui/tooltip' ;
import { cn } from '@/lib/utils' ;
interface BuildInfo {
version : string ;
commit : string ;
branch : string ;
buildTime : string ;
}
interface ChangeEntry {
type : string ;
icon : string ;
description : string ;
}
interface VersionEntry {
version : string ;
features : ChangeEntry [];
fixes : ChangeEntry [];
other : ChangeEntry [];
}
const ICON_MAP : Record <string , string > = {
sparkles : '✨' ,
bug : '🐛' ,
zap : '⚡' ,
warning : '⚠️' ,
recycle : '♻️' ,
book : '📖' ,
};
function getIcon (iconName : string ): string {
return ICON_MAP [iconName] || '•' ;
}
export function VersionBadge ( ) {
const buildInfo = useMemo<BuildInfo | null >(() => {
try {
const raw = process.env .NEXT_PUBLIC_BUILD_INFO ;
return raw ? JSON .parse (raw) : null ;
} catch {
return null ;
}
}, []);
const changelog = useMemo<VersionEntry []>(() => {
try {
const raw = process.env .NEXT_PUBLIC_CHANGELOG ;
return raw ? JSON .parse (raw) : [];
} catch {
return [];
}
}, []);
if (!buildInfo?.version || buildInfo.version === 'dev' ) {
return null ;
}
const shortCommit = buildInfo.commit ?.slice (0 , 7 ) || 'unknown' ;
const formattedDate = buildInfo.buildTime
? new Date (buildInfo.buildTime ).toLocaleString ('en-US' , {
month : 'short' ,
day : 'numeric' ,
year : 'numeric' ,
hour : 'numeric' ,
minute : '2-digit' ,
timeZoneName : 'short' ,
})
: 'Unknown' ;
return (
<TooltipProvider >
<Tooltip delayDuration ={300} >
<TooltipTrigger asChild >
<button
className ={cn(
'text- [10px ] text-muted-foreground /60 ',
'hover:text-muted-foreground /80 transition-colors ',
'focus:outline-none focus:ring-1 focus:ring-ring focus:ring-offset-1 ',
'rounded px-1 '
)}
aria-label ={ `Version ${buildInfo.version }, commit ${shortCommit }`}
>
v{buildInfo.version} | {shortCommit}
</button >
</TooltipTrigger >
<TooltipContent
side ="bottom"
align ="end"
className ="w-72 p-0"
>
<div className ="p-3 space-y-3" >
{/* Build Information */}
<div >
<h4 className ="text-xs font-semibold mb-2" > Build Information
Version
{buildInfo.version}
Commit
{buildInfo.commit}
Built
{formattedDate}
{buildInfo.branch && (
Branch
{buildInfo.branch}
)}
{/* Recent Changes */}
{changelog.length > 0 && (
Recent Changes
{changelog.map((version) => (
v{version.version}
{[...version.features, ...version.fixes, ...version.other].map(
(change, idx) => (
{getIcon(change.icon)}
{change.description}
)
)}
))}
)}
</TooltipProvider >
);
}
Next.js Config: next.config.mjs import { execSync } from 'child_process' ;
function getBuildInfo ( ) {
const version = process.env .npm_package_version || 'dev' ;
const commit = process.env .VERCEL_GIT_COMMIT_SHA
|| process.env .GITHUB_SHA
|| execSyncSafe ('git rev-parse HEAD' )
|| 'local' ;
const branch = process.env .VERCEL_GIT_COMMIT_REF
|| process.env .GITHUB_REF_NAME
|| execSyncSafe ('git branch --show-current' )
|| 'local' ;
return { version, commit, branch, buildTime : new Date ().toISOString () };
}
function execSyncSafe (cmd ) {
try {
return execSync (cmd, { encoding : 'utf-8' }).trim ();
} catch {
return null ;
}
}
function getChangelog ( ) {
try {
return execSync ('node scripts/parse-changelog.mjs' , { encoding : 'utf-8' }).trim ();
} catch {
return '[]' ;
}
}
const nextConfig = {
env : {
NEXT_PUBLIC_BUILD_INFO : JSON .stringify (getBuildInfo ()),
NEXT_PUBLIC_CHANGELOG : getChangelog (),
},
};
export default nextConfig;
For Vue 3, Svelte, and plain CSS implementations, as well as accessibility checklist, see REFERENCE.md .
Agentic Optimizations Context Action Quick implementation Use /components:version-badge command Check compatibility /components:version-badge --check-onlyCustom placement /components:version-badge --location footer
Quick Reference Framework Env Prefix Config File Next.js NEXT_PUBLIC_next.config.mjsNuxt NUXT_PUBLIC_nuxt.config.tsVite VITE_vite.config.tsSvelteKit PUBLIC_svelte.config.jsCRA REACT_APP_N/A (eject or craco)
</h4 >
<dl className ="grid grid-cols-[auto_1fr] gap-x-3 gap-y-1 text-xs" >
<dt className ="text-muted-foreground" >
</dt >
<dd className ="font-mono" >
</dd >
<dt className ="text-muted-foreground" >
</dt >
<dd className ="font-mono truncate" title ={buildInfo.commit} >
</dd >
<dt className ="text-muted-foreground" >
</dt >
<dd >
</dd >
<>
<dt className ="text-muted-foreground" >
</dt >
<dd className ="font-mono" >
</dd >
</>
</dl >
</div >
<div className ="border-t pt-3" >
<h4 className ="text-xs font-semibold mb-2" >
</h4 >
<div className ="space-y-2" >
<div key ={version.version} >
<div className ="text-xs font-medium text-muted-foreground mb-1" >
</div >
<ul className ="space-y-0.5 text-xs" >
<li key ={idx} className ="flex gap-1.5" >
<span >
</span >
<span className ="line-clamp-1" >
</span >
</li >
</ul >
</div >
</div >
</div >
</div >
</TooltipContent >
</Tooltip >