Signal Hub logoSignal Hub

Vue.js news and articles

Vue.js
Updated just now
Date
Source

10 articles

Dev.to (Vue)
~8 min readMay 6, 2026

RTL Is Not Just dir="rtl": What I Learned Shipping Vue Templates for the Arab Web

I spent the last several months building Vue 3 templates with native RTL support. Along the way I learned that "RTL support" is one of the most overpromised, underdelivered claims in the frontend ecosystem. Most templates that advertise Arabic or RTL support do roughly this: [dir="rtl"] { direction: rtl; } Ship it. Tweet it. Add a flag emoji to the README. Then a real Arabic-speaking user opens the page and within 5 seconds notices the gradient flowing the wrong way, the chevron icon pointing into a wall, the date showing as 12/05/2024 when their entire region writes it as ٢٠٢٤/٠٥/١٢, and a card layout where the avatar floats off into nothingness because someone wrote margin-left: 16px six months ago and forgot. This article is about what actually breaks, why a 2-line CSS rule cannot fix it, and the concrete patterns I now use in production with Vue 3 and Tailwind v4. Setting dir="rtl" on the <html> tag does flip text direction. That is approximately 15% of the work. The other 85% lives in things the browser cannot guess: Directional icons: chevrons, arrows, "send" buttons, breadcrumb separators Gradients with direction: linear-gradient(to right, ...) is now wrong Transforms: translateX(-100%) slides the wrong way Animations: a sidebar that slides in from the left is now coming out of the screen Box shadows with offsets: box-shadow: 4px 0 ... casts the shadow in the wrong direction Border radius on specific corners: border-top-left-radius is now visually wrong Asymmetric layouts: anything with position: absolute; left: 0 Mixed-direction content: Arabic with embedded numbers, URLs, code, brand names None of these are fixed by dir="rtl". All of them are visible to a real user. And every single one of them is in your production code right now if you've never thought about it. If you are still on Tailwind v3 and writing ml-4, pr-2, text-left, you are writing direction-dependent CSS. Every single one of those utilities has a logical equivalent that should be your default: Direction-dependent Logical (use these) ml-4 ms-4 (margin-start) mr-4 me-4 (margin-end) pl-2 ps-2 pr-2 pe-2 text-left text-start text-right text-end left-0 start-0 right-0 end-0 border-l border-s rounded-tl-lg rounded-ss-lg Tailwind v4 promotes these to first-class citizens with cleaner naming. The s is for start (left in LTR, right in RTL) and e is for end (right in LTR, left in RTL). The migration cost is real but mechanical. A regex sweep gets you 80% of the way: # In your editor: find and replace, with regex enabled \bml- → ms- \bmr- → me- \bpl- → ps- \bpr- → pe- \btext-left → text-start \btext-right → text-end Review the diff carefully. Some ml- should not become ms- (e.g., when the design genuinely requires "left" regardless of direction, like a dedicated LTR debug panel). But those cases are rare and identifiable. Switching language should never require a reload. Here's the minimum viable setup using vue-i18n plus a composable that exposes the direction: // composables/useDirection.ts import { computed } from 'vue' import { useI18n } from 'vue-i18n' const RTL_LOCALES = new Set(['ar', 'he', 'fa', 'ur']) export function useDirection() { const { locale } = useI18n() const dir = computed(() => RTL_LOCALES.has(locale.value) ? 'rtl' : 'ltr' ) const isRTL = computed(() => dir.value === 'rtl') return { dir, isRTL, locale } } Then in your root component (or a Nuxt plugin): <script setup lang="ts"> import { watchEffect } from 'vue' import { useDirection } from '~/composables/useDirection' const { dir, locale } = useDirection() watchEffect(() => { if (typeof document !== 'undefined') { document.documentElement.dir = dir.value document.documentElement.lang = locale.value } }) </script> This is the entire infrastructure. The <html> tag updates reactively, Tailwind's logical utilities respond, and your components don't need to know about direction — they just use ms-* and pe-* and trust the cascade. Icons that point in a direction (arrows, chevrons, send buttons, breadcrumb separators) need to flip in RTL. Doing this with conditional rendering is verbose. Doing it with CSS is cleaner: <template> <button class="flex items-center gap-2"> Continue <ChevronRightIcon class="h-4 w-4 rtl:rotate-180" /> </button> </template> Tailwind v4 ships RTL variants out of the box. rtl:rotate-180 applies only when dir="rtl" is in scope. Same for ltr: if you need to scope something to LTR only. For SVG icons that come from a library, this works for most. For icons with internal asymmetric details (like a "user with arrow" combined glyph), you may need to swap the asset entirely. But for the 90% case of simple directional indicators, rtl:rotate-180 is the answer. This is the section that separates real Arabic support from fake Arabic support. Arabic-speaking users in many regions read both Western numerals (123) and Arabic-Indic numerals (١٢٣). Some prefer one over the other depending on context. Hardcoding 123 in your dashboard charts will look correct to many users, but 2,500.50 SAR looks alien when displayed alongside Arabic text — the number reads in one direction, the currency code in another. Use Intl.NumberFormat with locale awareness: const formatCurrency = (value: number, locale: string, currency: string) => new Intl.NumberFormat(locale, { style: 'currency', currency, }).format(value) formatCurrency(2500.50, 'ar-SA', 'SAR') // "٢٬٥٠٠٫٥٠ ر.س.‏" formatCurrency(2500.50, 'en-US', 'USD') // "$2,500.50" The browser knows where the currency symbol goes, which separators to use, and which numeral system fits the locale. Don't reinvent this. Same story. Intl.DateTimeFormat handles the entire Arabic, Persian, Hebrew, Hindi calendar landscape: new Intl.DateTimeFormat('ar-SA', { dateStyle: 'long' }).format(new Date()) // "٢٠ ربيع الآخر ١٤٤٧ هـ" (Hijri calendar, used in Saudi Arabia) new Intl.DateTimeFormat('ar-EG', { dateStyle: 'long' }).format(new Date()) // "٦ نوفمبر ٢٠٢٥" (Gregorian calendar, but Arabic numerals + Arabic month names) Yes, the same Arabic-speaking user in Riyadh and Cairo on the same day will see two different dates, because they use different calendars by default. Hardcoding format: 'DD/MM/YYYY' is a tell to your users that you didn't think about them. The Saudi Riyal symbol (ر.س) goes after the number in some contexts and before in others. The browser handles this. Stop fighting it. Arabic mixed with English, numbers, URLs, or code is where the Bidirectional Algorithm enters your life. By default, browsers handle this reasonably with the Unicode BiDi algorithm — but ambiguous cases break. Example: a user-generated comment in Arabic that contains a phone number with a dash: "اتصل بي على 555-1234 الآن" Without explicit BiDi marks, the dash and digits can render in unexpected order. Use Unicode Right-to-Left and Left-to-Right marks (\u200F and \u200E) or, in CSS, the unicode-bidi and direction properties on inline spans. In practice, for the 95% case where you're rendering trusted content, browsers do this correctly. For user-generated content with embedded LTR sequences, test on actual devices with actual users. If you have an existing Vue project that you want to make properly RTL-friendly, in this order: Audit your spacing utilities. Run that regex sweep from ml-/mr- to ms-/me-. Review the diff. Ship it. Audit your text alignment. text-left to text-start. Same routine. Audit your transforms and animations. Look for translateX, translate-x-*, slide-from-left keyframes. Add rtl: variants or rewrite using logical positioning. Audit your icons. Search for chevron, arrow, send, share icons. Add rtl:rotate-180 where direction matters semantically. Replace hardcoded date and number formatting. Move everything to Intl.NumberFormat and Intl.DateTimeFormat with locale awareness. Add a locale switcher and test the whole app at dir="rtl" with a real Arabic translation, not Lorem Ipsum. That last point matters. The Latin-script Lorem Ipsum used in development hides 90% of the issues that show up with actual Arabic glyphs, line height, and word breaking. Test with real translated content. The reason most templates ship broken RTL is not malice or laziness — it's that the developers never spent a day using their own template in Arabic. If you have, you'll fix the issues. If you haven't, you'll keep shipping dir="rtl" and wondering why the Saudi market doesn't convert. If you want a starting point that has already gone through this audit (Vue 3, Tailwind v4, 7 languages, real Arabic content tested by real Arabic speakers), I built QalamUI for exactly this reason. There's also a free live demo if you want to poke at the components. But more than anything: the next time you read "RTL support" on a template page, ask the seller to send you a screenshot of the dashboard with dir="rtl" and Arabic content. The answer will tell you everything. If this article saved you a debugging session, drop a comment with what you're building for the Arabic-speaking web. I'm collecting case studies of teams shipping in MENA and would love to learn from yours.

