<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
    <title>James McGrath</title>
    <subtitle>Front End Architect</subtitle>
    <link href="https://jamesmcgrath.net/blog.rss" rel="self"/>
    <link href="https://jamesmcgrath.net"/>
    <updated>2026-08-16T10:45:02.856Z</updated>
    <id>https://jamesmcgrath.net</id>
    <author>
        <name>James McGrath</name>
    </author>
    
    <entry>
        <title>The AI Coding Guidelines Nobody Needed (And What They Taught Me)</title>
        <link href="https://jamesmcgrath.net/blog/ai-coding-guidelines-nobody-needed"/>
        <updated>2026-03-01T00:00:00.000Z</updated>
        <id>https://jamesmcgrath.net/blog/ai-coding-guidelines-nobody-needed</id>
        <content type="html"><![CDATA[<p>In the fall of 2024, my team started using <a href="https://aider.chat/">Aider</a> as our coding assistant, one of the first CLI-based AI coding tools out there. I was tasked with creating front-end guidelines to be used as LLM context. This was essentially what we&#39;d now call an <code>agents.md</code> or a skills file, but back then none of that existed yet. We were just experimenting.</p>
<p>The motivation was simple: LLM output at the time could be inconsistent, and we wanted it to write code in a style that matched our team&#39;s conventions. So I set out to put together a set of concise, token-efficient guidelines written in Markdown.</p>
<p>My process looked something like this: I&#39;d read through open-source style guides (Google&#39;s HTML/CSS guide, for example), strip out the parts that weren&#39;t relevant to us, then hand the result to Claude or ChatGPT with a prompt to make it more concise within a token budget. After a few rounds of that back-and-forth, I&#39;d usually land somewhere I was happy with.</p>
<p>Going into this, I had two goals in mind. First, give the LLM better context for our project. Second, produce something I could hand to a junior dev as a quick reference. The guides needed to be concise but still readable for a human.</p>
<p>Looking back, we probably didn&#39;t need most of what I wrote. Like I said, we didn&#39;t really know what we were doing, and this was new territory for the whole team. The only ones that proved genuinely useful were the opinionated ones. As the lead front-end dev, I have some personal rules I enforce across the team (like no <code>max-width</code> media queries) and that kind of thing is hard for an LLM to guess on its own. Out of everything I wrote, we still use three guidelines today: one for design tokens, one for our custom Tailwind implementation, and a general CSS styling guide.</p>
<p>Even though most of these are pretty useless now (AI tooling has advanced so much since late 2024), I don&#39;t think it was a waste of time. Creating those guidelines taught me two things that still matter: the importance of giving your AI assistant good context, and how to think about token management.</p>
<p>If you want to see the retired guidelines, I put them up on <a href="https://github.com/jamcgrath/the-llm-guidelines-nobody-needed/">GitHub</a>.</p>
]]></content>
    </entry>
    
    <entry>
        <title>Low hanging accessibility wins checklist</title>
        <link href="https://jamesmcgrath.net/blog/accessibility-checklist"/>
        <updated>2025-09-25T00:00:00.000Z</updated>
        <id>https://jamesmcgrath.net/blog/accessibility-checklist</id>
        <content type="html"><![CDATA[<h2>Keyboard &amp; Focus</h2>
<p><em>Check that all controls are reachable and usable with the keyboard.</em></p>
<ul>
<li>Can I <strong>Tab/Shift+Tab</strong> to all interactive elements in a logical order?</li>
<li>Can I activate controls with <strong>Enter/Space</strong> (and Arrow keys where expected)?</li>
<li>Is there a <strong>visible focus ring</strong> when I navigate via keyboard?</li>
<li>Can I <strong>close menus/dialogs with Escape</strong>, and does focus return to the trigger?</li>
<li>Does focus <strong>never get lost or trapped</strong> (except intentionally in modals)?</li>
</ul>
<hr>
<h2>Semantics &amp; Roles</h2>
<p><em>Check that elements use the right HTML semantics so assistive tech understands them.</em></p>
<ul>
<li>Am I using the <strong>correct native element</strong> (<code>&lt;button&gt;</code>, <code>&lt;a href&gt;</code>, <code>&lt;input&gt;</code>) instead of a <code>div</code>/<code>span</code>?</li>
<li>Does every input have a <strong>visible label</strong> that is programmatically associated?</li>
<li>Do all link/button labels clearly describe their purpose (e.g., “Save changes” not “Click here”)?</li>
<li>Do headings follow a <strong>logical order</strong> without skipping levels?</li>
<li>Does the component have <strong>one — and only one — primary heading</strong> when it renders standalone (e.g., dialog/card title with <code>&lt;h2&gt;</code>)?</li>
<li>Am I using <strong>landmark elements</strong> where relevant (<code>&lt;main&gt;</code>, <code>&lt;nav&gt;</code>, <code>&lt;header&gt;</code>, <code>&lt;footer&gt;</code>)?</li>
</ul>
<hr>
<h2>Motion &amp; Zoom</h2>
<p><em>Check that the UI works for users with motion sensitivity or magnification needs.</em></p>
<ul>
<li>Does the component <strong>respect reduced motion</strong> when <code>prefers-reduced-motion</code> is enabled?</li>
<li>Can I <strong>zoom to 200%</strong> without layout breaking or forcing sideways scrolling?</li>
</ul>
<hr>
<h2>Forms &amp; State</h2>
<p><em>Check that inputs, errors, and states are clearly labeled and communicated.</em></p>
<ul>
<li>Does every input have a <strong>visible, programmatically associated label</strong>?</li>
<li>Are error or help messages placed <strong>near the field</strong> and tied with <code>aria-describedby</code>?</li>
<li>Are states (checked, disabled, pressed, loading) <strong>clearly reflected visually</strong>?</li>
<li>Do buttons that trigger async actions show a <strong>loading or disabled state</strong> while processing?</li>
<li>Do <strong>disabled buttons look visually disabled</strong> (dimmed, muted, etc.) <em>without relying only on the <code>disabled</code> attribute</em>?</li>
</ul>
<p>​ <strong>Note:</strong> The <code>disabled</code> attribute makes elements <em>invisible to screen readers</em> and not keyboard-focusable. Use <code>aria-disabled=&quot;true&quot;</code> for accessible inactive states.</p>
<hr>
<h2>Media &amp; Images</h2>
<p><em>Check that images and media are properly described or hidden when decorative.</em></p>
<ul>
<li><p>Do all content images have an appropriate <strong>alt attribute</strong>?</p>
</li>
<li><p>If the <code>alt</code> is an empty string (<code>alt=&quot;&quot;</code>), does the <strong>surrounding text convey the image’s meaning</strong> so that no information is lost?</p>
</li>
<li><p>Are decorative images/icons hidden with <code>alt=&quot;&quot;</code> or <code>aria-hidden=&quot;true&quot;</code>?</p>
</li>
<li><p>Do icons that convey meaning have a <strong>text label</strong> (or accessible name)?</p>
</li>
<li><p>Does media (video/audio) <strong>avoid auto-playing</strong> on load?</p>
</li>
<li><p>Use descriptive <code>alt</code> for meaningful images.</p>
</li>
<li><p>Use <code>alt=&quot;&quot;</code> for purely decorative images <em>only when the surrounding text already conveys the meaning</em>.</p>
</li>
</ul>
<h2>​ <strong>Note:</strong> The <code>alt</code> attribute is <strong>required by the HTML spec</strong> on every <code>&lt;img&gt;</code> tag.</h2>
<h2>Custom Controls</h2>
<p><em>Check that custom UI components behave like their native equivalents.</em></p>
<ul>
<li>If I built a custom checkbox, radio, or switch, does it sync with a hidden native input?</li>
<li>Do custom controls respond to <strong>Space/Enter</strong> like native elements?</li>
</ul>
<hr>
<h2>Dialogs, Popovers, Tooltips</h2>
<p><em>Check that overlays and popups are labeled, focusable, and dismissible.</em></p>
<ul>
<li>Does a modal set <code>aria-modal=&quot;true&quot;</code> and <strong>trap focus</strong> while open?</li>
<li>Does a dialog, menu, or popover have a <strong>visible label</strong> or <code>aria-label</code>?</li>
<li>Do tooltips only provide <strong>supplemental info</strong>, not critical content?</li>
</ul>
<hr>
<h2>ARIA Hygiene</h2>
<p><em>Check that ARIA is used sparingly and correctly.</em></p>
<ul>
<li>Am I only using ARIA attributes when necessary?</li>
<li>Are all ARIA attributes used <strong>valid and appropriate</strong> (<code>aria-expanded</code> only on toggles, etc.)?</li>
<li>Do elements avoid <strong>duplicate or conflicting labels</strong>?</li>
</ul>
<hr>
<h2>Miscellaneous</h2>
<p><em>Check that global attributes and temporary messages are handled accessibly.</em></p>
<ul>
<li>Do snackbars/toasts give users <strong>enough time to read</strong> or provide a way to dismiss them?</li>
<li>Is the correct <strong>language attribute</strong> set (<code>lang=&quot;en&quot;</code>, etc.)?</li>
</ul>
<hr>
<h2>notes</h2>
<p>This is not an exhaustie list and there many more things that can be done ensure that your site is accessibile. In my opinion this list is something that a dev can review and take care of relatively quickly without having to be knowledgeable in accessibility practices. </p>
<p>I left out the color contrast because it can be a little tricky to understand and test quickly. I also feel that in some organizations the devs may not have a say in this matter and a broader discussion it required to resolve it. </p>
]]></content>
    </entry>
    
    <entry>
        <title>CSS only responsive typography</title>
        <link href="https://jamesmcgrath.net/blog/css-responsive-typography"/>
        <updated>2025-07-26T00:00:00.000Z</updated>
        <id>https://jamesmcgrath.net/blog/css-responsive-typography</id>
        <content type="html"><![CDATA[<p>A few years ago when I started using css custom properties, I would try all kinds of things with them to see what I could discover about using them. One of the things I made is this <code>.fs-scale</code> class that does responsive typography. It&#39;s pretty interesting in the way it uses the cascade and vars to make it work.</p>
<pre><code class="language-CSS">.fs-scale {
	--root-fs: 16;
	--min-fs: 1;
	--max-fs: 2;
	--mq1: 300;
	--mq2: 1024;
	--min-width: calc(var(--mq1) / var(--root-fs));
	--max-width: calc(var(--mq2) / var(--root-fs));
	--slope: calc(
		(var(--max-fs) - var(--min-fs)) / (var(--max-width) - var(--min-width))
	);
	--y-intersect: calc(
		(calc(var(--min-width) * -1) * var(--slope) + var(--min-fs))
	);
	--slope-vw: calc(var(--slope) * 100vw);
	--preferred-val: calc(calc(var(--y-intersect) * 1rem) + var(--slope-vw));
	--clamp-fs: clamp(
		calc(var(--min-fs) * 1rem),
		var(--preferred-val),
		calc(var(--max-fs) * 1rem)
	);
}

.fs-scale {
		font-size: calc(var(--min-fs) * 1rem);
}

@media (min-width: 64em) {
	.fs-scale {
		font-size: calc(var(--max-fs) * 1rem);
	}
}

@media (min-width: 1px) {
	@supports (font-size: clamp(1px, 2px, 3px)) {
		.fs-scale {
			font-size: var(--clamp-fs);
		}
	}
}
/* how to use */
&lt;div class=&quot;fs-scale title&quot;&gt;Title&lt;/div&gt;

.title {
	--min-fs: 1;
	--max-fs: 5;
}

</code></pre>
<p>I have actually used this in production. There is also a comment with it that says &quot;If you don&#39;t understand this, don&#39;t use it.&quot; It&#39;s been quite some time since I wrote it and it turns out that I know how to use it, but I do understand what it is doing. If you think this post will be me trying to explain it to you then you have guessed wrong. I gave the class to multiple AIs and asked them to explain it and then combined the results I liked into one. Here&#39;s the comprehensive explanation that emerged from that collaboration:</p>
<p>⸻</p>
<h2>What it does (in plain English)</h2>
<p><code>.fs-scale</code> creates <strong>smooth, proportional text scaling</strong> between viewport breakpoints instead of jarring jumps:
• From <code>--mq1</code> (300px) → use <code>--min-fs</code> (in rem)
• To <code>--mq2</code> (1024px) → use <code>--max-fs</code> (in rem)
• Between them → font-size grows <strong>linearly</strong> along a mathematically precise curve
• Below/above them → clamped to min/max boundaries</p>
<p>Think of it like plotting two points on a graph and drawing a straight line between them. Instead of text that&#39;s 16px until 768px then suddenly jumps to 24px, you get smooth scaling where text at 600px viewport might be 20px.</p>
<p><strong><a href="#progressive-enhancement-strategy">Progressive enhancement</a></strong>: Modern browsers get fluid scaling with <code>clamp()</code>, older browsers get sensible breakpoint-based steps.</p>
<p><strong>Ready to implement?</strong> If you just want to use this technique, skip to the <a href="#how-to-use-it">How to use it</a> section. Want to understand potential implementation challenges first? Check out the <a href="#gotchas--tips">gotchas and tips</a> section. If you want to understand the mathematical foundations, keep reading.</p>
<p>⸻</p>
<h2>The mathematical foundation</h2>
<p>This system implements <strong>linear interpolation</strong> using the classic equation <code>y = mx + b</code>:</p>
<h3>1. Coordinate system setup</h3>
<pre><code class="language-css">--root-fs: 16; /* Base reference: 16px = 1rem */
--min-fs: 1; /* Starting size: 1rem = 16px */
--max-fs: 2; /* Ending size: 2rem = 32px */
--mq1: 300; /* Starting viewport: 300px */
--mq2: 1024; /* Ending viewport: 1024px */
</code></pre>
<p>This establishes two coordinate points: <strong>(300px, 16px)</strong> and <strong>(1024px, 32px)</strong></p>
<h3>2. Unit normalization</h3>
<pre><code class="language-css">--min-width: calc(var(--mq1) / var(--root-fs)); /* 300÷16 = 18.75rem */
--max-width: calc(var(--mq2) / var(--root-fs)); /* 1024÷16 = 64rem */
</code></pre>
<p>Converts pixel breakpoints to rem for mathematical consistency.</p>
<h3>3. Slope calculation (rise over run)</h3>
<pre><code class="language-css">--slope: calc(
  (var(--max-fs) - var(--min-fs)) / (var(--max-width) - var(--min-width))
);
</code></pre>
<p><strong>Result</strong>: <code>(2-1) ÷ (64-18.75) = 0.022</code> rem per rem of viewport width
• For every rem the screen gets wider, font grows by ~0.022rem</p>
<h3>4. Y-intercept calculation</h3>
<pre><code class="language-css">--y-intersect: calc(
  (calc(var(--min-width) * -1) * var(--slope) + var(--min-fs))
);
</code></pre>
<p>Solves for <code>b</code> in <code>y = mx + b</code> using our known point (18.75rem, 1rem)
<strong>Result</strong>: <code>b ≈ 0.59rem</code></p>
<h3>5. Viewport-responsive value</h3>
<pre><code class="language-css">--slope-vw: calc(var(--slope) * 100vw);
--preferred-val: calc(calc(var(--y-intersect) * 1rem) + var(--slope-vw));
</code></pre>
<p>Translates the linear equation to work with current viewport width (<code>100vw</code>)
<strong>Formula becomes</strong>: <code>font-size = 0.59rem + (0.022 × viewport-width)</code></p>
<h3>6. Safety boundaries</h3>
<pre><code class="language-css">--clamp-fs: clamp(
  calc(var(--min-fs) * 1rem),
  /* Never below 1rem */ var(--preferred-val),
  /* Calculated value */ calc(var(--max-fs) * 1rem) /* Never above 2rem */
);
</code></pre>
<p>Acts like guardrails ensuring text stays within reasonable bounds on extreme screens.</p>
<p>⸻</p>
<p><a id="progressive-enhancement-strategy"></a></p>
<h2>Progressive enhancement strategy</h2>
<p>The implementation builds in <strong>three layers</strong> for maximum browser compatibility:</p>
<h3>Layer 1: Base fallback (all browsers)</h3>
<pre><code class="language-css">.fs-scale {
  font-size: calc(var(--min-fs) * 1rem); /* Safe minimum */
}
</code></pre>
<h3>Layer 2: Large screen fallback (older browsers)</h3>
<pre><code class="language-css">@media (min-width: 64em) {
  .fs-scale {
    font-size: calc(var(--max-fs) * 1rem); /* Step to maximum */
  }
}
</code></pre>
<h3>Layer 3: Modern browser enhancement</h3>
<pre><code class="language-css">@media (min-width: 1px) {
  @supports (font-size: clamp(1px, 2px, 3px)) {
    .fs-scale {
      font-size: var(--clamp-fs); /* Full fluid scaling */
    }
  }
}
</code></pre>
<p>The <code>@media (min-width: 1px)</code> hack ensures this rule has higher specificity than the previous media query.</p>
<p>⸻</p>
<p><a id="how-to-use-it"></a></p>
<h2>How to use it</h2>
<p>Understanding the theory is one thing, but putting this into practice is where it becomes truly valuable:</p>
<h3>1. Add the class</h3>
<pre><code class="language-html">&lt;h1 class=&quot;fs-scale title&quot;&gt;Fluid Title&lt;/h1&gt;
&lt;p class=&quot;fs-scale&quot;&gt;Body text that scales subtly&lt;/p&gt;
</code></pre>
<h3>2. Set per-element ranges</h3>
<p>Override min/max per component via companion classes or inline styles:</p>
<pre><code class="language-css">/* Dramatic title scaling */
.title {
  --min-fs: 1;
  --max-fs: 5;
} /* 16px → 80px */

/* Subtle body text scaling */
p.fs-scale {
  --min-fs: 1;
  --max-fs: 1.25;
} /* 16px → 20px */

/* Small text scaling */
.small {
  --min-fs: 0.875;
  --max-fs: 1;
} /* 14px → 16px */
</code></pre>
<p>Or inline:</p>
<pre><code class="language-html">&lt;h2 class=&quot;fs-scale&quot; style=&quot;--min-fs:1.25; --max-fs:2.5;&quot;&gt;Section&lt;/h2&gt;
</code></pre>
<h3>3. (Optional) Customize site-wide breakpoints</h3>
<pre><code class="language-css">:root {
  --root-fs: 16; /* Keep accurate: match your actual 1rem in px */
  --mq1: 360; /* Start scaling at 360px instead of 300px */
  --mq2: 1280; /* Stop scaling at 1280px instead of 1024px */
}
</code></pre>
<p>⸻</p>
<h2>What the provided .title does</h2>
<pre><code class="language-css">.title {
  --min-fs: 1;
  --max-fs: 5;
}
</code></pre>
<p>A <code>&lt;h1 class=&quot;fs-scale title&quot;&gt;</code> will be:
• <strong>1rem (16px)</strong> at ≤300px viewports
• <strong>Smoothly scales</strong> from 16px to 80px between 300px–1024px
• <strong>5rem (80px)</strong> at ≥1024px viewports</p>
<p>The scaling follows the mathematical curve: <code>font-size = 0.59rem + (0.022 × viewport-width)</code></p>
<p>⸻</p>
<h2>Advantages over traditional breakpoints</h2>
<p><strong>Traditional approach problems</strong>:
• Abrupt jumps create jarring user experience
• Limited control points (usually 2-3 breakpoints)
• Awkward in-between sizes on tablets and intermediate screens
• Requires manual fine-tuning for each breakpoint</p>
<p><strong>This fluid system benefits</strong>:
• <strong>Seamless scaling</strong> eliminates visual jumps
• <strong>Mathematical precision</strong> ensures proportional growth
• <strong>Infinite responsiveness</strong> works perfectly at any screen size
• <strong>Accessibility friendly</strong> respects user font-size preferences (rem-based)
• <strong>Future-proof</strong> automatically handles new device sizes</p>
<p>⸻</p>
<h2>Advanced usage patterns</h2>
<p>Once you&#39;ve mastered the basics, these advanced techniques can expand the system&#39;s capabilities:</p>
<h3>Container-based scaling (modern browsers)</h3>
<pre><code class="language-css">/* Replace 100vw with 100cqw for container queries */
--slope-cqw: calc(var(--slope) * 100cqw);
--preferred-val: calc(calc(var(--y-intersect) * 1rem) + var(--slope-cqw));

@container (min-width: 300px) {
  .fs-scale {
    font-size: var(--clamp-fs);
  }
}
</code></pre>
<h3>Multiple scaling ranges</h3>
<pre><code class="language-css">.hero-text {
  --min-fs: 2; /* Start larger */
  --max-fs: 8; /* Scale dramatically */
  --mq1: 320; /* Different breakpoints */
  --mq2: 1440;
}
</code></pre>
<h3>Reverse scaling limitation</h3>
<pre><code class="language-css">/* This WON&#39;T work - clamp() breaks when min &gt; max */
.broken-reverse {
  --min-fs: 1.5; /* clamp() minimum */
  --max-fs: 1; /* clamp() maximum - invalid! */
}
</code></pre>
<p><strong>Note</strong>: This system only supports scaling up (min-fs &lt; max-fs). Reverse scaling would require modifying the clamp logic.</p>
<p>⸻</p>
<p><a id="gotchas--tips"></a></p>
<h2>Gotchas / tips</h2>
<p>• <strong>Keep <code>--root-fs</code> accurate</strong>: If <code>html { font-size: 62.5%; }</code> (10px rem), set <code>--root-fs: 10</code>
• <strong>Values are viewport-based</strong>: Uses <code>100vw</code>, so horizontal scrollbars can affect scaling
• <strong>Respect user preferences</strong>: The rem-based system honors user zoom and font-size settings<br>• <strong>Performance</strong>: Calculations happen once per element, very efficient
• <strong>Debug tip</strong>: Use browser dev tools to inspect <code>--preferred-val</code> and see the live calculation
• <strong>Accessibility</strong>: Always test with 200% browser zoom to ensure readability
• <strong>Design systems</strong>: Consider creating preset classes for common scaling patterns</p>
<p>⸻</p>
<h2>Browser support</h2>
<p>• <strong>Full fluid scaling</strong>: All modern browsers (Chrome 79+, Firefox 75+, Safari 13.1+)
• <strong>Graceful fallback</strong>: All browsers back to IE9 get step-based responsive text<br>• <strong>Progressive enhancement</strong>: Older browsers still get a good experience, just less smooth</p>
]]></content>
    </entry>
    
    <entry>
        <title>Thoughts on writing PRDs and SRSs with AI assistance</title>
        <link href="https://jamesmcgrath.net/blog/prd-srs-ai"/>
        <updated>2025-07-19T00:00:00.000Z</updated>
        <id>https://jamesmcgrath.net/blog/prd-srs-ai</id>
        <content type="html"><![CDATA[<p>I use AI every day at work to assist me not only in coding but also in handling technical specifications and their challenges. As our organization began adopting AI more broadly, I have seen how the product team has encountered some stumbling blocks in trying to determine how best to use AI to assist them. Even though they are still in the early stages of figuring it out, here are my thoughts on how AI can be used in creating a product requirement document and software requirement specifications.</p>
<p>AI can be useful in project planning, especially when writing a product requirement document (PRD) and software requirement specification (SRS). While some teams might let AI handle the entire process independently, I believe AI works best as a collaborative assistant, helping guide and refine your thinking rather than replacing it entirely.</p>
<h2>PRD + AI</h2>
<p>Instead of telling it to just write the PRD, there should be a dialog about the product idea. Describe the basic concept and let AI probe deeper.</p>
<p>The product team can collaborate with AI throughout the PRD development process, leveraging its analytical capabilities in these key areas:</p>
<ul>
<li>Analyze user research and market data like a requirement analyst and thought partner</li>
<li>Structure insights and research findings into a coherent product vision</li>
<li>Identify and suggest additional success metrics that align with business objectives</li>
<li>Surface implicit assumptions that should be made explicit before development begins</li>
<li>Ensure the PRD addresses all technical, design, and business dimensions that downstream teams will need</li>
<li>Synthesize design and product perspectives to identify potential conflicts between business requirements and user experience</li>
<li>Review the completed PRD for inconsistencies, gaps, or unclear requirements</li>
<li>Analyze proposed changes and assess their ripple effects across other parts of the PRD</li>
</ul>
<p>This collaborative approach ensures your PRD becomes a robust foundation that reflects both AI insights and human strategic thinking.</p>
<h2>SRS + AI</h2>
<p>Once you have a solid PRD developed through this collaborative process, you can leverage that foundation to create an equally robust SRS. Use AI to help write the SRS by giving it the PRD, technical constraints and architecture details. This will help in guiding it to create your vision and architectural philosophy.</p>
<p>When writing the SRS you shouldn&#39;t solely rely on AI to write out the parts of the system you may not be familiar with. Development teams can enhance AI-generated specifications by leveraging domain expertise in targeted areas:</p>
<ul>
<li>Frontend developers review and refine frontend specifications, ensuring alignment with existing component libraries and user interface standards</li>
<li>Backend developers review and refine backend specifications, ensuring proper integration with current data architecture and API designs</li>
</ul>
<p>However, keep in mind that AI may not understand existing legacy requirements or technology limitations in either domain.</p>
<p>AI can be a stress test and help find inconsistencies between the PRD and SRS. It can help identify scenarios where the SRS might not address the user needs in the PRD or how tech constraints might limit the envisioned product UX and may reveal important trade-offs that need to be considered.</p>
<p>Analysis of the PRD and SRS can serve as a technical review before coding that can help spot challenges, ambiguities, and suggest alternative tech approaches. It can also help with determining how changes due to technical constraints or product requirements may affect the system.</p>
<h2>AI Output</h2>
<p>Quality of the outputted documents (SRS &amp; PRD) depends on specificity and context that is given to the AI. The more you can tell it about the technical environment, user base, architectural constraints, the better the AI assistance will be. It also can help with structural analysis, surface blind spots, and translate between different levels of abstraction.</p>
<p>If you have multiple team members or teams involved in writing the SRS or PRD it would help to develop prompt guidelines and technical vocabulary to ensure that there is a consistent style to the output.</p>
<p>The final product of AI assistance depends on you. Its suggestions may not align with team capabilities or organizational priorities. Ultimately, these are your documents. This collaborative approach—where AI enhances rather than replaces human judgment—ensures you get the benefits of AI&#39;s analytical capabilities while maintaining the strategic vision and practical wisdom that only your team possesses.</p>
]]></content>
    </entry>
    
    <entry>
        <title>Focus Styling: From :focus to :focus-visible</title>
        <link href="https://jamesmcgrath.net/blog/focus-styles"/>
        <updated>2025-07-16T00:00:00.000Z</updated>
        <id>https://jamesmcgrath.net/blog/focus-styles</id>
        <content type="html"><![CDATA[<p>Initially, browsers universally applied focus styles via the <code>:focus</code> pseudo-class to all focusable elements, typically as a focus ring. This default user-agent style ensured every link and button displayed a focus ring when focused. Developers often overrode this default for visual consistency.</p>
<p>However, always-visible focus rings on every element were considered visually distracting and detrimental to UX. Users found the constant focus indicators overwhelming.</p>
<p>Modern browsers now selectively apply focus rings based on heuristics. Focus styles are crucial for keyboard navigation and programmatic focus management (JavaScript). However, when users directly interact using mouse or touch, focus rings are often redundant. Form elements remain a key exception where clear focus indication remains important.</p>
<p>The <code>:focus-visible</code> pseudo-class addresses this by respecting the browser&#39;s intelligent focus indication while allowing style customization. This provides a more nuanced approach, but <code>:focus</code> styles remain necessary for older browser compatibility. However you can get away with just providing <code>:focus-visible</code> styles if you can absolutely guarantee that all your users are on a device that supports <code>:focus-visible</code> , otherwise you will need to use a polyfill or a CSS fallback method.</p>
<p>For broad browser support, consider these fallback strategies:</p>
<p><strong>Method 1: <code>@supports</code> Feature Query</strong></p>
<p>CSS</p>
<pre><code>*:focus { /* Default focus styles */ }
@supports selector(:focus-visible) {
  *:focus { /* Undo default styles in modern browsers */ }
  *:focus-visible { /* Reapply for :focus-visible */ }
}
</code></pre>
<p><strong>Method 2: <code>:not(:focus-visible)</code> Fallback</strong></p>
<p>CSS</p>
<pre><code>*:focus { /* Default focus styles */ }
*:focus:not(:focus-visible) { /* Undo default styles in :focus-visible browsers */ }
*:focus-visible { /* Styles for :focus-visible browsers */ }
</code></pre>
<p>While less explicit, you could also use:</p>
<p>CSS</p>
<pre><code>*:focus-visible { /* Focus styles for :focus-visible */ }
@supports not selector(:focus-visible) {
  *:focus { /* Focus styles for older browsers */ }
}
</code></pre>
<p>Methods 1 &amp; 2 are clearer in demonstrating the fallback approach, explicitly showing default <code>:focus</code> styles being refined for <code>:focus-visible</code> browsers.</p>
]]></content>
    </entry>
    
    <entry>
        <title>Alert Types: Notes for Better UX, Decision-Making and Accessibility</title>
        <link href="https://jamesmcgrath.net/blog/notes-on-alert-types"/>
        <updated>2025-06-27T00:00:00.000Z</updated>
        <id>https://jamesmcgrath.net/blog/notes-on-alert-types</id>
        <content type="html"><![CDATA[<p>Let&#39;s talk about alerts and how they are supposed to work. I was recently in a discussion about them at work, and to better prepare myself for a discussion with the product team and C-suite officers, I should refresh myself about them. So here are my notes about them.</p>
<h2>Interactive &amp; non-interactive</h2>
<p>There are two types of alerts: interactive and non-interactive. </p>
<h3>Interactive alerts</h3>
<p>Interactive alerts require user interaction, which is usually to confirm or provide an input and to capture the user&#39;s attention. They must be persistent, meaning that they are visible until an action that dismisses it is performed by the user. Additionally, it should guide the user through the required action. </p>
<p>For accessibility, alerts require that the focus moves to the alert and focus is trapped. It should not be an aria-live region. The first focusable element in the alert should be focused on. It should be dismissible via keyboard by pressing the Escape key. When the alert is dismissed, focus should return to where it was prior to being moved to the alert. </p>
<h3>Non-interactive alerts</h3>
<p>Non-interactive alerts are for informational messages that don&#39;t require an action. These include form validation, status updates, or system notifications. They can be auto-dismissed or persistent. It depends on the priority of the message. Most of the time, they are implemented to auto-dismiss after a certain time period. 4 seconds is what is recommended. These alerts should also have an action to dismiss, such as a close button or the ability to swipe it away. The alert should not disrupt the user&#39;s attention. </p>
<p>For accessibility, non-interactive alerts need to be announced to screen readers by using a live region. They should also appear in the DOM but not receive focus. </p>
<p>For auto-dismissing, the timing is important. It&#39;s recommended that the alert stay open for about four to five seconds or longer. The reasoning is: </p>
<ul>
<li><p>Users may miss it.</p>
</li>
<li><p>Users will have different reading speeds. </p>
</li>
<li><p>User with Visual or cognitive limitations may need more time to read the message.</p>
</li>
</ul>
<p>It&#39;s highly recommended not to auto-dismiss non-interactive alerts, but as with most things, it really depends on the importance of the message. </p>
<h2>Alert Decision Table</h2>
<div class="table-wrapper my-6 w-full overflow-auto">
<table>xs
  <thead>
    <tr>
      <th><strong>Alert Type</strong></th>
      <th><strong>What It's For</strong></th>
      <th><strong>When to Use</strong></th>
      <th><strong>User Experience</strong></th>
      <th><strong>Primary ARIA Role</strong></th>
      <th><strong>Focus Management</strong></th>
      <th><strong>Persistence</strong></th>
      <th><strong>Announcement</strong></th>
      <th><strong>Best For</strong></th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><strong>Interactive Alert</strong></td>
      <td>Getting user input or decisions</td>
      <td>Requires user action/response</td>
      <td>User must stop and respond</td>
      <td><code class="text-xs">role="alertdialog"</code></td>
      <td>Must move focus to first control</td>
      <td>Must be persistent</td>
      <td>Not applicable (focus-based)</td>
      <td>Error fixes, confirmations, critical decisions</td>
    </tr>
    <tr>
      <td><strong>Non-Interactive Alert</strong></td>
      <td>Giving users critical information</td>
      <td>Important information, no user action required</td>
      <td>User continues their current task</td>
      <td><code class="text-xs">role="alert"</code></td>
      <td>Don't move focus</td>
      <td>Should be persistent</td>
      <td>Announce immediately</td>
      <td>Connection lost, security warnings, system errors</td>
    </tr>
    <tr>
      <td><strong>Non-Interactive Status</strong></td>
      <td>Giving users general information</td>
      <td>Information only, no user action required</td>
      <td>User continues their current task</td>
      <td><code class="text-xs">role="status"</code></td>
      <td>Don't move focus</td>
      <td>Can auto-dismiss (4+ seconds)</td>
      <td>Announce politely</td>
      <td>Status updates, confirmations, notifications</td>
    </tr>
  </tbody>
</table>
</div>

<h2>Conclusion</h2>
<p>Overall, I think a basic alert is relatively straightforward. Like most things,  it all depends on what kind of alert you are implementing and the additional features that may need to go with it. For example, you may be asked to stack multiple consecutive alerts. During one of the discussions at my work, a stakeholder brought up the idea of having a timer in the alert. You need to think a little about how that would change things. If you want to meet WCAG timing criteria, you would have to devise a way to pause or extend the timer. That&#39;s just an example of how things can get complicated in their development. </p>
<p>I hope these notes about alerts have been helpful.</p>
]]></content>
    </entry>
    
    <entry>
        <title>Give your Ai assistant a map of your project</title>
        <link href="https://jamesmcgrath.net/blog/give-llm-project-file-list"/>
        <updated>2025-06-26T00:00:00.000Z</updated>
        <id>https://jamesmcgrath.net/blog/give-llm-project-file-list</id>
        <content type="html"><![CDATA[<p>One of the things I am really bad at is deciding where to put a file in a project. I realized that I can let AI solve that for me by giving  it more context on the project with a map of the project or a specific folder. I like to use tree to get this. </p>
<h2>Using tree</h2>
<p>In Aider I <code>/run tree -I node_modules</code> and add it to the chat.  Then I <code>/ask</code> Aider where to put the file I want to create. </p>
<p>This is what the tree output looks like from the command above. <code>-I</code> is ignore. </p>
<pre><code class="language-bash">├── jsconfig.json
├── package.json
├── pnpm-lock.yaml
├── README.md
├── src
│   ├── app.css
│   ├── app.html
│   ├── lib
│   │   └── images
│   │       ├── github.svg
│   │       ├── svelte-logo.svg
│   │       ├── svelte-welcome.png
│   │       └── svelte-welcome.webp
│   └── routes
│       ├── +layout.svelte
│       ├── +page.js
│       ├── +page.svelte
│       ├── about
│       │   ├── +page.js
│       │   └── +page.svelte
│       ├── Counter.svelte
│       ├── Header.svelte
│       └── sverdle
│           ├── +page.server.js
│           ├── +page.svelte
│           ├── game.js
│           ├── how-to-play
│           │   ├── +page.js
│           │   └── +page.svelte
│           └── words.server.js
├── static
│   ├── favicon.png
│   └── robots.txt
├── svelte.config.js
└── vite.config.js
</code></pre>
<h2>Using ls</h2>
<p>If you don&#39;t have tree installed you can do this with <code>ls -R1</code> which will list all the files in a single column.</p>
<pre><code class="language-bash">node_modules/
package.json
pnpm-lock.yaml
README.md
src/
static/
svelte.config.js
vite.config.js

./node_modules:
@fontsource/
@neoconfetti/
@sveltejs/
svelte@
vite@

./node_modules/@fontsource:
fira-mono@

./node_modules/@neoconfetti:
svelte@

./node_modules/@sveltejs:
adapter-auto@
kit@
vite-plugin-svelte@

./src:
app.css
app.html
lib/
routes/

./src/lib:
images/

./src/lib/images:
github.svg
svelte-logo.svg
svelte-welcome.png
svelte-welcome.webp

./src/routes:
+layout.svelte
+page.js
+page.svelte
about/
Counter.svelte
Header.svelte
sverdle/

./src/routes/about:
+page.js
+page.svelte

./src/routes/sverdle:
+page.server.js
+page.svelte
game.js
how-to-play/
words.server.js

./src/routes/sverdle/how-to-play:
+page.js
+page.svelte

./static:
favicon.png
robots.txt
</code></pre>
<h2>Misc Terminal ways</h2>
<p>Unfortunately macOs terminal <code>ls</code> command can&#39;t exclude files or directories. However these commands can. </p>
<p>**ls + grep **</p>
<pre><code class="language-bash"># pipe to grep
ls -R | grep -v node_modules

# pipe to grep with multiple exclusions
ls -R | grep -vE &quot;(node_modules|\.git|dist)&quot;
</code></pre>
<p>**find ** </p>
<pre><code class="language-bash"># Single exclude
find . -name node_modules -prune -o -print

# Multiple excludes 
find . -name node_modules -prune -o -name .git -prune -o -print
</code></pre>
<p>Another alternative for macOs is to install the GNU coreutils </p>
<pre><code class="language-bash">brew install coreutils
</code></pre>
<p><strong>gls</strong></p>
<pre><code class="language-bash"># ignore flag
gls -R1 --ignore=node_modules

gls -R1Inode_modules
</code></pre>
]]></content>
    </entry>
    
    <entry>
        <title>Vibe Coding an Alt Text Generator</title>
        <link href="https://jamesmcgrath.net/blog/vibe-coding-alt-text-generator"/>
        <updated>2025-06-23T00:00:00.000Z</updated>
        <id>https://jamesmcgrath.net/blog/vibe-coding-alt-text-generator</id>
        <content type="html"><![CDATA[<p>I made an alt text generator using Aider with Claude Sonnet 4, SvelteKit and Tailwind. I didn&#39;t think it was very hard to do since I straight up vibe coded it.</p>
<h2>Why?</h2>
<p>I made this for a few reasons.</p>
<ul>
<li>I wanted to see how Sonnet 4 is compared to other LLMs for coding.</li>
<li>I don&#39;t like writing alt text very much.</li>
<li>I can use it at work if they want to integrate it into the cms.</li>
<li>I never thought it would be hard.</li>
<li>I haven&#39;t really purely vibe coded anything</li>
</ul>
<p>The last point is really the whole reason why. I just wanted to vibe code something. I really didn&#39;t have any plans for this other than I wanted to upload an image or supply a url and have OpenAI&#39;s o4 mini model return alt text describing the image. No preconceived design, components, architecture or guidelines for the LLM.</p>
<p>Before we continue I think it&#39;s pretty important to inform you that where I work we are all in on AI assisted coding. I pretty much use Aider everyday all day. I don&#39;t consider that vibe coding because I do plan out everything I do. I review the code and make sure it is acceptable. I don&#39;t rely on it 100% for everything either. We spent a lot of time figuring out what system works for us and creating guidelines for the LLM to use to code.</p>
<h2>What I did.</h2>
<p>The first thing I did was setup a SvelteKit project with Tailwind. Then ran Aider and started prompting away.</p>
<p>I let the LLM decide on the look and layout of it. I do feel it looks rather Bootstrapish. No big deal. If I really wanted to I could style it myself.</p>
<p>In the end I got what I wanted and more. It does the image upload (to nowhere) and converts it to base64 and then sends it to Sonnet 3.5 for analysis. It returns the output, which is the alt text, into an editable area with a button that can copy the text to the user&#39;s clipboard. There is also a way to add additional context for the image to be sent along with the image to give the LLM some help in describing the image better. I was also able to add a history that will let you compare the output with a previous output. That is kept in the browser&#39;s session storage. Also on the page there is a section that lists the WCAG criteria for alt text.</p>
<p>I did have to direct Aider on accessibility. Can&#39;t make a tool for accessibility not accessible. It would either not do it or would do it badly.</p>
<p>You may have noticed that I am using Sonnet 3.5 instead of o4 mini. The reason is something I think is pretty funny. I felt that ChatGPT would be the right tool to use to write the function that sends the image data to o4 mini. However it failed at it. After playing with it for about 20min I decided, never mind this let&#39;s see if Sonnet can write its own function to upload to itself. It wrote one on the first try and I decided to use Sonnet 3.5 instead of 4. Which is not an issue, I can always change it. The reason I wanted o4 mini was because it&#39;s cheap.</p>
<p>I did this in 2 sessions and it took a total of about 4 hours. It cost something like $4 USD to make.</p>
<h2>What I did according to AI</h2>
<p>Since the entire Aider log is available, I gave it to Claude and ChatGPT to give me a summary of what I did.</p>
<p>Both of them broke it down into several steps. ChatGPT counted 8 and Claude counted 7. Both of them were pretty much the same. The following list is paraphrasing their steps.</p>
<h3>Step 1: Initial setup</h3>
<p>I setup the SvelteKit project and tailwind and the initial <code>AltText.svelte</code> component creation. This was done without Aider. It was just typing in the npm command in my terminal to create a Svelte project with tailwind and whatever other options I selected. After that it was creating the <code>AltText.svelte</code> component which I left empty.</p>
<p>After that is when I started to use Aider to start making the component. I had it make the switch which toggles between image upload and URL input, with base64 conversion for LLM processing. I didn&#39;t do any back-end wiring.</p>
<h3>Step 2: UI/UX</h3>
<p>This is where I added the file upload button and drop area, URL sanitization, and submit button. I also had it display some placeholder text in an editable textarea with copy-to-clipboard functionality. This gave me the first visible flow of how it would work before hooking up to an API. I let Sonnet decide on the look of everything. At most I told it to move things, for example to place the input for additional context above the submit button instead of after.</p>
<h3>Step 3: Accessibility</h3>
<p>In this step I reviewed the accessibility of what was made with Aider and Sonnet. It was missing some keyboard interactions. The uploader did not work at all with the keyboard. You would tab past it. The inputs were missing labels. I also took care of the error handling in this step and did a little bit of UX work.</p>
<p>I made the submit button look disabled. From my understanding when you add the <code>disabled</code> attribute to form elements, screen readers will not have any visibility on the element and skip over them. You could still click on it or submit via keyboard but you would get an error message saying you need to upload something.</p>
<p>I also added an accordion that displays the WCAG guidelines for alt text. I think it&#39;s a useful thing to have but also not be in the way of the UI. Which is why I put it in an accordion.</p>
<h3>Step 4: Polishing and more features</h3>
<p>I tried to add some quality metrics here. I added a character counter and tried to make a quality meter. This is where the AI really seemed to struggle. It chose to do some funky way of measuring the character count with a Svelte <code>$effect</code>. I had to tell it to just do a normal character count on the returned text.</p>
<p>The quality meter didn&#39;t really work. Sonnet implemented a series of general conditions that you could consider a good starting point for a quality meter. For example one of the conditions was that it checked if the words image, or picture of were used. The conditions were ok, however the meter just stayed red all the time and never changed to green. The best way to have done this would be to send it back to an LLM for evaluation. I considered that out of the scope of what I wanted. I decided to just eliminate that feature altogether.</p>
<p>I also added a reset feature to clear everything and a history section that uses the session storage. You can compare the text to a previous version in the event that you regenerated the alt text. Sonnet made a nice little modal that would show you the 2 versions.</p>
<h3>Step 5: Backend API</h3>
<p>Originally I wanted to use OpenAI&#39;s GPT o4 mini. I thought ChatGPT would be the best person (AI) to write code for that. I didn&#39;t use Aider with the OpenAI API for this. I did it in the chat window. The results didn&#39;t work. Even when I gave some example code from its own documentation it just didn&#39;t work. After mucking around with it for a bit, I decided to switch to having Sonnet write code for itself to analyze the image. I did this in Aider because it was still connected to the Anthropic API. Worked on the first try.</p>
<h3>Step 6: Clean up and even more features.</h3>
<p>The last feature I added was showing a thumbnail of the image in the history preview section. Then I had Sonnet clean up the code, add JSDoc and write a <code>README.md</code>.</p>
<h2>Insights from ChatGPT and Claude.</h2>
<p>In addition to summarizing, I also asked both ChatGPT and Claude to give me some key insights they got from the logs. I am just going to paste in exactly what each wrote out. Claude made a list and ChatGPT made a table. Overall I agree with their observations.</p>
<h3>Claude</h3>
<h4>1. <strong>Iterative UX Design</strong></h4>
<p>Your process shows excellent iterative refinement - starting with basic functionality and continuously improving based on usage patterns. The progression from prominent WCAG guidelines to a subtle question mark icon demonstrates user-centered design thinking.</p>
<h4>2. <strong>Accessibility as a Core Requirement</strong></h4>
<p>You spent significant time on accessibility improvements, treating it as essential rather than an afterthought. This included keyboard navigation, proper error messaging, and ARIA attributes - valuable for readers building inclusive applications.</p>
<h4>3. <strong>Technology Pragmatism</strong></h4>
<ul>
<li><strong>Svelte 5 over React</strong>: Your preference for Svelte 5 runes shows consideration for developer experience</li>
<li><strong>Provider Flexibility</strong>: Switching from OpenAI to Anthropic when encountering issues demonstrates adaptable architecture</li>
<li><strong>No TypeScript</strong>: Choosing JavaScript for rapid prototyping while leaving TypeScript as an option</li>
</ul>
<h4>4. <strong>Real-World Problem Solving</strong></h4>
<p>The log reveals common development challenges:</p>
<ul>
<li>Svelte 5 reactivity debugging (character count issues)</li>
<li>Browser API limitations (contenteditable with two-way binding)</li>
<li>Environment variable security considerations</li>
</ul>
<h4>5. <strong>User Experience Evolution</strong></h4>
<p>The progression from basic alt text generation to a full-featured tool with history, context, regeneration, and quality feedback shows how requirements naturally expand during development.</p>
<h4>6. <strong>Development Workflow Insights</strong></h4>
<p>Your use of an AI coding assistant for rapid iteration demonstrates modern development practices - using AI for boilerplate while maintaining control over architecture and UX decisions.</p>
<h3>ChatGPT</h3>
<table>
<thead>
<tr>
<th><strong>Insight</strong></th>
<th><strong>Why it matters / how readers can apply it</strong></th>
</tr>
</thead>
<tbody><tr>
<td><strong>Micro-prompts ≈ micro-commits</strong></td>
<td>You drove the assistant with <em>very small</em>, task-focused prompts (&quot;add a switch&quot;, &quot;make it collapsible&quot;). This mirrors good Git practice: atomic commits and fast feedback. Encourage readers to keep prompts laser-focused; it reduces hallucinations and makes diff-based review trivial.</td>
</tr>
<tr>
<td><strong>Design → a11y → code, not the other way around</strong></td>
<td>You repeatedly paused to ask &quot;Does this meet WCAG?&quot; before finalizing UI pieces. Front-loading accessibility avoids painful retrofits. Call out the specific a11y issues you caught early (keyboard-only upload, aria roles, disabled-state pattern).</td>
</tr>
<tr>
<td><strong>Human-in-the-loop UX for alt text</strong></td>
<td>Instead of a one-click generator, you implemented editable output, context injection, regeneration, and quality hints. This respects that alt text is descriptive writing, not raw inference. Highlight that the AI is a <em>drafting partner</em>, not an oracle.</td>
</tr>
<tr>
<td><strong>Local persistence for iteration</strong></td>
<td>Storing histories in localStorage let you compare versions offline—useful when model calls are metered. Point out this lightweight alternative to databases for single-page tools.</td>
</tr>
<tr>
<td><strong>Vendor agility &amp; fallback strategy</strong></td>
<td>When OpenAI vision failed you toggled to Anthropic with minimal diff. Abstracting the fetch layer (endpoint file, env vars) future-proofs the project and is a live example of &quot;LLM polyglot&quot; design.</td>
</tr>
<tr>
<td><strong>Security basics still apply</strong></td>
<td>Even rapid AI prototyping must respect .gitignore, .env, and example templates. Readers often overlook this when copying tutorial repos—reinforce that habit.</td>
</tr>
<tr>
<td><strong>Early realism about stretch goals</strong></td>
<td>You removed the real-time quality bar when it proved flaky. Blogging about <em>what you decided</em> *<strong>not*</strong> <em>to ship</em> is valuable: it teaches scope management and honesty over feature-checkbox culture.</td>
</tr>
<tr>
<td><strong>Tooling friction points</strong></td>
<td>Notable hurdles: Svelte two-way binding forbids dynamic contenteditable, event-handler syntax errors, SDK typings. Sharing those fixes saves readers time.</td>
</tr>
<tr>
<td><strong>Prompt patterns</strong></td>
<td>&quot;/ask&quot; to explore options, then &quot;do it&quot; for execution. Calling out this <em>explore → commit</em> cycle shows how to &quot;pair program&quot; with an LLM effectively.</td>
</tr>
<tr>
<td><strong>Progressive disclosure of complexity</strong></td>
<td>You started with a static mock, then added context, then regeneration, then history—each layer unlocked after the previous one felt solid. That incremental reveal keeps cognitive load low for both the AI and the human maintainer. Encourage readers to resist the urge to prompt everything at once.</td>
</tr>
</tbody></table>
<h2>The overall experience</h2>
<p>I&#39;d say the overall experience of vibe coding something was pretty fun. A little frustrating when it would just kind of do something stupid or just not be able to do what you asked at all. However that is not much different from dealing with people in my opinion.</p>
<p>It wasn&#39;t a super complex project or really a hard project. If I had not used AI it probably would have taken about 1-3 days. I know I would have gotten caught up in the UI parts of it especially the styling part. The longer I would have taken on it, the more scope creep would happen. I recognized from my past personal projects I need to watch adding additional unplanned features. It&#39;s always &quot;while I am doing this I can extend it to do that&quot; or &quot;why not one more crazy idea.&quot;</p>
<p>Coding with AI without a fully structured plan can be pretty tricky and hard. I mentioned earlier that I do use Aider at work but we have a plan, designs and a product to make. If you let the vibe coding get out of hand you will get a lot of weird code and kind of waste money because you will find yourself going 2 steps forward 1 step back.</p>
]]></content>
    </entry>
    
    <entry>
        <title>Array of elements from Svelte each block</title>
        <link href="https://jamesmcgrath.net/blog/array-of-elements-svelte"/>
        <updated>2023-10-02T00:00:00.000Z</updated>
        <id>https://jamesmcgrath.net/blog/array-of-elements-svelte</id>
        <content type="html"><![CDATA[<p>Since moving from Vue to Svelte one of the things I miss from Vue is when it would create an array of elements using a <code>ref</code> in a <code>v-for</code> loop. </p>
<p>Unfortanately you can&#39;t do this in Svelte. The binded variable will be assigned the last element in the each block. </p>
<pre><code class="language-js">&lt;script&gt;
    let elementArray; 
    // elementArray = div
&lt;/script&gt;

{each items as item } 
    &lt;div bind:this={elementArray}&gt;{item}&lt;/div&gt;
{/each}
</code></pre>
<p>A small change to that example will produce an array of elements.</p>
<pre><code class="language-js">&lt;script&gt;
    let elementArray = [];
    // elementArray = [div, div, div...]
&lt;/script&gt;

{each items as item, index } 
    &lt;div bind:this={elementArray[index]}&gt;{item}&lt;/div&gt;
{/each}
</code></pre>
]]></content>
    </entry>
    
    <entry>
        <title>How to calculate vw and vh</title>
        <link href="https://jamesmcgrath.net/blog/how-to-calculate-vw-vh"/>
        <updated>2023-09-25T00:00:00.000Z</updated>
        <id>https://jamesmcgrath.net/blog/how-to-calculate-vw-vh</id>
        <content type="html"><![CDATA[<p>Recently I found a need to calculate <code>vh</code> and <code>vw</code>  </p>
<p><code>vw</code> = hundredths of the viewport width.
<code>vh</code> = hundredths of the viewport height.</p>
<p>This is how to calculate <code>1vw</code> or <code>1vh</code></p>
<pre><code class="language-js">
// 1vh
100 / document.documentElement.clientHeight;

// 1vw
100 / document.documentElement.clientWidth;

// calculate a px value of 30
(100 / document.documentElement.clientHeight) * 30
(100 / document.documentElement.clientWidth) * 30
</code></pre>
]]></content>
    </entry>
    
</feed>