[{"data":1,"prerenderedAt":270},["ShallowReactive",2],{"post-why-loading-states-matter":3,"blog-posts":14},{"_path":4,"title":5,"description":6,"date":7,"tags":8,"readingTime":12,"body":13},"\u002Fblog\u002Fwhy-loading-states-matter\u002F","Why Loading States Matter More Than Animations","Skeletons, disabled buttons, and honest feedback beat flashy spinners when data takes time to arrive.","2025-03-28",[9,10,11],"ux","frontend","design",5,"# Why Loading States Matter More Than Animations\n\nUsers forgive slow networks more easily than ambiguous interfaces. Fancy motion does not fix uncertainty—a loading state answers one question: *did my action register?*\n\n## Immediate feedback on click\n\nDisable the submit button and show progress as soon as someone saves a form or fetches data. Without that, double-clicks and duplicate requests follow—and people assume the app ignored them.\n\nA minimal pattern that works across Vue and plain JS:\n\n```vue\n\u003Cscript setup>\nconst pending = ref(false);\nconst error = ref(null);\n\nasync function onSubmit() {\n  if (pending.value) return;\n  pending.value = true;\n  error.value = null;\n  try {\n    await saveProfile(form.value);\n  } catch (e) {\n    error.value = 'Could not save. Try again.';\n  } finally {\n    pending.value = false;\n  }\n}\n\u003C\u002Fscript>\n\n\u003Ctemplate>\n  \u003Cbutton type=\"submit\" :disabled=\"pending\" @click.prevent=\"onSubmit\">\n    {{ pending ? 'Saving…' : 'Save' }}\n  \u003C\u002Fbutton>\n\u003C\u002Ftemplate>\n```\n\nFeedback should be local to the action. A global page spinner for changing one field trains users to wait on everything. Prefer:\n\n- Button label or adjacent status text that changes immediately\n- `aria-busy=\"true\"` on the region that is updating\n- Idempotent handlers so a double click cannot create two records even if the UI lags\n\nOptimistic UI is optional. Immediate acknowledgment is not. If you show the new state before the server confirms, you still need a path for rollback and error messaging when the request fails.\n\n## Skeletons over blank screens\n\nA gray placeholder shaped like the content that is coming reduces perceived wait time. A blank white panel feels broken; a skeleton feels intentional.\n\nGood skeletons:\n\n- Match the approximate layout of the final content (title line, avatar circle, two text rows)\n- Use calm motion or none—pulsing everything on the page is noise\n- Reserve the same space as the real content so the swap does not cause layout shift\n- Avoid fake “content-looking” paragraphs that users try to read\n\n```html\n\u003Cdiv class=\"post-skeleton\" aria-hidden=\"true\">\n  \u003Cdiv class=\"sk-title\">\u003C\u002Fdiv>\n  \u003Cdiv class=\"sk-line\">\u003C\u002Fdiv>\n  \u003Cdiv class=\"sk-line sk-line--short\">\u003C\u002Fdiv>\n\u003C\u002Fdiv>\n```\n\nPair skeletons with accessible status text for assistive tech. A decorative skeleton alone may not announce that loading is happening. A visually subtle `Loading posts…` (or an `aria-live` region) covers the gap.\n\nBlank screens are especially harmful on soft navigations in SPAs. If the old page disappears before the new data arrives, users see a flash of emptiness. Keep the previous shell, show a local skeleton, or use a progress bar at the edge of the viewport—something that says “navigation started.”\n\n## Error states are part of loading\n\nEvery async path needs failure UI: a message, a retry action, and enough detail to debug without exposing internals. Loading that never resolves is worse than loading that fails clearly.\n\nChecklist for each request:\n\n1. **Pending** — user sees that work started\n2. **Success** — content or confirmation appears\n3. **Failure** — message + retry (or next step)\n4. **Empty** — distinct from failure (“No posts yet” vs “Could not load posts”)\n\n```vue\n\u003Ctemplate>\n  \u003Cdiv v-if=\"pending\">Loading projects…\u003C\u002Fdiv>\n  \u003Cdiv v-else-if=\"error\" role=\"alert\">\n    \u003Cp>{{ error }}\u003C\u002Fp>\n    \u003Cbutton type=\"button\" @click=\"load\">Retry\u003C\u002Fbutton>\n  \u003C\u002Fdiv>\n  \u003Cdiv v-else-if=\"!items.length\">No projects yet.\u003C\u002Fdiv>\n  \u003Cul v-else>\u003C!-- … -->\u003C\u002Ful>\n\u003C\u002Ftemplate>\n```\n\nTimeouts matter. A spinner that runs forever after a hung fetch trains people to refresh the whole page. Set a client timeout, abort with `AbortController` when navigating away, and surface a clear “This is taking too long” state with retry.\n\nDo not toast every failure into a corner that disappears. Toasts are easy to miss for keyboard and screen-reader users if focus and live regions are wrong. For primary flows (checkout, save, publish), keep the error near the control that failed.\n\n## Match the state to the wait\n\nSub-second fetches might need only a subtle indicator. Multi-second operations deserve progress text or a determinate bar when you know the steps.\n\nRough guidance I use:\n\n| Expected wait | Feedback |\n|---------------|----------|\n| Under ~300ms | Often nothing, or disable the control quietly |\n| ~0.3–2s | Inline spinner \u002F “Saving…” \u002F small skeleton |\n| Multi-second | Explicit progress copy; steps if you have them |\n| Background job | Status page or banner with refresh\u002Fretry |\n\nAnimations can support these states—they should not replace them. A delightful loader that never explains what failed is still a poor experience. Prefer motion that reinforces hierarchy (a short fade when content arrives) over looping ornaments that compete with reading.\n\nAlso respect `prefers-reduced-motion`. If you animate skeletons or progress bars, provide a static alternative. Honesty about state is the requirement; motion is optional polish.\n\nOne more habit: name pending, error, and empty states in the component the same way you name them in design review. If the mock only shows the happy path, ask for the other three before you ship. Async UI that only designs success is where silent spinners and dead ends come from.\n\n## Wrap-up\n\nPolished animations are optional; honest loading states are not. Tell people something is happening, show where content will land, handle empty and error paths as carefully as success, and scale the feedback to the wait. That is most of good async UX—and it matters more than any spinner aesthetic.",[15,23,33,41,48,56,63,71,78,85,92,100,106,113,120,128,134,142,151,159,165,172,178,186,194,201,207,213,220,226,232,234,241,248,255,263],{"_path":16,"title":17,"description":18,"date":19,"tags":20,"readingTime":12},"\u002Fblog\u002Fclient-side-tools-that-earn-trust\u002F","Client-Side Tools That Earn Trust","How to design browser tools that feel private by default—local processing, clear data boundaries, and UX that never asks people to guess where their files go.","2026-07-20",[21,10,22],"privacy","tools",{"_path":24,"title":25,"description":26,"date":27,"tags":28,"readingTime":32},"\u002Fblog\u002Fusb-c-cables-that-actually-matter\u002F","USB-C Cables That Actually Matter","How to buy USB-C cables by wattage and data speed so you stop guessing which cord charges slowly or fails a file transfer.","2026-07-14",[29,30,31],"gadgets","hardware","productivity",4,{"_path":34,"title":35,"description":36,"date":37,"tags":38,"readingTime":12},"\u002Fblog\u002Fcolor-contrast-people-can-read\u002F","Color Contrast People Can Actually Read","Quick checks for text and UI colors that stay legible in light mode, dark mode, and on mediocre displays.","2026-07-07",[39,40,11],"accessibility","css",{"_path":42,"title":43,"description":44,"date":45,"tags":46,"readingTime":12},"\u002Fblog\u002Fportable-ssds-worth-carrying\u002F","Portable SSDs Worth Carrying","When a pocket SSD beats cloud sync for travel, backups, and large media—and how to pick one that stays fast and durable.","2026-06-26",[29,47,31],"storage",{"_path":49,"title":50,"description":51,"date":52,"tags":53,"readingTime":12},"\u002Fblog\u002Fwireless-earbuds-that-last-the-day\u002F","Wireless Earbuds That Last the Day","Simple charging and fit habits that stretch earbud battery life without babysitting the case every few hours.","2026-06-18",[29,54,55],"audio","habits",{"_path":57,"title":58,"description":59,"date":60,"tags":61,"readingTime":32},"\u002Fblog\u002Fwhen-a-second-monitor-helps\u002F","When a Second Monitor Actually Helps","A practical take on dual screens for coding, writing, and research—plus when a single good display is still the better desk.","2026-06-10",[29,31,62],"workspace",{"_path":64,"title":65,"description":66,"date":67,"tags":68,"readingTime":32},"\u002Fblog\u002Fshort-session-game-picks\u002F","Short-Session Games That Respect Your Evening","Genres and design patterns that fit a 30-minute window without the “one more hour” trap.","2026-06-04",[69,55,70],"gaming","indie",{"_path":72,"title":73,"description":74,"date":75,"tags":76,"readingTime":12},"\u002Fblog\u002Fphone-charging-habits-that-age-better\u002F","Phone Charging Habits That Age Better","Practical charging routines that keep a phone useful longer—without obsessing over the perfect battery percentage.","2026-05-23",[29,77,55],"mobile",{"_path":79,"title":80,"description":81,"date":82,"tags":83,"readingTime":12},"\u002Fblog\u002Ffinishing-side-projects-without-burning-out\u002F","Finishing Side Projects Without Burning Out","A practical playbook for shrinking scope, shipping thin vertical slices, and keeping personal projects fun long enough to actually launch.","2026-05-14",[31,84,55],"side-projects",{"_path":86,"title":87,"description":88,"date":89,"tags":90,"readingTime":12},"\u002Fblog\u002Faccessible-forms-people-finish\u002F","Accessible Forms That People Actually Finish","Practical accessibility checks for labels, errors, and focus so your forms work for more users.","2026-05-07",[39,91,10],"html",{"_path":93,"title":94,"description":95,"date":96,"tags":97,"readingTime":12},"\u002Fblog\u002Fvue-composition-api-patterns\u002F","Practical Vue Composition API Patterns","Reusable patterns for composables, shared state, and cleaner component logic in Vue 3.","2026-04-27",[98,99,10],"vue","javascript",{"_path":101,"title":102,"description":103,"date":104,"tags":105,"readingTime":12},"\u002Fblog\u002Fdesigning-empty-states-that-teach\u002F","Designing Empty States That Teach the Product","Empty screens are not placeholders—they are onboarding. How to write, layout, and sequence first-run UI so people know what to do next.","2026-04-19",[9,10,11],{"_path":107,"title":108,"description":109,"date":110,"tags":111,"readingTime":12},"\u002Fblog\u002Fwhy-indie-games-keep-winning\u002F","Why Indie Games Keep Winning Attention","How small teams punch above their weight with focused design, personality, and smart scope.","2026-04-13",[69,70,112],"industry",{"_path":114,"title":115,"description":116,"date":117,"tags":118,"readingTime":12},"\u002Fblog\u002Fkeeping-component-props-simple\u002F","Keeping Component Props Simple","When to split a component, when to use slots, and how to avoid prop objects that grow forever.","2026-04-03",[98,10,119],"best-practices",{"_path":121,"title":122,"description":123,"date":124,"tags":125,"readingTime":12},"\u002Fblog\u002Fcalm-frontend-performance\u002F","A Calm Approach to Frontend Performance","Performance work that sticks: measure what users feel, fix the critical path first, and avoid optimization theater on personal and portfolio sites.","2026-03-26",[126,10,127],"performance","web",{"_path":129,"title":130,"description":131,"date":132,"tags":133,"readingTime":12},"\u002Fblog\u002Fcss-layout-without-the-hacks\u002F","CSS Layout Without the Hacks","Modern Flexbox and Grid techniques that replace brittle floats, magic numbers, and overflow tricks.","2026-03-17",[40,10,11],{"_path":135,"title":136,"description":137,"date":138,"tags":139,"readingTime":12},"\u002Fblog\u002Fcloud-gaming-what-gets-in-the-way\u002F","Cloud Gaming: What Still Gets in the Way","Latency, input feel, and library lock-in—why streaming games is impressive and still uneven.","2026-03-10",[69,140,141],"cloud-gaming","tech",{"_path":143,"title":144,"description":145,"date":146,"tags":147,"readingTime":12},"\u002Fblog\u002Fshipping-static-sites-with-nuxt\u002F","Shipping Static Sites with Nuxt","How I build and deploy a Nuxt site to Cloudflare Pages with predictable routes and content.","2026-03-02",[148,149,150],"nuxt","ssg","devops",{"_path":152,"title":153,"description":154,"date":155,"tags":156,"readingTime":12},"\u002Fblog\u002Fdebugging-faster-in-the-browser\u002F","Debugging Faster in the Browser","A short toolkit of DevTools habits—breakpoints, network filters, and DOM inspection—that cut debug time.","2026-02-24",[99,157,158],"devtools","debugging",{"_path":160,"title":161,"description":162,"date":163,"tags":164,"readingTime":12},"\u002Fblog\u002Fsmall-javascript-habits-that-scale\u002F","Small JavaScript Habits That Scale","Everyday habits—naming, early returns, and clearer async flow—that keep front-end codebases maintainable.","2026-02-18",[99,119,10],{"_path":166,"title":167,"description":168,"date":169,"tags":170,"readingTime":32},"\u002Fblog\u002Fhealthier-gaming-routine\u002F","Building a Healthier Gaming Routine","Simple habits for sessions that stay fun—breaks, goals, and knowing when to stop.","2026-02-02",[69,55,171],"wellness",{"_path":173,"title":174,"description":175,"date":176,"tags":177,"readingTime":12},"\u002Fblog\u002Fresponsive-images-without-layout-shift\u002F","Responsive Images Without Layout Shift","Width, height, srcset, and lazy loading patterns that keep pages stable while images load.","2026-01-22",[91,126,10],{"_path":179,"title":180,"description":181,"date":182,"tags":183,"readingTime":12},"\u002Fblog\u002Fgit-habits-for-small-teams\u002F","Git Habits for Solo and Small Teams","Branch naming, commit messages, and PR hygiene that keep history useful without slowing you down.","2026-01-09",[184,185,31],"git","workflow",{"_path":187,"title":188,"description":189,"date":190,"tags":191,"readingTime":12},"\u002Fblog\u002Fwhat-makes-a-game-stream-worth-watching\u002F","What Makes a Game Stream Worth Watching","Commentary, pacing, and community—why some streams click even when the gameplay is messy.","2025-12-14",[69,192,193],"streaming","content",{"_path":195,"title":196,"description":197,"date":198,"tags":199,"readingTime":12},"\u002Fblog\u002Fwhen-to-reach-for-typescript-vue\u002F","When to Reach for TypeScript on a Vue App","A pragmatic take on where TypeScript pays off in Vue—and where plain JS is still fine.","2025-11-30",[200,98,10],"typescript",{"_path":202,"title":203,"description":204,"date":205,"tags":206,"readingTime":12},"\u002Fblog\u002Fenvironment-variables-for-static-sites\u002F","Environment Variables for Static Sites","How to use build-time env vars in Nuxt and other static generators without leaking secrets or breaking CI.","2025-10-05",[148,150,10],{"_path":208,"title":209,"description":210,"date":211,"tags":212,"readingTime":12},"\u002Fblog\u002Fpicking-a-side-project-stack\u002F","Picking a Side Project Stack Without Overthinking It","How to choose tools for a personal project when the goal is shipping, not building the perfect architecture.","2025-09-18",[31,10,119],{"_path":214,"title":215,"description":216,"date":217,"tags":218,"readingTime":32},"\u002Fblog\u002Fco-op-games-for-busy-friends\u002F","Co-op Games That Work When Everyone Is Busy","Async-friendly and drop-in co-op picks for friend groups that rarely share the same free evening.","2025-08-22",[69,219,55],"co-op",{"_path":221,"title":222,"description":223,"date":224,"tags":225,"readingTime":12},"\u002Fblog\u002Fsemantic-html-still-matters\u002F","Semantic HTML Still Matters","Landmarks, headings, and native elements that make pages easier to use, style, and maintain.","2025-07-06",[91,39,10],{"_path":227,"title":228,"description":229,"date":230,"tags":231,"readingTime":12},"\u002Fblog\u002Fgit-without-the-jargon\u002F","Learning Git Without the Jargon","A plain-language mental model for commits, branches, and merges when tutorials assume you already know Git.","2025-05-14",[184,185,31],{"_path":4,"title":5,"description":6,"date":7,"tags":233,"readingTime":12},[9,10,11],{"_path":235,"title":236,"description":237,"date":238,"tags":239,"readingTime":12},"\u002Fblog\u002Fnaming-things-so-future-you-survives\u002F","Naming Things So Future You Survives","Practical rules for naming variables, components, and files so a codebase stays readable months after the clever jokes fade.","2025-03-10",[99,119,240],"maintainability",{"_path":242,"title":243,"description":244,"date":245,"tags":246,"readingTime":12},"\u002Fblog\u002Fsave-systems-that-respect-time\u002F","Save Systems That Respect the Player’s Time","Checkpoints, autosaves, and manual slots are design choices. How thoughtful saving reduces rage-quits without deleting tension from the game.","2025-02-28",[69,247,9],"game-design",{"_path":249,"title":250,"description":251,"date":252,"tags":253,"readingTime":12},"\u002Fblog\u002Fmicrocopy-that-makes-interfaces-human\u002F","Microcopy That Makes Interfaces Feel Human","Button labels, error lines, and helper text are product design. How to write UI words that reduce hesitation without sounding like a chatbot.","2025-01-22",[9,254,11],"writing",{"_path":256,"title":257,"description":258,"date":259,"tags":260,"readingTime":12},"\u002Fblog\u002Freadmes-that-get-projects-running\u002F","READMEs That Get Projects Running","How to write a README that helps someone—including future you—install, run, and understand a project without opening a support chat.","2024-12-18",[261,262,55],"documentation","open-source",{"_path":264,"title":265,"description":266,"date":267,"tags":268,"readingTime":12},"\u002Fblog\u002Fprogressive-enhancement-still-pays-off\u002F","Progressive Enhancement Still Pays Off","Build the core experience in HTML first, then layer CSS and JavaScript so your site stays useful when networks, scripts, or devices misbehave.","2024-11-05",[91,10,269],"architecture",1784620505219]