Dev.to (Vue)
~8 min readMay 5, 2026

How I Built a Scalable i18n System in Nuxt 3 — Rolling Out New Languages One Page at a Time

I run BulkPicTools — a browser-based image tools site with 38 landing pages, all processing happening locally with no file uploads. English and Chinese were live. Then came the decision to add Japanese. The naive approach: configure ja in nuxt-i18n, dump machine-translated JSON files, deploy. Done in an afternoon. The problem: half the tools weren't translated yet. And nuxt-i18n doesn't know that. It'll happily generate hreflang tags pointing to /ja/image-compressor even if that page returns a 404 — or worse, silently falls back to English content while telling Google it's Japanese. That's the kind of thing that quietly tanks your search rankings. So I built something different. A rollout system where each tool declares its own language support, and everything else — hreflang, content fallback, SEO metadata — reacts to that declaration automatically. Here's how it works. Every tool on BulkPicTools is defined by a JSON file. It already had en and zh keys: { "slug": "image-compressor", "category": "compress", "icon": "lucide:file-zip", "en": { "name": "Image Compressor", "meta": { "title": "...", "description": "..." }, "hero": { "scene": "Drag in a photo, get back a smaller one." } }, "zh": { "name": "图片压缩", "meta": { "title": "...", "description": "..." }, "hero": { "scene": "拖入图片,输出更小的文件。" } } } When the Japanese translation is ready, you add a ja key. When it's not, you don't. That's the entire "is this language ready?" signal. No config file to maintain separately. No deployment flag to flip. The data IS the feature flag. One file acts as the single source of truth for which languages the site supports: // nuxt.config.ts i18n: { locales: [ { code: 'en', language: 'en', file: 'en.json' }, { code: 'zh', language: 'zh-CN', file: 'zh.json' }, { code: 'ja', language: 'ja', file: 'ja.json' }, // Adding Korean later? Just drop this in: // { code: 'ko', language: 'ko', file: 'ko.json' }, ], defaultLocale: 'en', strategy: 'prefix_except_default', } That's the only place a new language gets registered. Everything downstream reads from useI18n().locales at runtime — nothing is hardcoded in utility functions. ja vs jp — This trips people up. ja is the ISO 639-1 language code for Japanese. jp is the ISO 3166-1 country code for Japan. hreflang wants the language. Always ja, never jp. Same pattern: Chinese is zh, not cn. By default, nuxt-i18n's useLocaleHead() generates alternate links for every configured locale. Once you add ja to your config, it starts outputting: <link rel="alternate" hreflang="ja" href="https://bulkpictools.com/ja/image-compressor" /> ...on every page, whether or not that Japanese page actually exists. Google's documentation on hreflang is pretty clear: if you declare an alternate URL, it should return the actual localized content. Pointing it at a page that either 404s or just shows English is worse than not having the tag at all. The fix is a filter function that sits between useLocaleHead() and your <head>: // utils/hreflang.ts export interface HreflangLink { rel: 'alternate' hreflang: string href: string } /** * Filters the hreflang links generated by nuxt-i18n down to only * the locales that this specific page actually supports. * * @param i18nLinks Raw links from useLocaleHead() * @param supportedCodes Locale codes this page has content for, e.g. ['en', 'zh'] * @param allLocales The full locales array from useI18n() */ export function filterHreflangLinks( i18nLinks: any[], supportedCodes: string[], allLocales: { code: string; language?: string; iso?: string }[], ): HreflangLink[] { // Build a map from locale code → hreflang value // e.g. { en: 'en', zh: 'zh-CN', ja: 'ja' } const codeToHreflang = Object.fromEntries( allLocales.map((l) => [l.code, l.language ?? l.iso ?? l.code]) ) const supportedHreflangs = supportedCodes.map((code) => codeToHreflang[code]) return (i18nLinks ?? []).filter((link) => link.hreflang === 'x-default' || supportedHreflangs.some((h) => link.hreflang?.startsWith(h)) ) } The function doesn't know or care which languages exist. It just takes what nuxt-i18n generates and filters it against the list you pass in. When you add Korean later, nothing here needs to change. Each tool landing page reads its own JSON, checks which locale keys exist, and uses that list for both hreflang and content: <script setup lang="ts"> import { filterHreflangLinks } from '~/utils/hreflang' // tool is the raw JSON for this page const { locale, locales } = useI18n() const i18nHead = useLocaleHead({ addSeoAttributes: true }) // The supported locales come directly from the JSON structure. // If 'ja' key exists in the JSON → Japanese is supported. // If not → it won't appear in hreflang, and content falls back to 'en'. const supportedLocales = Object.keys(tool).filter((k) => typeof tool[k] === 'object' && tool[k]?.meta ) // Fallback chain: current locale → English → empty object const activeLocale = computed(() => supportedLocales.includes(locale.value) ? locale.value : 'en' ) const localeMeta = computed(() => tool[activeLocale.value]) // SEO — uses the fallback locale, so Japanese visitors get English // meta rather than undefined crashing the page useSeoMeta({ title: "localeMeta.value.meta.title," description: "localeMeta.value.meta.description," ogTitle: localeMeta.value.meta.title, ogDescription: localeMeta.value.meta.description, }) // hreflang — only outputs languages that actually have content definePageMeta({ customHreflang: true }) useHead({ link: computed(() => filterHreflangLinks(i18nHead.value.link, supportedLocales, locales.value) ), }) </script> The customHreflang: true flag tells app.vue to skip the automatic hreflang injection for this page — the page handles it itself. In app.vue: link: [ // If the page sets customHreflang, trust it to handle its own hreflang. // Otherwise, let nuxt-i18n do it automatically (fine for pages // where all languages are always available, like the homepage). ...(route.meta.customHreflang ? [] : i18nHead.value.link || []), { rel: 'canonical', href: canonicalUrl.value }, // ...favicon etc ] Getting hreflang right is the SEO half. The other half is making sure your Vue components don't crash when a locale key is missing. Here's the kind of thing that breaks silently in dev and loudly in production: <!-- This explodes if tool['ja'] is undefined --> <p v-if="tool[locale].hero.scene"> {{ tool[locale].hero.scene }} </p> locale in the template auto-unwraps to a string like 'ja'. If tool['ja'] doesn't exist, you're calling .hero on undefined. Vue will throw, the component won't render, and you'll spend 20 minutes wondering why a page that looks fine in English is broken in Japanese. The fix is a small helper function in the component's <script setup>: // Safe accessor — falls back to 'en' if the requested locale isn't in the JSON function getScene(tool: ProcessedTool): string { const content = tool[locale.value] || tool['en'] return content?.hero?.scene ?? '' } Then in the template: <p v-if="getScene(tool)" class="..."> {{ getScene(tool) }} </p> This pattern — "try the current locale, fall back to English, then use optional chaining" — is worth applying anywhere you access locale-specific nested data in a template. It's more defensive than it looks because you're protecting against two failure modes at once: the locale key not existing and the nested property not existing. The composable that processes all tool data has a similar trap. When building category lists, it was spreading locale data directly: // Dangerous — meta['ja'] is undefined if _meta.json has no 'ja' key result.push({ slug: meta.slug, ...meta[currentLang] // 💥 spreading undefined }) Spreading undefined doesn't always throw immediately — sometimes it just silently produces an object missing all the localized fields. The fix is a tiny helper that normalizes the fallback: // Inside useTools() const getLangContent = (obj: any, lang: string) => { return obj[lang] || obj['en'] || {} } // Now safe everywhere result.push({ slug: meta.slug, ...getLangContent(meta, currentLang) }) The || {} at the end is load-bearing. Without it, if somehow neither the requested language nor English exists, you'd spread undefined. With it, you get an empty object and the UI renders with empty strings instead of crashing. When the Japanese translation for a tool is done — say, the image compressor — the change is: Add the ja key to that tool's JSON file Deploy That's it. No config changes. No feature flags. No separate "ready tools" list to maintain. The hreflang for that page automatically updates from: <link rel="alternate" hreflang="en" href="..." /> <link rel="alternate" hreflang="zh-CN" href="..." /> <link rel="alternate" hreflang="x-default" href="..." /> to: <link rel="alternate" hreflang="en" href="..." /> <link rel="alternate" hreflang="zh-CN" href="..." /> <link rel="alternate" hreflang="ja" href="..." /> <link rel="alternate" hreflang="x-default" href="..." /> The SEO metadata switches to Japanese. The content renders in Japanese. Everything reacts to the single source of truth in the JSON. When Korean is ready to roll out, the full change is: nuxt.config.ts — add one locale entry: { code: 'ko', language: 'ko', file: 'ko.json' } /locales/ko.json — the UI strings (nav, buttons, shared copy) Each tool JSON — add ko: { ... } when the translation is ready filterHreflangLinks, getLangContent, getScene, app.vue — untouched. They all read from the locale config at runtime. There's nothing to update. The whole system is about 60 lines of utility code. The payoff is that adding a language to a page is a data change, not a code change — and that distinction matters a lot when you're doing it 38 times. If you want to see the end result, BulkPicTools is live with this exact setup. The Japanese rollout is ongoing — you can spot which tools are done by checking whether /ja/tools/... serves localized content or quietly falls back to English while the hreflang stays clean.

Dev.to (Vue)
~5 min readMay 3, 2026

From Modal to Full Page: How We Refactored a Vue 3 Recipe Detail View

One of the longest-lived technical decisions in our recipe finder app was showing recipe details inside a dialog modal. It worked — until it didn't. Here's how we migrated from a bloated modal to a clean, SEO-friendly full page, what we cut along the way, and what the app looks like now. Demo: https://recipe-finder.org/recipe/644488-german-rhubarb-cake-with-meringue The original setup opened a <v-dialog> when a user clicked a recipe card. The modal held the entire recipe detail UI: ingredients, nutrition, videos, AI chef, grocery import, recipe scaler — all of it. The logic for opening it, fetching the recipe, and handling deep-link slugs lived inside HomePage.vue. <!-- Old: HomePage.vue controlled everything --> <RecipeDetailsModal :is-open="isRecipeModalOpen" :recipe="selectedRecipeDetails" :loading="loadingRecipeDetails" @close="closeModal"/> The problem was that HomePage.vue had become a god component. It managed: The search form and results Cuisine carousel Recipe of the day Recent recipes And the modal open/close state, slug parsing, and detail fetch On top of that, because everything was in a modal, the URL never changed. Users couldn't share a link to a specific recipe, Google couldn't index the content, and the Back button did nothing useful. We created /recipe/:slug as a proper route and moved the recipe detail logic into a standalone RecipeDetailPage.vue. // router/index.ts { path: '/recipe/:slug', component: () => import('@/pages/RecipeDetailPage.vue'), meta: { title: 'Recipe' } } Slugs are derived from the recipe ID and title, making them human-readable and stable: // utils/index.ts export const toRecipeSlug = (id: number, title: string): string => { const slug = title .toLowerCase() .replace(/[^a-z0-9]+/g, '-') .replace(/(^-|-$)/g, ''); return `${id}-${slug}`; }; export const extractRecipeIdFromSlug = (slug: string): number | null => { const id = parseInt(slug.split('-')[0]); return isNaN(id) ? null : id; }; Once the page was independent, we stripped HomePage.vue of everything modal-related. Gone: isRecipeModalOpen ref selectedRecipeDetails and loadingRecipeDetails state The openRecipeModal / closeModal handlers The slug-watching watch that re-fetched on URL change The <RecipeDetailsModal> import and component registration The handleOpenRecipeDetails function passed down through three component layers The result was HomePage.vue shrinking by roughly 40% in script size. It now does one thing: show the search form and results. The page uses a standard Vuetify two-column grid — main content on the left, sticky sidebar on the right (desktop only). On mobile, the sidebar collapses and the tools surface inline. <v-row> <!-- Left: main content --> <v-col cols="12" lg="8"> <v-img :src="recipe.image" cover rounded="lg" class="mb-6" /> <h1 class="recipe-title">{{ recipe.title }}</h1> <!-- chips, rating, action buttons, summary, ingredients, instructions, nutrition --> </v-col> <!-- Right: sticky sidebar (desktop only) --> <v-col cols="12" lg="4" class="d-none d-lg-flex flex-column"> <div class="sidebar-sticky"> <!-- Recipe Tools card --> <!-- AI Cooking Chef card --> <!-- Nutrition Snapshot card --> </div> </v-col> </v-row> The sidebar cards use a glass morphism style that matches the rest of the app: .glass-card { border: 1px solid rgba(255, 255, 255, 0.1); background: radial-gradient(circle at top right, rgba(255, 163, 92, 0.08), transparent 40%), linear-gradient(165deg, rgba(255, 255, 255, 0.07), rgba(255, 255, 255, 0.02)); border-radius: 16px !important; } On mobile, the page can't show a sidebar. We solved this in two ways: Action buttons become icon-only on small screens, matching how the modal used to look: <!-- Mobile: icon-only --> <div class="d-flex d-sm-none gap-2 mb-6 align-center"> <v-btn icon variant="tonal" size="small" @click.stop="handleToggleFavoriteRecipe"> <v-icon>{{ isFavorited ? 'mdi-heart' : 'mdi-heart-outline' }}</v-icon> </v-btn> <v-btn icon variant="tonal" size="small" @click="shareRecipe"> <v-icon>{{ isCopied ? 'mdi-check' : 'mdi-share-variant' }}</v-icon> </v-btn> <v-btn icon variant="tonal" size="small" @click="printRecipe"> <v-icon>mdi-printer</v-icon> </v-btn> </div> <!-- Desktop: text buttons --> <div class="d-none d-sm-flex flex-wrap gap-2 mb-6"> <v-btn prepend-icon="mdi-heart-outline" variant="tonal" class="page-action-btn"> Add to Favorites </v-btn> <!-- ... --> </div> The AI chef becomes a bottom sheet triggered by a text button inline with the ingredient tools (no floating action button cluttering the screen): <!-- Ingredient row: Scale / Analyze / Grocery / AI Chef --> <v-btn color="primary" variant="text" size="small" prepend-icon="mdi-robot-excited" @click="showAiSheet = true"> AI Chef </v-btn> <v-bottom-sheet v-model="showAiSheet"> <!-- full AI interface --> </v-bottom-sheet> Since the content is now on a real URL, we inject <title> and Open Graph meta tags dynamically on load: const injectMetaTags = (title: string, summary: string, imageUrl: string) => { const pageTitle = `${title} | Recipe Finder`; document.title = pageTitle; const setMeta = (selector: string, attr: string, value: string) => { document.querySelector(selector)?.setAttribute(attr, value); }; setMeta("meta[property='og:title']", 'content', pageTitle); setMeta("meta[name='twitter:title']", 'content', pageTitle); if (summary) { const clean = summary.replace(/<[^>]*>/g, '').trim().slice(0, 155); setMeta("meta[name='description']", 'content', clean); setMeta("meta[property='og:description']", 'content', clean); } if (imageUrl) { setMeta("meta[property='og:image']", 'content', imageUrl); } }; This is called inside a watch on the recipe computed ref, so it fires on both initial load and when navigating between similar recipes. Every non-critical UI piece is lazy-loaded: const ImportToGroceryList = defineAsyncComponent( () => import('@/components/ImportToGroceryList.vue') ); const PremiumUpgradeDialog = defineAsyncComponent( () => import('@/components/PremiumUpgradeDialog.vue') ); const RecipeScaler = defineAsyncComponent( () => import('@/components/RecipeScaler.vue') ); const RecipeEmbedWatermark = defineAsyncComponent( () => import('@/components/RecipeEmbedWatermark.vue') ); The main recipe content renders immediately. The grocery dialog, scaler, embed widget, and upgrade dialog only load if the user actually interacts with them. One regression we caught: clicking "Send to Grocery List" while logged out was calling getAccessTokenSilently() and throwing an Auth0 missing refresh token error. Fixed by checking auth state before opening the dialog: const onDialogToggle = (open: boolean) => { if (open) { if (!isAuthenticated.value) { dialogOpen.value = false; loginWithRedirect(); return; } loadGroceryLists(); } }; Metric Before After HomePage.vue Script Size ~650 lines ~390 lines Recipe URL Shareable ❌ ✅ Google Indexable ❌ ✅ Mobile Layout Fullscreen modal Native page with bottom sheet Auth Crash on Grocery ⚠️ Existed ✅ Fixed AI on Mobile Floating button Inline trigger → bottom sheet The modal still exists for the recipe list view (quick-peek without leaving the page), but the canonical experience is now a proper page. The code is cleaner, the app is faster to load, and every recipe finally has a real URL. Built with Vue 3, Vuetify 3, TypeScript, and Tailwind CSS. Auth via Auth0.

Dev.to (Vue)
~5 min readMay 2, 2026

NanoStores: A Tiny Redux Alternative for React, Vue, Svelte, and More

If Redux ever felt like too much ceremony for a small or mid-sized app, Nano Stores is worth a look. It is a tiny state manager built around small atomic stores instead of one big global store. The idea is simple: keep state close to the feature, derive what you need, and avoid reducers, action types, and framework lock-in unless you actually need them.[1] There are a lot of state libraries that promise to be "simple," but Nano Stores earns that label in a very literal way. According to the official README, the core package is between 294 and 831 bytes minified and brotlied, depending on what parts you import, and it has zero dependencies.[1] The published package also marks specific size limits like 294 B for atom and 831 B for a more common map + computed set. That alone does not make it better than Redux, Zustand, Jotai, or Vue state tools. But it does make the trade-off very clear: Nano Stores is optimized for people who want a very small, tree-shakable state layer with direct APIs and no extra ceremony.[1] Nano Stores is based on atomic stores. Instead of centralizing every update behind one global reducer, you create small focused stores for individual concerns, then derive data from them with helpers like computed().[1] That leads to a different development style: create one store for one concern update it directly with set() or setKey() derive new values with computed() subscribe from your UI with framework-specific helpers If you already like the "small pieces loosely joined" approach in modern frontend architecture, Nano Stores will feel natural. Here is the verified feature set that matters most. Nano Stores' README describes the library as tree-shakable, meaning a bundle includes only the stores actually used by the components in that chunk.[1] That matters if you care about keeping client JavaScript lean, especially in multi-page apps, islands architecture, or component-heavy frontends. This is one of the more practical parts of the project. The official README documents integrations for: React and Preact Vue Svelte Solid Lit Angular Alpine.js vanilla JavaScript SSR workflows[1] That makes Nano Stores more interesting than a "React-only Redux replacement." It is really a small cross-framework state primitive. Nano Stores does not require reducers or action objects for routine state changes.[1] That means you can write updates in a much more direct style: import { atom, computed } from 'nanostores' export const $users = atom<{ name: string; isAdmin: boolean }[]>([]) export const $admins = computed($users, users => { return users.filter(user => user.isAdmin) }) export function addUser(user: { name: string; isAdmin: boolean }) { $users.set([...$users.get(), user]) } That snippet is very close to the official examples, and it shows the library's pitch clearly: define state, derive state, and mutate state without a big abstraction wall in the middle.[1] One genuinely useful feature here is mount-aware stores. Nano Stores supports onMount() so a store can start work only when something is actually listening to it, then clean up later when it is unused.[1] That is a nice fit for things like: timers subscriptions URL listeners data loading browser-only side effects This is a practical feature, not just a theoretical one. If you are coming from Redux, the "how do I use this in a component?" question matters more than the philosophy. Nano Stores' React integration uses @nanostores/react and a useStore() hook.[1] import { atom } from 'nanostores' import { useStore } from '@nanostores/react' const $count = atom(0) function increment() { $count.set($count.get() + 1) } export function Counter() { const count = useStore($count) return <button onClick={increment}>Count: {count}</button> } That is the kind of example where Nano Stores clicks for people. There is almost nothing to learn beyond the store API itself. I would not frame Nano Stores as "Redux, but smaller" and stop there. That undersells the real difference. The better comparison is this: Redux gives you a structured global update model with strong ecosystem conventions Nano Stores gives you a tiny reactive state layer with atomic pieces and very little ceremony Nano Stores will likely feel better when: your app does not need a large global event architecture you want state shared across components without introducing heavy tooling you care about bundle size you work across multiple frameworks you prefer direct mutations through small store APIs over reducers and action plumbing This is not a magic library. The project explicitly documents Nano Stores as an ESM-only package. If your environment still has older CommonJS assumptions or tricky framework tooling, you should verify compatibility first.[1] Redux can feel verbose, but that verbosity also creates a predictable architecture for large teams. Nano Stores gives you more freedom. That is good for speed, but it also means you need your own discipline around: store boundaries naming side effects testing Small size is great, but library choice should still depend on your app shape. If your team benefits from time-travel debugging, strict event modeling, middleware-heavy workflows, or a very opinionated architecture, Nano Stores may feel too lightweight. The repo itself gives a clue here. At the time of writing, the project has about 7.3k GitHub stars, and the repository page shows an actively maintained project with recent releases, including v1.3.0 listed in April 2026. That does not prove technical superiority, but it does suggest Nano Stores has found a real audience among developers who want small, framework-friendly state management without Redux-level ceremony. Nano Stores looks compelling for the same reason many developers moved away from heavyweight state tools in the first place: most apps do not need a complicated state architecture on day one. What Nano Stores offers is pretty clear: very small footprint atomic store model no reducers or action boilerplate support across React, Vue, Svelte, and more useful primitives like computed(), onMount(), and framework adapters[1] If you want a super lightweight alternative to Redux, Nano Stores is one of the most credible options in that space. Not because it tries to do everything, but because it very intentionally does less. [1] Nano Stores GitHub. https://github.com/nanostores/nanostores

Dev.to (Vue)
~3 min readMay 1, 2026

Building a Self-Healing SEO Architecture for a Vue SPA

Building a modern Single Page Application (SPA) with Vite and Vue is great for user experience, but it's a minefield for SEO. We faced three major hurdles: Aggressive Bot Protection: Our .htaccess was so tight it was blocking crawlers that we actually wanted. The "SPA Meta Trap": Social media bots (Facebook, WhatsApp) couldn't read our dynamic recipe titles or images because they don't execute JavaScript. The Scale Problem: We have access to millions of recipes via the Spoonacular API, but we don't own the full database. How do you tell Google about millions of pages you don't physically store? Frontend: Vite + Vue 3 (Hosted on Apache) Backend: Node.js + Express (Hosted on Firebase Functions) Database: MongoDB Provider: Spoonacular API Since our frontend is on a standard Apache host, we couldn't use edge functions easily. Instead, we optimized our URL structure to include SEO-friendly slugs: recipe-finder.org/recipe/644488-german-rhubarb-cake-with-meringue We then implemented a backend-driven meta-injection strategy. When a recipe is requested, our Express server pre-fills the Open Graph tags (og:title, og:image, og:description) using the recipe summary, ensuring beautiful previews on social media. We didn't want to scrape millions of recipes (and get banned). Instead, we created an Organic Growth Engine. Every time a user (guest or authenticated) clicks a recipe, our Express backend performs an Upsert into MongoDB. If it's a new recipe, it's added to our "SEO Index." If it's an existing one, we update the lastViewed timestamp. // Remove stale entry, then push back to front with a fresh timestamp await recipeViewedModel.findOneAndUpdate( { auth0Id }, { $pull: { recipes: { id: recipe.id } } } ); await recipeViewedModel.findOneAndUpdate( { auth0Id }, { $push: { recipes: { $each: [{ ...recipe, viewedAt: new Date() }], $position: 0 } } }, { upsert: true, new: true } ); This ensures our database only grows with high-quality, relevant content that users actually care about. A static sitemap.xml was impossible for millions of potential links. We built a Dynamic Sitemap Index: sitemap-main.xml — A static file on our hosting server for core pages (Home, Tools, About). sitemap-recipes-[n].xml — Dynamic routes on Express that query MongoDB and generate XML on the fly in 50,000-unit chunks. The Master Index — A central sitemap.xml that bridges the two, served via a silent proxy in .htaccess. RewriteRule ^sitemap\.xml$ [https://your-region-your-project.cloudfunctions.net/api/sitemap.xml](https://your-region-your-project.cloudfunctions.net/api/sitemap.xml) [R=301,L] RewriteRule ^sitemap-recipes-([0-9]+)\.xml$ [https://your-region-your-project.cloudfunctions.net/api/sitemap-recipes-$1.xml](https://your-region-your-project.cloudfunctions.net/api/sitemap-recipes-$1.xml) [R=301,L] Any request for a sitemap is silently routed to the Express API, which assembles the XML from MongoDB on the fly — no static file maintenance required. Google Search Console Verified: Live URL testing shows Google successfully rendering the SPA and reading the dynamic content. Automated SEO: The more our users cook, the larger our sitemap grows. We don't have to manually add a single link. Zero-Maintenance Scaling: The system handles 10 recipes or 10 million with the same memory footprint thanks to MongoDB's $group and $limit aggregations. Don't build for millions of pages on Day 1. Build a system that lets your users' activity grow your SEO footprint for you. Work with the bots, not against them. Author: Rusu Ionut Project: recipe-finder.org

Dev.to (Vue)
~9 min readMay 1, 2026

BoldKit Spring 2026: v3.0 v3.2.2 - ASCII Animations, Canvas Effects, Dot Matrix Studio & More

Spring 2026 has been the most productive stretch in BoldKit's history — the free neubrutalism UI component library for React and Vue 3. Six weeks, five major version bumps (v2.6 → v3.2.2), and more new capabilities than any previous release cycle. Here's everything that shipped. The homepage got a full editorial neubrutalism overhaul. The design direction: bold display typography, tight grid discipline, and strong color contrast — no gradients, no rounded corners, no safe choices. What changed: Bebas Neue + DM Mono — Bebas Neue for all display headlines (clamp(56px, 14vw, 180px)), DM Mono for code snippets and labels Component collage hero — a floating grid of live BoldKit components sits beside the headline on desktop, giving visitors an instant visual feel for the library Stats bar — 4 colored cells (50+ components, 10 charts, 45 shapes, MIT licensed) using the full neubrutalism vocabulary: border-3, 4px offset shadows, zero border-radius Component showcase — asymmetric grid showing cards, badges, buttons, charts, and shapes in a dense, editorial layout Dark CTA section — bg-foreground background, high contrast, GitHub and docs CTAs side by side The font stack ships via <link> in index.html (not CSS @import) to avoid PostCSS ordering constraints with Tailwind v4. Several overflow and layout issues were fixed across breakpoints: Hero overflow below 518px — the CLI command snippet was overflowing on very small screens. Fixed by adding min-w-0 to the <code> element and w-full min-w-0 to its wrapper, removing the fixed max-w Stats bar mobile border — the item at grid position [1] had a right border that appeared mid-row on 2-column mobile layout; made it md:border-r-3 only Component showcase tablet — added md:grid-cols-2 lg:grid-cols-3 with md:col-span-1 overrides so the layout works properly at all widths Global scrollbar hiding — scrollbar-width: none + *::-webkit-scrollbar { display: none } applied globally in globals.css 🧩 Empty State: Animations, Layouts, and New Presets The Empty State component had undocumented features. All of them are now fully documented with live demos. Animations — three entrance animation variants — fadeIn, bounce, and scale — now have live interactive demos with a Replay button so you can see them fire on demand: <EmptyState preset="no-results" animation="bounce" /> Layouts — both vertical (default) and horizontal layouts are now documented with side-by-side examples. 14 presets are fully covered: no-results, no-data, error, offline, permission-denied, coming-soon, maintenance, upload, no-notifications, no-messages, no-favorites, no-activity, empty-cart, and success. Plus success, warning, and destructive icon color variants. Every ExampleSection on the BoldKit docs previously showed only React code. All 34 component doc pages that were missing Vue examples now have them: Dialog · Drawer · DropdownMenu · Pagination · Popover · Progress · RadioGroup · ScrollArea · Select · Separator · Sheet · Skeleton · Sonner · Sticker · Switch · Table · Tabs · Textarea · Toggle · ToggleGroup · Tooltip · AlertDialog · AspectRatio · Avatar · Breadcrumb · Calendar · Collapsible · Command · HoverCard · InputOtp · Label · LayeredCard · Marquee · EmptyState Stateful examples use <script setup> with ref(): <script setup> import { ref } from 'vue' const open = ref(false) </script> <template> <Dialog v-model:open="open"> ... </Dialog> </template> Header — full neubrutalism upgrade: 3px coral accent stripe full-width above the nav Bebas Neue wordmark; icon rotates -6° on hover Active nav: 3px primary underline (matches top stripe weight) Square border-3 theme toggle and hamburger that invert on hover Bordered search pill: icon + Search... label + ⌘K kbd badge Mobile: full-screen overlay, body scroll lock, env(safe-area-inset-bottom) for notched iPhones Footer — restructured into three sections: Always-dark hero with ghost BUILD BOLD watermark in Bebas Neue Side-by-side React and Vue 3 install cards with framework branding 4-column links grid with DM Mono bottom bar The chart library expanded with four advanced chart types, all styled in full neubrutalism with React and Vue 3 examples: Chart Description FunnelChart Conversion funnels, sales pipelines TreemapChart Hierarchical area visualization HeatmapChart Two-dimensional intensity grids SankeyChart Flow and allocation diagrams Install any chart via the shadcn CLI: npx shadcn@latest add "https://boldkit.dev/r/funnel-chart.json" All 45+ BoldKit shapes now support animation props: <StarShape animation="float" // spin | pulse | float | wiggle | bounce | glitch speed="normal" // slow | normal | fast /> 7 animation variants, 3 speeds, pure CSS keyframes — no JS animation libraries required. A live showcase section was added to the /shapes page. The Theme Builder got an upgraded card UI showing a 3-color strip preview, preset name, and descriptor tag. 14 curated presets are now available out of the box — covering everything from Cyberpunk to Pastel to Monochrome. A new Documentation Site template was added to /templates — header, sidebar navigation, content area, and table of contents, all in neubrutalism style. Three new components driven by real parametric math — no third-party dependencies, pure SVG paths computed via a shared math engine: MathCurveLoader — animated loading spinner with 8 curve variants: <MathCurveLoader curve="lissajous" // rose | lissajous | butterfly | hypotrochoid | // cardioid | lemniscate | fourier | rose3 size="md" // sm | md | lg | xl speed="normal" // slow | normal | fast /> MathCurveProgress — value-driven progress indicator (0–100) across 5 open curves: spiral, heart, lissajous, cardioid, rose. MathCurveBackground — ambient animated background layer wrapping children via asChild. Four slow ambient curves playing simultaneously. All three ship for both React and Vue 3 / Nuxt. An interactive tool at /shapes/builder — pick any of the 54 SVG shapes, adjust props visually (color, stroke, fill, size, animation), preview live, and copy the exact component code. No more guessing prop combinations. The headline feature of v3.1.0. A complete ASCII animation engine built from scratch — no canvas, no WebGL, just text characters rendered at up to 60fps using real 3D math. 5 three-dimensional shapes using perspective projection, z-buffering, and Lambertian shading: Component Description AsciiTorus Rotating torus — X+Y axes AsciiDonut Classic donut.c — X+Z axes, fatter tube AsciiSphere Globe with rotating lat/lon grid and dark side AsciiCube Solid cube with back-face culling and per-face shading AsciiHelix DNA double helix — two parametric strands with rungs 7 generative 2D animations drawn into a character grid each frame: Component Description AsciiSpiral Archimedean spiral arms rotating continuously AsciiRose Rose curve r=cos(5θ) blooming and phase-shifting AsciiWave Multi-frequency sine interference scrolling left→right AsciiVortex Rotating density field collapsing toward center AsciiPulse Concentric rings expanding outward and fading AsciiMatrix Characters raining downward per column AsciiGrid Grid intersections pulsing with traveling waves Shared prop API across all 12 components: <AsciiTorus size="md" // sm (24×12) | md (48×24) | lg (72×36) | hero (120×60) charset="blocks" // blocks | braille | classic | line | dots speed="normal" // slow (0.4×) | normal (1×) | fast (2.2×) color="#e74c3c" // any CSS color; ignored when multicolor=true multicolor // cycles primary→secondary→accent→warning→info→success per row animated={false} // static first frame — fully SSR-safe, no RAF /> Nuxt support: animated={false} works without <ClientOnly>. Animated variants need <ClientOnly> to avoid hydration mismatch. # React npx shadcn@latest add "https://boldkit.dev/r/ascii-shapes.json" # Vue 3 / Nuxt npx shadcn-vue@latest add "https://boldkit.dev/r/vue/ascii-shapes.json" 7 new parametric curves added to all three Math Curve components: Curve Formula astroid x=cos³(t), y=sin³(t) deltoid 3-cusped hypocycloid nephroid 2-cusped epicycloid (R=2, r=1) epicycloid General epicycloid superellipse Lamé curve with n=2.5 triskelion 3-armed rotational spiral involute Involute of a circle Previously 8 curves, now 15 total. 10 new shapes added, including a new Mathematical category: Geometric: HeptagonShape, DecagonShape, RhombusShape, EllipseShape, TrefoilShape Mathematical: FibonacciSpiralShape, PenroseTriangleShape, KochSnowflakeShape, MobiusStripShape, TorusShape Previously 54, now 64 total. New component with three icon variants, keyboard navigation, and half-step precision: <Rating value={3.5} max={5} icon="star" // star | heart | circle size="md" // sm | md | lg | xl precision={0.5} // 1 | 0.5 readOnly aria-label="Product rating" /> Full keyboard navigation (arrow keys), aria-readonly support, configurable aria-label. npx shadcn@latest add "https://boldkit.dev/r/rating.json" A full in-browser pixel art and animation editor at /studio — built entirely with BoldKit components. Drawing tools: Pencil, eraser, shapes (rectangle, ellipse, line), text tool Selection with Fill, Clear, and Invert actions Undo/redo stack Drag-and-drop image import Animation system: 10 presets: Blink, Typewriter, ScanLine, Marquee, Ripple, Bounce, Slide, Wave, Rain, Fade Frame-by-frame editing Looping playback with reliable frame timing Export options: WebM video PNG (single frame or spritesheet) SVG JSON (for re-import) UX details: localStorage persistence — your work survives a page refresh Share Tech Mono font for the authentic pixel-art terminal feel Pastel indigo theme 10 zero-dependency animated canvas components, copy-pasteable for React, Vue 3, and Nuxt 3: Effect Description Aurora Northern lights gradient ribbons DotBlob Organic morphing dot field DotWave Wave-propagating dot grid FlowField Perlin noise vector field LissajousGrid Animated Lissajous curve grid MatrixRain Classic character rain Metaballs Organic merging blobs MouseRipple Ripple rings following the cursor ParticleWeb Connected particle network Plasma Sine-wave plasma color field Each ships with: Full React and Vue 3 implementations (no runtime dependencies) shadcn CLI registry entries for one-command install Per-card install command copy buttons Live demos on the /canvas-effects page # React npx shadcn@latest add "https://boldkit.dev/r/aurora.json" # Vue 3 / Nuxt npx shadcn-vue@latest add "https://boldkit.dev/r/vue/aurora.json" A dedicated accessibility pass across the component library: Slider — aria-label and aria-valuetext props on thumbs; fixes stale closure in onValueCommit Rating — configurable aria-label; aria-readonly attribute when readOnly Spinner — role="status" and aria-label="Loading" defaults across all 5 variants Skeleton — aria-hidden="true" by default Pagination — PaginationEllipsis gets role="img" so its aria-label is exposed to screen readers InputOTP — InputOTPSeparator gets role="separator"; InputOTPGroup gets role="group" Breadcrumb — removed invalid role="link" and aria-disabled from BreadcrumbPage TreeView — improved expand/collapse labels; ArrowUp/ArrowDown keyboard navigation Tour — role="dialog", aria-modal, aria-labelledby, and Escape key to close All 10 canvas effects now apply devicePixelRatio scaling in their resize() functions — canvas components are sharp on Retina and HiDPI displays instead of blurry at 2× or 3× pixel density. // Before: blurry on HiDPI canvas.width = w; canvas.height = h; // After: sharp everywhere const dpr = window.devicePixelRatio || 1; canvas.width = w * dpr; canvas.height = h * dpr; ctx.setTransform(dpr, 0, 0, dpr, 0, 0); Since March 2026, BoldKit grew from: March May UI Components 50+ 55+ Charts 10 14 SVG Shapes 45 64 Math Curves — 15 ASCII Art Components — 12 Canvas Effects — 10 Tools ThemeBuilder ThemeBuilder + Shape Builder + Dot Matrix Studio Version 2.6.x 3.2.2 React: npx shadcn@latest add "https://boldkit.dev/r/[component].json" Vue 3 / Nuxt: npx shadcn-vue@latest add "https://boldkit.dev/r/vue/[component].json" Links: 🌐 boldkit.dev ⭐ GitHub 📖 Docs 🎨 Canvas Effects 🖥️ Dot Matrix Studio If you're building with neubrutalism design — thick borders, hard shadows, sharp corners, bold type — BoldKit has everything you need for both React and Vue 3. Give it a star if you find it useful! ⭐

Dev.to (Vue)
~5 min readApr 30, 2026

Why God Kit? A Lightweight Vue 3 UI Library and Design System for Admin Apps

Why God Kit? A Lightweight Vue 3 Component Library Built for Real Dashboards If you are evaluating Vue UI libraries, Vue component frameworks, or a complete design system for Vue 3, you have strong options—from full design-language suites with hundreds of primitives to minimal headless kits. God Kit sits in a different niche: admin and internal tools that need consistent design tokens, small bundles, and a steady path away from legacy UI—including dense data tables and dashboard chrome—without carrying a massive runtime. This post explains what God Kit is, why teams choose it over heavier Vue component libraries, and where to find documentation, source, and npm. We also share where the project is going: a visual UI builder and a growing ecosystem around Vue 3 and Nuxt 4. Resource URL Documentation & live component docs https://godkit.godplans.org/ GitHub (source, issues, contributing) https://github.com/god-plans/god-kit npm package https://www.npmjs.com/package/god-kit Sponsor / fund the project https://github.com/sponsors/god-plans God Kit is an open source Vue 3 UI kit and design system for teams building dashboards, back-office tools, and Nuxt 4 apps. It ships: Typed Gk* components (forms, containment, navigation, data table, feedback, layout) Semantic design tokens (--gk-* CSS variables) for light/dark, density, and theming Subpath exports so you import only what you need—friendly to tree-shaking and smaller bundles than “install everything” suites createGkKit-style global configuration for theme, display, locale, and defaults A CLI to scaffold components (npx god-kit add …) aligned with kit conventions Accessibility coverage on key primitives (e.g. axe-based specs in the repo) Optional bridge stylesheets in the repo for incremental migration from older component layers when you cannot rewrite every screen at once It is MIT-licensed and aimed at developers who want simple, lightweight Vue components with a token-first workflow—not necessarily every marketing-site block in a single package. Many popular guides follow the same recipe: open community, deep component catalogs, layouts, tooling, and long-term support. That pattern works beautifully when you want the fullest possible surface area and a single-vendor design language end to end. God Kit takes the same spirit—clarity, docs, ecosystem—but narrows the product: If you are searching for… God Kit emphasizes… Lightweight Vue UI library Smaller surface area; admin-first components; subpath imports Vue 3 component library with design tokens Semantic tokens and themes, not scattered one-off colors Nuxt 4 UI components Documented CSS order, SSR-minded usage, explicit imports Incremental migration from a legacy UI stack Bridge + cookbook mindset: replace screens in phases, not one risky big bang Best Vue UI framework (for your dashboard) Predictable TypeScript APIs, Gk* prefix, changelog discipline—not the widest marketing-site catalog So if your keywords are Vue component library, design system Vue, admin dashboard Vue 3, lightweight Vue components, or Nuxt 4 component kit, God Kit is meant to match that intent honestly: a complete enough design system for internal products, not a contest for the longest feature grid on npm. Vue 3 + TypeScript — Composition API components with exported types. Design system that works — Tokens, themes (presets + registration), RTL-aware documentation. Lightweight by design — Import from god-kit/vue/form, god-kit/vue/layout, and related subpaths instead of the whole barrel. CLI for components — Scaffold files that follow project conventions (god-kit CLI). Data-heavy UI — GkDataTable and patterns aligned with admin and data-dense screens, with migration-oriented docs in the repo. Free and open source — MIT, issues and PRs on GitHub, docs at godkit.godplans.org. Teams shipping internal admin or SaaS dashboards on Vue 3 or Nuxt 4 Engineers who want a Vue design system with governed tokens and component authoring docs Projects that need a practical migration path from an older UI layer rather than a single frozen rewrite Developers comparing best UI library for Vue for their use case—not only total download volume We are actively working on a visual UI builder and continuing to improve documentation, components, and tooling so the Vue community has a simple, lightweight option that stays maintainable over time. Sponsors and feedback help prioritize Nuxt, a11y, data components, and developer experience—see the sponsors page and the roadmap in the docs. Read Getting started and Why God Kit on the official site. Install from npm: npm install god-kit (see the package page for the current version). Star and watch god-plans/god-kit for releases and discussions. Suggested title tag (≤ ~60 characters): Why God Kit? Vue 3 UI Library, Design Tokens & Nuxt 4 Suggested meta description (≤ ~155 characters): God Kit is a lightweight Vue 3 component library and design system for admin dashboards—design tokens, CLI, Nuxt 4, and incremental migration from legacy UI. Docs, npm, and GitHub links inside. Primary keywords (use naturally in headings and first paragraphs): Vue 3 UI library, Vue component library, design system Vue, lightweight Vue components, Nuxt 4 UI, admin dashboard Vue, design tokens CSS, Vue TypeScript components, open source UI kit Secondary / long-tail: Vue 3 component framework, scaffold Vue components CLI, tree-shaking Vue UI, migrate legacy Vue UI, accessibility Vue components, semantic CSS variables UI This article reflects the God Kit project as published in the repo’s docs and package.json; versions and features evolve—check godkit.godplans.org and npm for the latest.

Dev.to (Vue)
~12 min readApr 28, 2026

Vue vs. React: Which JavaScript UI framework is best?

Choosing the right UI framework can make a big difference to your project's success. Vue and React are two of the most popular frameworks for UI development, and both offer huge improvements in development speed and useful functionality over vanilla JavaScript. Both are lightweight and modular with a component-based architecture and rely on a virtual DOM to improve performance. But which is best? The truth is that there isn't one clear winner for all possible use cases, but it's likely that one will be better than another for your specific project. So, to understand which framework will work best for you, you'll need to consider which is the best fit for the developers in your team, and for the type and size of your particular application. This post provides detailed, practical comparisons between React and Vue that will help you make an informed decision about leveraging either for your project's specific needs. Vue.js is a front-end framework for building UIs, developed by Evan You (a former Google engineer) in 2014, and is particularly well suited for building single-page applications (SPAs). It's a progressive framework, which means you can adopt it incrementally, starting by plugging just one part of it into an existing application. This incremental approach makes it easy to integrate into existing projects as well as new ones. Vue is known for having a gentle learning curve. One reason behind this is its HTML-based template syntax, which is easier to get started with than learning a new syntax from scratch. Another reason is its use of single file components — files with a .vue suffix that contain HTML, CSS, and JavaScript all together. An example of the template syntax within a single file component is shown below. <template> <div> <h1>{{ message }}</h1> <button v-on:click="changeMessage">Click me</button> </div> </template> <script> export default { data() { return { message: 'Hello, World!' }; }, methods: { changeMessage() { this.message = 'You clicked the button!'; } } } </script> <style scoped> h1 { color: blue; } </style> In the example above, you'll notice the use of one of Vue's directives. Directives tell Vue to do something to a DOM element, and are prefixed with a v-. In the above example, the v-on:click directive handles the button being clicked. There are a wide variety of directives for different use cases, ranging from v-bind (which allows you to dynamically bind a variable to an HTML element) to v-for (which allows you to iterate over a list). As well as the standard HTML-based components like <div> and <img>, Vue has its own built-in components, such as for managing animation effects like fading a component in or out. The project structure of a Vue app looks like this: The public directory is for static assets that are not processed by Webpack (like index.html — the main HTML template), whereas assets processed by Webpack (such as images and fonts) live in the assets directory. The main.js file is the entry point for your application and App.vue is the root Vue component. All child components go in the components directory except page components, which go in views. The router directory is for your Vue router configuration and store is for state management. Finally, vue.config.js is an optional configuration file for the Vue CLI. React is a JavaScript library, originally developed by Facebook in 2013. Like Vue, it's also used for building UIs and SPAs, as well as more complex web applications. Although React is technically considered to be more of a library than a full-fledged framework, this distinction doesn't mean that it's less comprehensive than Vue, as the extra features that make Vue a framework (such as routing and state management) are still available to be used with React; they’re just not part of the core library. Instead, you could use Redux for state management and React Router for routing. Like Vue, React has a component-based architecture and uses a virtual DOM to improve performance. It also uses declarative syntax, however, instead of templating it uses JSX, a syntax extension of JavaScript allowing you to describe UI elements in React. The code example from Vue above would look like this in React: import React, { useState } from 'react'; import './MyComponent.css'; // Import the CSS file for styling const MyComponent = () => { const [message, setMessage] = useState('Hello, World!'); const changeMessage = () => { setMessage('You clicked the button!'); }; return ( <div> <h1>{message}</h1> <button onClick={changeMessage}>Click me</button> </div> ); }; export default MyComponent; /* MyComponent.css */ h1 { color: blue; } As we can see above, the HTML tags are included as part of JSX, but the CSS is separate. (There are alternative approaches to this, such as styled-components, a library that allows you to add CSS as part of your React code, but this isn’t part of the core React library.) You can also create custom components in React: //Greeting.jsx import React from 'react'; Greeting = ({ name }) => { return <h1>Hello, {name}!</h1>; }; export default Greeting; And then refer to them in another JSX file: <Greeting name={name} /> React projects typically have this structure: The public directory is for static assets (like index.html or the favicon). The index.js file is the entry point for your application, and App.js is the root React component. All child components go in the components directory and pages go in pages. Finally, the services directory is for service logic such as API calls and the store directory is for state management (e.g. Redux). Vue.js React More opinionated Vue offers a single integrated solution with libraries that work together seamlessly. It has its own built-in tools for things like state management and routing. More customizable It's a library, not a framework — with a massive ecosystem of third-party libraries to choose from. React uses these third-party libraries to handle state management and routing. HTML templates React uses HTML templating syntax with directives (by default). It's possible to configure Vue to use JSX but this is less common. JSX This is essentially just JavaScript, so you have the full power of JavaScript in your application. Dynamic rendering is easier in JSX. Less commonly used In terms of market share, Vue is used by 1.1% of all JavaScript websites. More commonly used React is used by 5.1% of all JavaScript websites. While it might not sound like a lot, this is 5x the usage of Vue. Less community support and backing Vue has a decent amount of community support but not as much as React. It doesn't have a huge corporate backer like Meta in React's case, but does have sponsorship from some medium-sized companies. Notable users: Alibaba, GitLab, Upwork, Wizz Air More community support and backing React is supported by a massive community and has corporate backing from Meta. Notable users: Instagram, Reddit, Airbnb, Netflix A combination of one-way and two-way data binding v-model, a directive for form input fields, does two-way data binding, which simplifies the handling of form inputs. The v-bind directive does one-way data binding. One-way data binding Data flows in one direction — from parent to child via props. You can still implement things that require bi-directional data flows, such as with forms, but such functionality requires managing state, which adds some complexity to your application. How to choose: React vs. Vue for your project You need to make informed framework selection decisions to avoid wasting time and resources and introducing technical debt. Thorough understanding of your needs (as much as that’s possible up front) and planning for them makes app development more straightforward and predictable. React is best for large complex projects: Complex and large projects usually benefit from flexible tooling, so that, over time, development teams can adapt the tooling to fit their needs and their scale exactly. React is a good fit for such projects because it has a smaller amount of built-in functionality in its core library. Teams can pick and choose additional tooling to suit their own needs. React also has an extensive ecosystem, making it more flexible, which is often required with larger or more complex projects. Vue is easier to learn: For smaller projects that don't require the flexibility of React, Vue is the better choice as it's simpler and easier to learn. If you're not familiar with either framework you'll probably find Vue easier. Vue is easier to integrate: As Vue is a progressive framework, you can just add one small part to your application at a time, making it easy to start integrating into your existing JavaScript projects. It's fairly easy to add a single file component to an existing project. Compare this to React, where you have to create many more files and import a root component into your code. It's often simpler to start from scratch and create a whole new React project, migrating all the old code from your old project into the new one. Vue is easier to learn: If you already have experience with either React or Vue, stick with the one you know, but otherwise use Vue as it has the simplest learning curve. It's a tie: Both have adequate solutions for SSR. You can either use React with Next.js or Vue with Nuxt.js. So, it really comes down to other considerations that you might care about — such as whether you're looking for a more opinionated or a more flexible framework. React has more support and backing: Most importantly, it has corporate backing from one of the big tech giants (Meta), but it's also used more, by some of the biggest companies in the world, and has a bigger community of support. You can use the Vue CLI to create a new Vue project: Install Node and NPM. Install the Vue CLI: npm install -g @vue/cli Use the Vue CLI to create your new project: vue create my-vue-app Enter your new project directory: cd my-vue-app Start the Vue development server: npm run serve Navigate to http://localhost:8080/ in your browser. The simplest way to create a React project is to use the Create React App. Install Node and NPM. Use Create React App to create your new project: npx create-react-app my-react-app Enter your new project directory: cd my-react-app Start the React development server: npm start Navigate to http://localhost:3000/ in your browser. TypeScript: Whether you're using React or Vue, there are some best practices that are relevant for both. For example, we recommend using TypeScript instead of JavaScript for all React and Vue projects as its static typing will help catch many errors early (at compile time) and will also give you features like improved automatic code completion that will speed up your development. GraphQL: A potentially helpful practice for both types of projects is to use GraphQL. GraphQL enables you to fetch all the data you need from a single endpoint, which is much more efficient than having to use multiple REST queries to get the same data. Using GraphQL for fetching data can massively increase your performance, although there is a learning curve. Use the key attribute with lists: When it comes to using listing items, Vue uses the v-for directive, and as this isn't plain JavaScript, it can be harder to understand exactly what it's doing, which can lead to unintended side effects. For example, items in a list can lose their state when they're updated, as their order isn't necessarily preserved. To avoid this, add a key attribute to your list. React also has a similar key property for dealing with lists. <template> <!--Don't do this--> <div v-for="item in items">{{ item }}</div> <!--Do this--> <div v-for="item in items" :key="item.id">{{ item }}</div> </template>   Don't use v-if with v-for: If you want to filter a list or array, don't use the v-if directive with v-for, as this is very inefficient. Vue gives priority to v-for over v-if, meaning that every single element will be looped over before the v-if conditional is checked. <!--Don't do this--> <div v-for='item in items' v-if='item.available' >   Instead, extract this logic to your JavaScript code and use a filter within there. <!--Do this--> <template> <div v-for="item in availableItems">{{ item }}</div> </template> <script> export default { computed: { availableItems() { return this.items.filter(item => item.available); }, }, } </script>   React best practices React hooks: Using hooks allows you to simplify your code by managing state, side effects, and context all from within functional components. Without this, you'd have to use class components, which contain a lot more boilerplate code and are less performant. The most common React hooks are useState and useEffect, but you can also create your own custom hooks. useState is responsible for managing state, and it returns the current state value along with a function to update it. import React, { useState } from 'react'; function Counter() { const [count, setCount] = useState(0); return ( <div> <p>You clicked {count} times</p> <button onClick={() => setCount(count + 1)}>Click me</button> </div> ); }   useEffect focuses on separating side effects from your main code logic, which makes your code more modular, readable, and maintainable. The code below runs useEffect after every render, and the side effect is that it updates the title of the web page. import React, { useState, useEffect } from 'react'; function Counter() { const [count, setCount] = useState(0); useEffect(() => { document.title = `You clicked ${count} times`; // Optional cleanup function return () => { console.log('Cleanup on unmount or before next effect'); }; }, [count]); return ( <div> <p>You clicked {count} times</p> <button onClick={() => setCount(count + 1)}>Click me</button> </div> ); }   React Suspense: This allows you to display a fallback component when waiting for your main components to load. Suspense is particularly useful when your components rely on data that you're fetching from an API that may not have loaded yet. StrictMode: Wrapping a component (or your entire application) in StrictMode will add extra checks and warnings to that component and its descendents, allowing you to identify potential problems and catch them early on. <React.StrictMode> <App /> </React.StrictMode>

Dev.to (Vue)
~4 min readApr 26, 2026

"React, Vue, or Svelte: The Ultimate Showdown - Which JavaScript Framework to Learn First in 2025 for a Future-Proof Career"

Introduction As a developer, choosing the right JavaScript framework can be a daunting task, especially with the ever-evolving landscape of front-end development. With React, Vue, and Svelte being the most popular choices, it's essential to consider which one to learn first to future-proof your career. In this article, we'll dive into the pros and cons of each framework, providing you with practical advice to make an informed decision. React is one of the most widely used JavaScript frameworks, developed by Facebook. It's known for its component-based architecture, virtual DOM, and large community support. Here's an example of a simple React component: import React from 'react'; function Counter() { const [count, setCount] = React.useState(0); return ( <div> <p>Count: {count}</p> <button onClick={() => setCount(count + 1)}>Increment</button> </div> ); } React's strengths include: Large community and ecosystem Wide adoption in the industry Robust set of tools and libraries (e.g., Redux, React Router) However, React also has some drawbacks: Steeper learning curve due to its unique concepts (e.g., JSX, hooks) Can be overkill for small projects Vue is another popular JavaScript framework, known for its progressive and flexible nature. It's designed to be approachable and easy to learn, with a strong focus on simplicity and ease of use. Here's an example of a simple Vue component: <template> <div> <p>Count: {{ count }}</p> <button @click="increment">Increment</button> </div> </template> <script> export default { data() { return { count: 0 } }, methods: { increment() { this.count++ } } } </script> Vue's strengths include: Easy to learn and use, even for developers without prior JavaScript framework experience Flexible and modular architecture Growing ecosystem and community support However, Vue also has some weaknesses: Smaller community compared to React Less support for large-scale, complex applications Svelte is a relatively new JavaScript framework, developed by Rich Harris. It's designed to be lightweight, efficient, and easy to use, with a strong focus on compiler-based architecture. Here's an example of a simple Svelte component: <script> let count = 0; function increment() { count++; } </script> <div> <p>Count: {count}</p> <button on:click={increment}>Increment</button> </div> Svelte's strengths include: Extremely lightweight and efficient Easy to learn and use, with a simple and intuitive syntax Compiler-based architecture provides excellent performance However, Svelte also has some drawbacks: Relatively small community and ecosystem Limited support for large-scale, complex applications When deciding which framework to learn first, consider the following factors: Project requirements: If you're working on a large-scale, complex application, React might be a better choice. For smaller projects or prototypes, Vue or Svelte could be more suitable. Personal preference: If you prefer a more traditional, template-based approach, Vue might be the way to go. If you enjoy a more functional, component-based architecture, React could be a better fit. If you're looking for a lightweight, efficient solution, Svelte is worth considering. Career goals: If you want to work on large-scale, enterprise-level applications, React might be a more valuable skill to have. If you're interested in working on smaller projects or startups, Vue or Svelte could be a better choice. Learning curve: If you're new to JavaScript frameworks, Vue might be an easier starting point due to its simplicity and ease of use. Ultimately, the choice between React, Vue, and Svelte depends on your individual needs, preferences, and goals. By considering the pros and cons of each framework, you can make an informed decision and start building a future-proof career in front-end development. Remember, it's not about which framework is "better," but about which one is the best fit for you and your projects. So, take the time to explore each framework, and don't be afraid to experiment and try new things – it's the best way to learn and grow as a developer.