<?xml version="1.0" encoding="UTF-8"?>
<rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/"
    xmlns:atom="http://www.w3.org/2005/Atom" xmlns:media="http://search.yahoo.com/mrss/" version="2.0">
    <channel>
        
        <title>
            <![CDATA[ Aiyedogbon Abraham - freeCodeCamp.org ]]>
        </title>
        <description>
            <![CDATA[ Browse thousands of programming tutorials written by experts. Learn Web Development, Data Science, DevOps, Security, and get developer career advice. ]]>
        </description>
        <link>https://www.freecodecamp.org/news/</link>
        <image>
            <url>https://cdn.freecodecamp.org/universal/favicons/favicon.png</url>
            <title>
                <![CDATA[ Aiyedogbon Abraham - freeCodeCamp.org ]]>
            </title>
            <link>https://www.freecodecamp.org/news/</link>
        </image>
        <generator>Eleventy</generator>
        <lastBuildDate>Tue, 29 Sep 2026 05:58:39 +0000</lastBuildDate>
        <atom:link href="https://www.freecodecamp.org/news/author/abrahamaiyedogbon/rss.xml" rel="self" type="application/rss+xml" />
        <ttl>60</ttl>
        
            <item>
                <title>
                    <![CDATA[ How to Build More Accessible Websites with WCAG 2.2 ]]>
                </title>
                <description>
                    <![CDATA[ A website can look polished, work perfectly with a mouse, and still be difficult for some people to use. A form might use colour as the only indication that something went wrong. A sticky header might ]]>
                </description>
                <link>https://www.freecodecamp.org/news/how-to-build-more-accessible-websites-with-wcag-2-2/</link>
                <guid isPermaLink="false">6a84895dd197512208831afc</guid>
                
                    <category>
                        <![CDATA[ Accessibility ]]>
                    </category>
                
                    <category>
                        <![CDATA[ #WCAG ]]>
                    </category>
                
                    <category>
                        <![CDATA[ wcag compliance ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Web Development ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ Aiyedogbon Abraham ]]>
                </dc:creator>
                <pubDate>Tue, 18 Aug 2026 16:33:33 +0000</pubDate>
                <media:content url="https://cdn.hashnode.com/uploads/covers/5e1e335a7a1d3fcc59028c64/5cb22632-e793-40cc-9ded-4b813430e708.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>A website can look polished, work perfectly with a mouse, and still be difficult for some people to use.</p>
<p>A form might use colour as the only indication that something went wrong. A sticky header might completely cover the element that currently has keyboard focus. A login form might prevent users from pasting a password from their password manager. Or a custom button might work when clicked with a mouse but do nothing when someone uses a keyboard.</p>
<p>These are development decisions, not problems that only appear during an accessibility audit.</p>
<p>The Web Content Accessibility Guidelines (WCAG) provide a common standard for identifying and reducing many of these barriers. WCAG 2.2 is the latest WCAG 2 Recommendation, and the World Wide Web Consortium (W3C) advises developers and organisations to use WCAG 2.2 whenever possible.</p>
<p>This article focuses on the WCAG 2.2 Level A and AA requirements that frequently affect frontend development. The aim is to show how accessibility requirements connect to frontend development decisions.</p>
<h2 id="heading-table-of-contents">Table of Contents</h2>
<ul>
<li><p><a href="#heading-what-is-wcag-22">What Is WCAG 2.2?</a></p>
</li>
<li><p><a href="#heading-how-wcag-conformance-works">How WCAG Conformance Works</a></p>
</li>
<li><p><a href="#heading-how-the-four-wcag-principles-work">How the Four WCAG Principles Work</a></p>
</li>
<li><p><a href="#heading-how-to-start-with-semantic-html">How to Start with Semantic HTML</a></p>
</li>
<li><p><a href="#heading-how-to-write-useful-text-alternatives-for-images">How to Write Useful Text Alternatives for Images</a></p>
</li>
<li><p><a href="#heading-how-to-handle-colour-contrast-text-resizing-and-reflow">How to Handle Colour, Contrast, Text Resizing, and Reflow</a></p>
</li>
<li><p><a href="#heading-how-to-make-an-interface-work-with-a-keyboard">How to Make an Interface Work with a Keyboard</a></p>
</li>
<li><p><a href="#heading-how-to-keep-keyboard-focus-visible">How to Keep Keyboard Focus Visible</a></p>
</li>
<li><p><a href="#heading-how-to-design-pointer-targets-and-dragging-interactions">How to Design Pointer Targets and Dragging Interactions</a></p>
</li>
<li><p><a href="#heading-how-to-build-more-accessible-forms">How to Build More Accessible Forms</a></p>
</li>
<li><p><a href="#heading-how-to-avoid-redundant-entry">How to Avoid Redundant Entry</a></p>
</li>
<li><p><a href="#heading-how-to-keep-help-consistent">How to Keep Help Consistent</a></p>
</li>
<li><p><a href="#heading-how-wcag-22-affects-authentication">How WCAG 2.2 Affects Authentication</a></p>
</li>
<li><p><a href="#heading-how-to-use-aria-without-replacing-html">How to Use ARIA Without Replacing HTML</a></p>
</li>
<li><p><a href="#heading-how-to-make-dynamic-status-messages-accessible">How to Make Dynamic Status Messages Accessible</a></p>
</li>
<li><p><a href="#heading-how-to-test-your-website-for-accessibility">How to Test Your Website for Accessibility</a></p>
</li>
<li><p><a href="#heading-conclusion">Conclusion</a></p>
</li>
</ul>
<h2 id="heading-what-is-wcag-22">What Is WCAG 2.2?</h2>
<p>WCAG stands for <strong>Web Content Accessibility Guidelines</strong>. W3C develops the standard to describe how web content can be made more accessible to people with disabilities.</p>
<p>WCAG 2.2 organises its requirements into principles, guidelines, and testable success criteria. The success criteria are technology-independent, which is important because WCAG doesn't exist specifically for HTML, React, WordPress, or any other implementation technology.</p>
<p>Consider this hierarchy:</p>
<pre><code class="language-text">Principle: Operable

    Guideline 2.1: Keyboard Accessible

        Success Criterion 2.1.1: Keyboard
</code></pre>
<p>The principle gives you the broad accessibility objective. The guideline narrows that objective, while the success criterion provides the testable requirement.</p>
<p>W3C also publishes resources such as <a href="https://www.w3.org/WAI/WCAG22/Understanding/">Understanding WCAG 2.2</a>, <a href="https://www.w3.org/WAI/WCAG22/quickref/">How to Meet WCAG 2.2</a>, and <a href="https://www.w3.org/WAI/WCAG22/Techniques/">Techniques for WCAG 2.2</a>. These resources explain the success criteria and provide implementation approaches, examples, and known failures. They're informative rather than part of the normative WCAG requirements.</p>
<p>A W3C technique can show one recognised way to satisfy a criterion, but WCAG generally doesn't require you to use that exact technique. Another implementation can also be valid if it meets the actual success criterion.</p>
<h2 id="heading-how-wcag-conformance-works">How WCAG Conformance Works</h2>
<p>WCAG defines three conformance levels: <strong>A, AA,</strong> and <strong>AAA</strong>.</p>
<p>The levels build on one another. A page can't claim Level AA conformance by satisfying only the criteria labelled AA. It must satisfy all applicable Level A and Level AA success criteria. Level AAA similarly includes A, AA, and AAA requirements.</p>
<p>This distinction is important because accessibility discussions sometimes reduce WCAG to individual checks.</p>
<p>You might fix the keyboard interaction on a menu, add alternatives to your images, and correct several contrast problems. Those are useful accessibility improvements, but they don't automatically make the entire website "WCAG AA compliant".</p>
<p>WCAG conformance applies to complete web pages. When a process requires several pages to complete, such as a checkout process, all pages in that process must conform at the claimed level.</p>
<p>The examples in this article therefore demonstrate ways to address particular accessibility requirements. They don't constitute a conformance claim for an entire application.</p>
<h2 id="heading-how-the-four-wcag-principles-work">How the Four WCAG Principles Work</h2>
<p>WCAG groups its guidelines under four principles commonly remembered with the acronym <strong>POUR</strong>: Perceivable, Operable, Understandable, and Robust.</p>
<p><strong>Perceivable</strong> means users need to be able to perceive the information you provide. Text alternatives, captions, contrast, and adaptable layouts fall under this principle.</p>
<p><strong>Operable</strong> concerns how people interact with the interface. Keyboard operation, focus behaviour, navigation, pointer interactions, and timing are examples.</p>
<p><strong>Understandable</strong> deals with whether users can understand the information and the way the interface behaves. Form instructions, useful error messages, predictable interfaces, and accessible authentication are relevant here.</p>
<p><strong>Robust</strong> concerns whether browsers and assistive technologies can correctly interpret the content. Semantic HTML, accessible names, roles, values, and states are central to this principle.</p>
<p>These categories are useful, but accessibility problems rarely respect the boundary between HTML, CSS, and JavaScript.</p>
<p>A custom dropdown, for example, might need semantic information in the markup, visible focus styling in CSS, and correct keyboard behaviour in JavaScript.</p>
<p>Accessibility therefore works best when it forms part of the implementation itself rather than becoming a separate task at the end of development.</p>
<h2 id="heading-how-to-start-with-semantic-html">How to Start with Semantic HTML</h2>
<p>One of the most useful accessibility decisions happens before you write any ARIA: choosing the correct HTML element.</p>
<p>Consider this:</p>
<pre><code class="language-html">&lt;div onclick="submitForm()"&gt;Submit&lt;/div&gt;
</code></pre>
<p>A mouse user may be able to click the element, but a <code>div</code> doesn't automatically behave like a button.</p>
<p>Compare it with this:</p>
<pre><code class="language-html">&lt;button type="submit"&gt;Submit&lt;/button&gt;
</code></pre>
<p>The native <code>button</code> already communicates its role to the browser and provides the expected keyboard behaviour.</p>
<p>This relates to <a href="https://www.w3.org/TR/WCAG22/#name-role-value">Success Criterion 4.1.2 Name, Role, Value</a>, which requires user interface components to expose information such as their name and role programmatically. W3C notes that standard controls already provide much of this information when developers use them according to their specification.</p>
<p>The practical implication is simple: don't recreate browser behaviour unless you need to.</p>
<h3 id="heading-how-semantic-html-communicates-page-structure">How Semantic HTML Communicates Page Structure</h3>
<p>Semantic HTML also helps expose relationships between parts of a page.</p>
<p>You could build a page like this:</p>
<pre><code class="language-html">&lt;div class="top"&gt;
  ...
&lt;/div&gt;

&lt;div class="navigation"&gt;
  ...
&lt;/div&gt;

&lt;div class="content"&gt;
  &lt;div class="title"&gt;Account Settings&lt;/div&gt;
  ...
&lt;/div&gt;
</code></pre>
<p>The classes may create the visual layout you want, but they don't necessarily communicate the same structure programmatically.</p>
<p>A more meaningful structure could be:</p>
<pre><code class="language-html">&lt;header&gt;
  ...
&lt;/header&gt;

&lt;nav aria-label="Primary"&gt;
  ...
&lt;/nav&gt;

&lt;main id="main-content"&gt;
  &lt;h1&gt;Account Settings&lt;/h1&gt;
  ...
&lt;/main&gt;
</code></pre>
<p><a href="https://www.w3.org/TR/WCAG22/#info-and-relationships">Success Criterion 1.3.1 Info and Relationships</a> requires structure and relationships communicated visually to also be programmatically determinable or available in text. Semantic markup can provide this information without requiring developers to recreate it with additional accessibility attributes.</p>
<p>This doesn't mean that using <code>&lt;main&gt;</code>, <code>&lt;nav&gt;</code>, and <code>&lt;h1&gt;</code> automatically makes a page accessible. It means you're giving the browser more accurate information about what the content represents.</p>
<h3 id="heading-how-to-add-a-skip-link">How to Add a Skip Link</h3>
<p>Repeated page navigation creates another issue.</p>
<p>If a page has a large navigation menu, a keyboard user may otherwise need to move through those links every time before reaching the main content.</p>
<p>A skip link provides another path:</p>
<pre><code class="language-html">&lt;a class="skip-link" href="#main-content"&gt;
  Skip to main content
&lt;/a&gt;

&lt;header&gt;
  ...
&lt;/header&gt;

&lt;nav aria-label="Primary"&gt;
  ...
&lt;/nav&gt;

&lt;main id="main-content"&gt;
  ...
&lt;/main&gt;
</code></pre>
<p>You can position the link outside the normal view until it receives keyboard focus:</p>
<pre><code class="language-css">.skip-link {
  position: absolute;
  top: -4rem;
  left: 1rem;
}

.skip-link:focus {
  top: 1rem;
}
</code></pre>
<p>This is one recognised way to support <a href="https://www.w3.org/TR/WCAG22/#bypass-blocks">Success Criterion 2.4.1 Bypass Blocks</a>, which requires a mechanism for bypassing blocks of repeated content. WCAG requires the outcome rather than this exact CSS implementation.</p>
<p>The broader principle is worth keeping: <strong>use HTML's existing semantics before adding custom semantics yourself</strong>.</p>
<h2 id="heading-how-to-write-useful-text-alternatives-for-images">How to Write Useful Text Alternatives for Images</h2>
<p>Adding <code>alt</code> text is one of the best-known accessibility practices, but the rule is often oversimplified.</p>
<p><a href="https://www.w3.org/TR/WCAG22/#non-text-content">Success Criterion 1.1.1 Non-text Content</a> requires non-text content to have a text alternative that serves an equivalent purpose, subject to several exceptions. Decorative content, for example, should be implemented so assistive technologies can ignore it.</p>
<p>The important word is <strong>purpose</strong>.</p>
<p>Consider this image:</p>
<pre><code class="language-html">&lt;img src="revenue-chart.png" alt="Chart"&gt;
</code></pre>
<p>The alternative tells the user that the page contains a chart. It doesn't communicate anything the chart actually tells a sighted user.</p>
<p>If the main message is the change in revenue, an alternative could be:</p>
<pre><code class="language-html">&lt;img
  src="revenue-chart.png"
  alt="Revenue increased from £1.2 million in 2024 to
       £1.8 million in 2025."
&gt;
</code></pre>
<p>That doesn't mean every chart can be reduced to one sentence.</p>
<p>If the chart contains several data series or detailed values that readers need, you may also need a nearby explanation, accessible table, or another way of communicating the underlying information.</p>
<p>The alternative should reflect what the image contributes in its context.</p>
<h3 id="heading-how-to-handle-decorative-images">How to Handle Decorative Images</h3>
<p>A decorative image serves a different purpose.</p>
<p>Consider a visual divider:</p>
<pre><code class="language-html">&lt;img src="decorative-line.svg" alt=""&gt;
</code></pre>
<p>The empty <code>alt</code> value indicates that the image doesn't contribute information that needs to be announced.</p>
<p>A missing <code>alt</code> attribute and <code>alt=""</code> are therefore not interchangeable. The empty alternative is an intentional decision.</p>
<h3 id="heading-how-to-handle-icons-inside-controls">How to Handle Icons Inside Controls</h3>
<p>Now consider a search button containing a magnifying-glass SVG.</p>
<p>The relevant information isn't that the user is looking at a magnifying glass. The important information is that the control starts a search.</p>
<pre><code class="language-html">&lt;button type="submit" aria-label="Search"&gt;
  &lt;svg aria-hidden="true" viewBox="0 0 24 24"&gt;
    &lt;path d="M10 4a6 6 0 1 0 0 12a6 6 0 0 0 0-12Z"&gt;&lt;/path&gt;
    &lt;path d="m14.5 14.5 5 5"&gt;&lt;/path&gt;
  &lt;/svg&gt;
&lt;/button&gt;
</code></pre>
<p>The button receives the accessible name <code>Search</code>, while the SVG itself doesn't add duplicate information.</p>
<p>When deciding what alternative to provide, ask a more useful question than "What does this image look like?"</p>
<p>Ask: <strong>What information or function would the user lose if they couldn't perceive this image visually?</strong> That distinction matters when implementing accessibility.</p>
<h2 id="heading-how-to-handle-colour-contrast-text-resizing-and-reflow">How to Handle Colour, Contrast, Text Resizing, and Reflow</h2>
<p>Accessibility also affects ordinary CSS decisions.</p>
<p>A layout may look correct at your preferred viewport size and still become difficult to use when somebody changes the way content is displayed.</p>
<h3 id="heading-how-to-avoid-relying-only-on-colour">How to Avoid Relying Only on Colour</h3>
<p>Imagine a form that changes an input border from grey to red when validation fails:</p>
<pre><code class="language-css">.input {
  border: 1px solid #777;
}

.input.error {
  border-color: red;
}
</code></pre>
<p>The colour communicates that something changed, but a user needs to perceive that colour difference to understand the state.</p>
<p><a href="https://www.w3.org/TR/WCAG22/#use-of-color">Success Criterion 1.4.1 Use of Color</a> requires colour not to be the only visual means used to convey information, indicate an action, prompt a response, or distinguish a visual element.</p>
<p>An improved implementation can combine styling with actual text:</p>
<pre><code class="language-html">&lt;label for="email"&gt;Email address&lt;/label&gt;

&lt;input
  id="email"
  name="email"
  type="email"
  aria-invalid="true"
  aria-describedby="email-error"
&gt;

&lt;p id="email-error"&gt;
  Enter an email address in the format name@example.com.
&lt;/p&gt;
</code></pre>
<p>You can still use a red border. It just shouldn't carry the message alone.</p>
<h3 id="heading-how-to-check-text-contrast">How to Check Text Contrast</h3>
<p><a href="https://www.w3.org/TR/WCAG22/#contrast-minimum">Success Criterion 1.4.3 Contrast (Minimum)</a> requires regular text to have a contrast ratio of at least <strong>4.5:1</strong>. Qualifying large-scale text has a minimum ratio of <strong>3:1</strong>, subject to the criterion's exceptions.</p>
<p>Don't judge contrast only by looking at the colours. Two colours can look sufficiently different on your display while still falling below the required ratio. Use a contrast-testing tool as part of your design and development process.</p>
<h3 id="heading-how-non-text-contrast-differs-from-text-contrast">How Non-Text Contrast Differs from Text Contrast</h3>
<p><a href="https://www.w3.org/TR/WCAG22/#non-text-contrast">Success Criterion 1.4.11 Non-text Contrast</a> deals with visual information needed to identify interface components, states, and meaningful graphical objects. The required ratio is generally <strong>3:1</strong> against adjacent colours, subject to the criterion's scope and exceptions.</p>
<p>This can affect things such as custom form controls, meaningful icons, component boundaries, selected states, and graphical information.</p>
<p>Passing the text contrast requirement therefore doesn't automatically mean the rest of the interface has sufficient contrast.</p>
<h3 id="heading-how-to-support-text-resizing">How to Support Text resizing</h3>
<p><a href="https://www.w3.org/TR/WCAG22/#resize-text">Success Criterion 1.4.4 Resize Text</a> requires text, with specified exceptions, to be resizable up to 200% without loss of content or functionality.</p>
<p>Fixed dimensions often expose problems here.</p>
<p>Consider:</p>
<pre><code class="language-css">.card {
  height: 180px;
  overflow: hidden;
}
</code></pre>
<p>If text grows beyond the space the developer assumed it would need, some content can disappear.</p>
<p>Where the design doesn't genuinely require a fixed height, allowing the component to grow is safer:</p>
<pre><code class="language-css">.card {
  min-height: 180px;
}
</code></pre>
<p>This doesn't prove that the component passes the criterion. You still need to resize the text and inspect the result.</p>
<p>The CSS simply removes one common source of failure.</p>
<h3 id="heading-how-to-design-for-reflow">How to Design for Reflow</h3>
<p><a href="https://www.w3.org/TR/WCAG22/#reflow">Success Criterion 1.4.10 Reflow</a> addresses the ability to use content at narrow equivalent dimensions without losing information or functionality or requiring prohibited two-dimensional scrolling.</p>
<p>For vertically scrolling content, the criterion uses a width equivalent to <strong>320 CSS pixels</strong>. Certain content, such as some maps and data tables, may genuinely require two-dimensional layout and falls under the criterion's exceptions.</p>
<p>A flexible layout can help ordinary content adapt:</p>
<pre><code class="language-css">.settings-grid {
  display: grid;
  grid-template-columns:
    repeat(auto-fit, minmax(min(100%, 18rem), 1fr));
  gap: 1rem;
}
</code></pre>
<p>As space decreases, the cards move onto new rows rather than forcing the entire page to remain wide.</p>
<p>Responsive design helps here, but "responsive" and "accessible" aren't synonyms.</p>
<p>A responsive page can still hide controls, clip text, overlap content, or remove functionality at high zoom. Test the behaviour rather than assuming a media query solves the accessibility requirement.</p>
<h2 id="heading-how-to-make-an-interface-work-with-a-keyboard">How to Make an Interface Work with a Keyboard</h2>
<p>One of the simplest manual accessibility tests is to put the mouse aside and use the application with a keyboard.</p>
<p><a href="https://www.w3.org/TR/WCAG22/#keyboard">Success Criterion 2.1.1 Keyboard</a> requires functionality to be operable through a keyboard interface except where the underlying function genuinely depends on the path of the user's movement. WCAG doesn't prevent the interface from also supporting mouse, touch, voice, or other forms of input.</p>
<p>Let's return to our custom control:</p>
<pre><code class="language-html">&lt;div onclick="saveSettings()"&gt;Save&lt;/div&gt;
</code></pre>
<p>Making the <code>div</code> look like a button doesn't give it button behaviour.</p>
<p>You could begin rebuilding that behaviour yourself:</p>
<pre><code class="language-html">&lt;div
  role="button"
  tabindex="0"
&gt;
  Save
&lt;/div&gt;
</code></pre>
<p>But now your JavaScript must also provide the appropriate keyboard interaction.</p>
<p>In most cases, this is unnecessary:</p>
<pre><code class="language-html">&lt;button type="button"&gt;
  Save
&lt;/button&gt;
</code></pre>
<p>Native controls reduce the amount of interaction behaviour you need to reproduce.</p>
<p>W3C's ARIA Authoring Practices Guide makes this distinction explicit: ARIA roles don't cause browsers to add the keyboard behaviour that comes with native HTML controls. If you create a custom ARIA widget, you're responsible for implementing those interactions.</p>
<h3 id="heading-how-to-check-for-keyboard-traps">How to Check for Keyboard Traps</h3>
<p><a href="https://www.w3.org/TR/WCAG22/#no-keyboard-trap">Success Criterion 2.1.2 No Keyboard Trap</a> addresses situations where keyboard focus enters a component but can't leave through a keyboard interface.</p>
<p>This is particularly relevant to custom editors, dialogs, embedded widgets, and other complex controls.</p>
<p>Keyboard testing should go beyond asking whether you can press <code>Tab</code> until an element receives focus.</p>
<p>Try to complete the actual task. If you open a dialog, can you use its controls and close it? If you enter a custom widget, can you leave it? If a menu opens, can you operate it using its expected keyboard pattern?</p>
<p>Keyboard accessibility concerns the whole interaction, not simply whether an element appears in the tab order.</p>
<h2 id="heading-how-to-keep-keyboard-focus-visible">How to Keep Keyboard Focus Visible</h2>
<p>Keyboard navigation becomes difficult when users can't tell which element currently has focus.</p>
<p>This CSS is therefore risky:</p>
<pre><code class="language-css">*:focus {
  outline: none;
}
</code></pre>
<p>It removes the browser's default focus indication without providing an alternative.</p>
<p><a href="https://www.w3.org/TR/WCAG22/#focus-order">Success Criterion 2.4.3 Focus Order</a> requires a keyboard-operable interface to provide a mode in which the keyboard focus indicator is visible.</p>
<p>If the default outline doesn't fit your design, replace it with another visible focus treatment rather than simply removing it:</p>
<pre><code class="language-css">button:focus-visible,
a:focus-visible,
input:focus-visible,
select:focus-visible,
textarea:focus-visible {
  outline: 3px solid currentColor;
  outline-offset: 3px;
}
</code></pre>
<p>This is an example, not a guarantee of conformance. Your chosen indicator still needs to remain visible against the colours surrounding the component.</p>
<h3 id="heading-how-wcag-22-deals-with-obscured-focus">How WCAG 2.2 Deals with Obscured Focus</h3>
<p>WCAG 2.2 added <a href="https://www.w3.org/TR/WCAG22/#focus-not-obscured-minimum">Success Criterion 2.4.11 Focus Not Obscured (Minimum)</a> at Level AA.</p>
<p>When a user interface component receives keyboard focus, author-created content must not completely hide it. The Level AA criterion requires at least part of the focused component to remain visible.</p>
<p>A sticky header illustrates the problem:</p>
<pre><code class="language-css">.site-header {
  position: sticky;
  top: 0;
  height: 5rem;
}
</code></pre>
<p>There's nothing inherently inaccessible about a sticky header. The problem occurs if the page scrolls a focused link or control entirely behind that header.</p>
<p>CSS such as this can help when scroll positioning is involved:</p>
<pre><code class="language-css">html {
  scroll-padding-top: 6rem;
}
</code></pre>
<p>But don't treat it as a complete fix.</p>
<p>Test the real keyboard interaction because cookie notices, fixed bottom navigation, chat windows, sticky toolbars, and other overlays can create similar problems.</p>
<p>The important requirement is the outcome: when focus moves, the user should still be able to see the focused component.</p>
<h2 id="heading-how-to-design-pointer-targets-and-dragging-interactions">How to Design Pointer Targets and Dragging Interactions</h2>
<p>Keyboard support doesn't cover every interaction barrier.</p>
<p>WCAG 2.2 introduced additional requirements that are particularly relevant to touchscreens, drag-and-drop interfaces, and compact controls.</p>
<h3 id="heading-how-to-provide-an-alternative-to-dragging">How to Provide an Alternative to Dragging</h3>
<p>Imagine a task board where users reorder cards only by dragging them.</p>
<p>Dragging may work well for many users, but it depends on pressing a pointer, moving it while maintaining that interaction, and releasing it in the correct place.</p>
<p><a href="https://www.w3.org/TR/WCAG22/#dragging-movements">Success Criterion 2.5.7 Dragging Movements</a> requires functionality that uses a dragging movement to also be achievable without dragging through a single-pointer operation, unless dragging is essential to the function.</p>
<p>You can keep drag-and-drop while providing another control:</p>
<pre><code class="language-html">&lt;article class="task"&gt;
  &lt;h3&gt;Prepare monthly report&lt;/h3&gt;

  &lt;button type="button"&gt;
    Move up
  &lt;/button&gt;

  &lt;button type="button"&gt;
    Move down
  &lt;/button&gt;
&lt;/article&gt;
</code></pre>
<p>The exact reorder logic depends on your application.</p>
<p>The important part is that the user has another pointer-based way to perform the same function without having to drag the card.</p>
<p>WCAG isn't saying "don't use drag-and-drop". It's saying that dragging shouldn't unnecessarily become the only route to the functionality.</p>
<h3 id="heading-how-to-think-about-target-size">How to Think About Target Size</h3>
<p><a href="https://www.w3.org/TR/WCAG22/#target-size-minimum">Success Criterion 2.5.8 Target Size (Minimum)</a> is another WCAG 2.2 Level AA addition.</p>
<p>The criterion uses a minimum target size of <strong>24 by 24 CSS pixels</strong> or a defined spacing alternative and contains several exceptions. It therefore should not be simplified to "every clickable element must always be at least 24 pixels wide and high".</p>
<p>For an isolated icon button, you can choose to provide an even larger target:</p>
<pre><code class="language-css">.icon-button {
  min-width: 2.75rem;
  min-height: 2.75rem;

  display: inline-grid;
  place-items: center;
}
</code></pre>
<p>At a typical root font size, this deliberately creates a target larger than the WCAG minimum.</p>
<p>The visible icon can remain smaller:</p>
<pre><code class="language-html">&lt;button
  class="icon-button"
  type="button"
  aria-label="Delete invoice"
&gt;
  &lt;svg
    width="16"
    height="16"
    aria-hidden="true"
    viewBox="0 0 16 16"
  &gt;
    &lt;path d="M3 4h10M6 4V2h4v2M5 6v7M8 6v7M11 6v7"&gt;&lt;/path&gt;
  &lt;/svg&gt;
&lt;/button&gt;
</code></pre>
<p>The size of the icon and the size of the interactive target are not the same thing.</p>
<p>That distinction is useful when designing dense interfaces.</p>
<h3 id="heading-how-to-keep-the-accessible-name-aligned-with-the-visible-label">How to Keep the Accessible Name Aligned with the Visible Label</h3>
<p><a href="https://www.w3.org/TR/WCAG22/#label-in-name">Success Criterion 2.5.3 Label in Name</a> concerns controls that have a visible text label.</p>
<p>The accessible name should contain the visible label text. This is particularly important for users who operate interfaces using speech and refer to controls by the words they can see.</p>
<p>Avoid this:</p>
<pre><code class="language-html">&lt;button aria-label="Find products"&gt;
  Search
&lt;/button&gt;
</code></pre>
<p>The visible label is <code>Search</code>, but the accessible name is <code>Find products</code>.</p>
<p>In this case, the simplest version is better:</p>
<pre><code class="language-html">&lt;button&gt;
  Search
&lt;/button&gt;
</code></pre>
<p>If additional accessible context is genuinely necessary, retain the visible wording:</p>
<pre><code class="language-html">&lt;button aria-label="Search products"&gt;
  Search
&lt;/button&gt;
</code></pre>
<p>Before adding an <code>aria-label</code>, check whether the visible text already gives the control an adequate accessible name.</p>
<h2 id="heading-how-to-build-more-accessible-forms">How to Build More Accessible Forms</h2>
<p>Forms combine several areas of accessibility: structure, instructions, errors, input purpose, and status changes.</p>
<p>Start with the field itself.</p>
<h3 id="heading-how-to-label-form-controls">How to Label Form Controls</h3>
<p>This pattern is common:</p>
<pre><code class="language-html">&lt;input
  type="email"
  name="email"
  placeholder="Email address"
&gt;
</code></pre>
<p>The placeholder provides a visual hint, but it's not a good replacement for a proper label.</p>
<p>Use:</p>
<pre><code class="language-html">&lt;label for="email"&gt;
  Email address
&lt;/label&gt;

&lt;input
  id="email"
  name="email"
  type="email"
&gt;
</code></pre>
<p><a href="https://www.w3.org/TR/WCAG22/#labels-or-instructions">Success Criterion 3.3.2 Labels or Instructions</a> requires labels or instructions when content requires user input.</p>
<p>The <code>for</code> and <code>id</code> values also create a programmatic relationship between the label and the field.</p>
<h3 id="heading-how-to-identify-common-input-purposes">How to Identify Common Input Purposes</h3>
<p><a href="https://www.w3.org/TR/WCAG22/#identify-input-purpose">Success Criterion 1.3.5 Identify Input Purpose</a> applies to fields collecting certain types of information about the user. Their purpose needs to be programmatically determinable when the technology supports it.</p>
<p>HTML's <code>autocomplete</code> tokens help communicate common purposes:</p>
<pre><code class="language-html">&lt;label for="full-name"&gt;
  Full name
&lt;/label&gt;

&lt;input
  id="full-name"
  name="full-name"
  type="text"
  autocomplete="name"
&gt;

&lt;label for="email"&gt;
  Email address
&lt;/label&gt;

&lt;input
  id="email"
  name="email"
  type="email"
  autocomplete="email"
&gt;
</code></pre>
<p>This also allows browsers and other tools to provide useful input assistance.</p>
<h3 id="heading-how-to-write-useful-validation-errors">How to Write Useful Validation Errors</h3>
<p>Now consider an error message:</p>
<pre><code class="language-text">Invalid input.
</code></pre>
<p>The message tells the user almost nothing: Which input is invalid? What's wrong with it? What needs to change?</p>
<p><a href="https://www.w3.org/TR/WCAG22/#error-identification">Success Criterion <strong>3.3.1 Error Identification</strong></a> requires an automatically detected input error to identify the item in error and describe the error in text.</p>
<p>An implementation might look like this:</p>
<pre><code class="language-html">&lt;label for="email"&gt;
  Email address
&lt;/label&gt;

&lt;input
  id="email"
  name="email"
  type="email"
  aria-invalid="true"
  aria-describedby="email-error"
&gt;

&lt;p id="email-error"&gt;
  Enter an email address in the format name@example.com.
&lt;/p&gt;
</code></pre>
<p><code>aria-invalid="true"</code> exposes the invalid state. <code>aria-describedby</code> associates the explanation with the field.</p>
<p>More importantly, the message tells the user what needs correcting.</p>
<p><a href="https://www.w3.org/TR/WCAG22/#error-suggestion">Success Criterion 3.3.3 Error Suggestion</a> goes further at Level AA. When the system detects an input error and knows how it can be corrected, it should provide an appropriate suggestion unless doing so would compromise the security or purpose of the content.</p>
<p>The aim isn't to make every error message long. The aim is to make it actionable.</p>
<h2 id="heading-how-to-avoid-redundant-entry">How to Avoid Redundant Entry</h2>
<p>Consider a checkout process.</p>
<p>The user enters a delivery address on one step. The next step asks them to type exactly the same address again for billing.</p>
<p>WCAG 2.2 introduced <a href="https://www.w3.org/TR/WCAG22/#redundant-entry">Success Criterion 3.3.7 Redundant Entry</a> at Level A.</p>
<p>When information previously entered by or provided to the user is required again during the same process, the information must generally be auto-populated or available for the user to select. The criterion includes exceptions where re-entry is essential, necessary for security, or where the previous information is no longer valid.</p>
<p>A checkout might offer:</p>
<pre><code class="language-html">&lt;label&gt;
  &lt;input
    type="checkbox"
    name="billing-same-as-delivery"
  &gt;
  Use my delivery address as my billing address
&lt;/label&gt;
</code></pre>
<p>Notice that the requirement concerns information within the same process. It doesn't mean every website has to remember every value a user entered during earlier visits.</p>
<p>This criterion also shows why accessibility extends beyond screen-reader support.</p>
<p>Reducing unnecessary repetition can lower the cognitive and interaction effort required to complete a task.</p>
<h2 id="heading-how-to-keep-help-consistent">How to Keep Help Consistent</h2>
<p>WCAG 2.2 also added <a href="https://www.w3.org/TR/WCAG22/#consistent-help">Success Criterion 3.2.6 Consistent Help</a> at Level A.</p>
<p>If certain help mechanisms appear repeatedly across a set of pages, they need to appear in the same relative order unless the user initiates a change. These mechanisms can include human contact details, contact mechanisms, self-help options, and automated contact mechanisms.</p>
<p>Suppose your account pages all provide a support link in the header:</p>
<pre><code class="language-html">&lt;header&gt;
  &lt;a href="/"&gt;Acme&lt;/a&gt;

  &lt;nav aria-label="Primary"&gt;
    &lt;!-- Navigation links --&gt;
  &lt;/nav&gt;

  &lt;a href="/support"&gt;Support&lt;/a&gt;
&lt;/header&gt;
</code></pre>
<p>Do not move that support mechanism unpredictably between otherwise related pages.</p>
<p>A key nuance is that WCAG 2.2 does <strong>not</strong> require every website to introduce one of these help mechanisms.</p>
<p>The criterion applies when qualifying help is already available and repeated across multiple pages in the same set.</p>
<p>The development implication is therefore mostly about consistency.</p>
<p>If users learn where help appears on one page, avoid making them search for it again on the next.</p>
<h2 id="heading-how-wcag-22-affects-authentication">How WCAG 2.2 Affects Authentication</h2>
<p>Authentication is another area that changed in WCAG 2.2.</p>
<p>Consider a login form that deliberately blocks paste:</p>
<pre><code class="language-javascript">passwordInput.addEventListener("paste", (event) =&gt; {
  event.preventDefault();
});
</code></pre>
<p>That may appear to encourage users to type a password manually, but it can also interfere with mechanisms that reduce the need to remember or transcribe credentials.</p>
<p><a href="https://www.w3.org/TR/WCAG22/#accessible-authentication-minimum">Success Criterion 3.3.8 Accessible Authentication (Minimum)</a> addresses authentication steps that require cognitive function tests.</p>
<p>The Level AA requirement allows such tests when an accepted alternative or assistance mechanism is available. W3C specifically identifies password-manager support and copy-and-paste as mechanisms that can reduce the cognitive burden involved in authentication.</p>
<p>A conventional login form can allow these tools to work:</p>
<pre><code class="language-html">&lt;label for="username"&gt;
  Email address
&lt;/label&gt;

&lt;input
  id="username"
  name="username"
  type="email"
  autocomplete="username"
&gt;

&lt;label for="password"&gt;
  Password
&lt;/label&gt;

&lt;input
  id="password"
  name="password"
  type="password"
  autocomplete="current-password"
&gt;
</code></pre>
<p>It would be inaccurate to simplify this criterion to "WCAG 2.2 prohibits passwords". It does not.</p>
<p>A password is a cognitive function test, but the criterion permits it when the user has a mechanism that assists with completing that test, such as a password manager that can fill the field.</p>
<p>The same reasoning becomes relevant to multi-factor authentication.</p>
<p>If a process requires a user to read a code on one device and manually transcribe it to another, consider whether the authentication flow offers a path that avoids that cognitive burden. W3C's guidance explicitly discusses authentication processes with several steps and the need for an accessible path through them.</p>
<p>This is a good example of why the exact criterion matters more than a simplified accessibility checklist.</p>
<h2 id="heading-how-to-use-aria-without-replacing-html">How to Use ARIA Without Replacing HTML</h2>
<p>ARIA stands for <strong>Accessible Rich Internet Applications</strong>.</p>
<p>It provides roles, states, and properties that help web applications communicate information that may not otherwise be available to assistive technologies.</p>
<p>ARIA is useful. It's also easy to misuse.</p>
<p>Consider this example:</p>
<pre><code class="language-html">&lt;div role="button"&gt;
  Place order
&lt;/div&gt;
</code></pre>
<p>The <code>role</code> tells accessibility APIs that the element represents a button. It doesn't make the element behave like a button.</p>
<p>W3C <a href="https://www.w3.org/WAI/ARIA/apg/practices/read-me-first/">ARIA Authoring Practices Guide</a> describes an ARIA role as a promise. When you use <code>role="button"</code>, you take responsibility for providing the expected keyboard and interaction behaviour yourself. ARIA doesn't cause the browser to add that behaviour automatically.</p>
<p>Where a native HTML element already exists, prefer it:</p>
<pre><code class="language-html">&lt;button type="button"&gt;
  Place order
&lt;/button&gt;
</code></pre>
<h3 id="heading-how-aria-can-communicate-state">How ARIA Can Communicate State</h3>
<p>ARIA becomes useful when HTML alone doesn't communicate enough about a component's current state.</p>
<p>Consider a disclosure control:</p>
<pre><code class="language-html">&lt;button
  id="account-options-trigger"
  type="button"
  aria-expanded="false"
  aria-controls="account-options"
&gt;
  Account options
&lt;/button&gt;

&lt;div id="account-options" hidden&gt;
  &lt;a href="/profile"&gt;Profile&lt;/a&gt;
  &lt;a href="/security"&gt;Security&lt;/a&gt;
&lt;/div&gt;
</code></pre>
<p>You can keep <code>aria-expanded</code> in sync with the visible state:</p>
<pre><code class="language-javascript">const trigger = document.querySelector(
  "#account-options-trigger"
);

const panel = document.querySelector(
  "#account-options"
);

trigger.addEventListener("click", () =&gt; {
  const isExpanded =
    trigger.getAttribute("aria-expanded") === "true";

  trigger.setAttribute(
    "aria-expanded",
    String(!isExpanded)
  );

  panel.hidden = isExpanded;
});
</code></pre>
<p>The JavaScript does two related things.</p>
<p>It changes whether the panel is hidden, and it updates the accessibility state exposed by the trigger.</p>
<p>If the panel opens visually but <code>aria-expanded</code> remains <code>false</code>, the interface now communicates two conflicting states.</p>
<p>This illustrates a useful ARIA rule: <strong>ARIA state must describe the interface that actually exists.</strong></p>
<p>For more complex patterns such as dialogs, comboboxes, tabs, menus, and grids, the W3C <a href="https://www.w3.org/WAI/ARIA/apg/">ARIA Authoring Practices Guide</a> provides documented interaction patterns and examples. W3C also makes clear that APG is implementation guidance rather than a normative accessibility standard.</p>
<h2 id="heading-how-to-make-dynamic-status-messages-accessible">How to Make Dynamic Status Messages Accessible</h2>
<p>Modern interfaces frequently update without loading a new page.</p>
<p>A user might save a profile and see:</p>
<pre><code class="language-text">Your settings were saved.
</code></pre>
<p>Or run a search and see:</p>
<pre><code class="language-text">18 results found.
</code></pre>
<p>A sighted user can often notice these updates without moving away from the current control.</p>
<p>Assistive technology also needs a programmatic way to identify relevant status messages.</p>
<p><a href="https://www.w3.org/TR/WCAG22/#status-messages">Success Criterion 4.1.3 Status Messages</a> requires qualifying status messages to be programmatically determinable so assistive technologies can present them without requiring the message itself to receive focus.</p>
<p>For a routine save confirmation, you can use <code>role="status"</code>:</p>
<pre><code class="language-html">&lt;button id="save-settings" type="button"&gt;
  Save settings
&lt;/button&gt;

&lt;p id="save-status" role="status"&gt;&lt;/p&gt;
</code></pre>
<p>Then update its content:</p>
<pre><code class="language-javascript">const saveButton = document.querySelector(
  "#save-settings"
);

const saveStatus = document.querySelector(
  "#save-status"
);

saveButton.addEventListener("click", () =&gt; {
  saveStatus.textContent =
    "Your settings were saved.";
});
</code></pre>
<p>The browser can expose that status change to supporting assistive technologies without moving keyboard focus away from the Save button.</p>
<p>Not every dynamic DOM change is a status message.</p>
<p>WCAG defines the term more narrowly. It includes information about the result or success of an action, an application's waiting state, the progress of a process, or the existence of errors when that update doesn't itself constitute a change of context.</p>
<p>Don't make every changing piece of content a live announcement. An excessively chatty interface can create a different usability problem.</p>
<p>Use status semantics for information users need to receive while continuing their current task.</p>
<h2 id="heading-how-to-test-your-website-for-accessibility">How to Test Your Website for Accessibility</h2>
<p>Accessibility testing works best as a combination of methods.</p>
<p>WCAG itself is designed to support testing through both automated tools and human evaluation. An automated scanner can identify many technical problems, but it can't reliably judge every accessibility requirement or determine whether an entire user journey makes sense.</p>
<h3 id="heading-how-to-start-with-automated-testing">How to Start with Automated Testing</h3>
<p>Automated tools are useful for repeatable technical checks.</p>
<p>They can identify many problems involving accessible names, some contrast failures, invalid ARIA usage, form relationships, and other machine-detectable conditions.</p>
<p>The limitation appears when correctness depends on meaning.</p>
<p>A tool can tell you that an image has an <code>alt</code> attribute. It can't always determine whether the text accurately communicates the purpose of the image.</p>
<p>Automation should therefore start the evaluation, not end it.</p>
<h3 id="heading-how-to-perform-keyboard-testing">How to Perform Keyboard Testing</h3>
<p>Open the page, put the mouse aside, and try to complete an actual task using only your keyboard.</p>
<p>Start with <code>Tab</code> to move forwards through interactive elements and <code>Shift + Tab</code> to move backwards.</p>
<p>Use <code>Enter</code> and <code>Space</code> to activate controls where appropriate. Custom widgets may also use arrow keys or <code>Escape</code> depending on their interaction pattern. The <a href="https://www.w3.org/WAI/ARIA/apg/practices/keyboard-interface/">ARIA Authoring Practices keyboard guidance</a> documents expected behaviour for common widget patterns.</p>
<p>Don't simply press <code>Tab</code> a few times and stop.</p>
<p>For example, if you're testing a checkout flow:</p>
<ol>
<li><p>Navigate to the basket.</p>
</li>
<li><p>Change a quantity.</p>
</li>
<li><p>Continue to checkout.</p>
</li>
<li><p>Move through the form.</p>
</li>
<li><p>Submit it.</p>
</li>
<li><p>Correct an error.</p>
</li>
<li><p>Complete the process.</p>
</li>
</ol>
<p>As you do this, check whether you can reach and operate every required control, move away from every component, follow a sensible focus sequence, see where focus currently is, and avoid having focused content hidden by an overlay.</p>
<p>If something goes wrong, the relevant requirements include <a href="https://www.w3.org/TR/WCAG22/#keyboard">Success Criterion 2.1.1 Keyboard</a>, <a href="https://www.w3.org/TR/WCAG22/#no-keyboard-trap">Success Criterion 2.1.2 No Keyboard Trap</a>, <a href="https://www.w3.org/TR/WCAG22/#focus-order">Success Criterion 2.4.3 Focus Order</a>, <a href="https://www.w3.org/TR/WCAG22/#focus-visible">Success Criterion 2.4.7 Focus Visible</a>, and <a href="https://www.w3.org/TR/WCAG22/#focus-not-obscured-minimum">Success Criterion 2.4.11 Focus Not Obscured (Minimum)</a>.</p>
<h3 id="heading-how-to-test-zoom-resizing-and-reflow">How to Test Zoom, Resizing, and Reflow</h3>
<p>You can perform a basic zoom test directly in your browser.</p>
<p>In most browsers:</p>
<ul>
<li><p>use <code>Ctrl + +</code> on Windows or Linux</p>
</li>
<li><p>use <code>Cmd + +</code> on macOS</p>
</li>
<li><p>use <code>Ctrl/Cmd + 0</code> to return to the default zoom</p>
</li>
</ul>
<p>W3C's <a href="https://www.w3.org/WAI/test-evaluate/easy-checks/zoom/">Zoom Easy Check</a> suggests testing at 200%.</p>
<p>As you increase zoom, work through the page and look for:</p>
<ul>
<li><p>clipped text</p>
</li>
<li><p>overlapping elements</p>
</li>
<li><p>controls that disappear</p>
</li>
<li><p>navigation that stops working</p>
</li>
<li><p>content hidden behind other content</p>
</li>
<li><p>horizontal scrolling across ordinary page content</p>
</li>
</ul>
<p>Also use a narrow browser window or responsive browser tools to inspect how the content reflows.</p>
<p>This is to verify the behaviour discussed under <a href="https://www.w3.org/TR/WCAG22/#resize-text">Success Criterion 1.4.4 Resize Text</a> and <a href="https://www.w3.org/TR/WCAG22/#reflow">Success Criterion 1.4.10 Reflow</a>.</p>
<h3 id="heading-how-to-test-colour-and-contrast">How to Test Colour and Contrast</h3>
<p>Use a contrast checker or the colour information available in your browser developer tools to measure foreground and background combinations.</p>
<p>Do this for ordinary text as well as important non-text elements such as custom control borders, icons, and state indicators.</p>
<p>Then test colour-dependent information separately.</p>
<p>For example, if an error field turns red, temporarily ignore the colour change and ask whether another visible indication still communicates the error.</p>
<p>These checks correspond to <a href="https://www.w3.org/TR/WCAG22/#use-of-color">Success Criterion 1.4.1 Use of Color</a>, <a href="https://www.w3.org/TR/WCAG22/#contrast-minimum">Success Criterion 1.4.3 Contrast (Minimum)</a>, and <a href="https://www.w3.org/TR/WCAG22/#non-text-contrast">Success Criterion 1.4.11 Non-text Contrast</a>.</p>
<h3 id="heading-how-to-test-forms-manually">How to Test Forms Manually</h3>
<p>Don't test a form only with valid information. You should deliberately make mistakes to test as many cases as possible.</p>
<p>Leave a required field empty. Enter an incorrectly formatted email address. Submit a value the form should reject.</p>
<p>Then check whether you can:</p>
<ul>
<li><p>identify the field that has a problem</p>
</li>
<li><p>understand the error message</p>
</li>
<li><p>determine how to correct it</p>
</li>
<li><p>reach the error using the keyboard</p>
</li>
<li><p>correct the information and continue</p>
</li>
</ul>
<p>Also inspect form controls in your browser developer tools to confirm that visible labels and descriptions are associated with the correct fields.</p>
<p>These tests help you verify <a href="https://www.w3.org/TR/WCAG22/#error-identification">Success Criterion 3.3.1 Error Identification</a>, <a href="https://www.w3.org/TR/WCAG22/#labels-or-instructions">Success Criterion 3.3.2 Labels or Instructions</a>, and <a href="https://www.w3.org/TR/WCAG22/#error-suggestion">Success Criterion 3.3.3 Error Suggestion</a>.</p>
<h3 id="heading-how-to-test-pointer-and-dragging-interactions">How to Test Pointer and Dragging Interactions</h3>
<p>If your interface contains drag-and-drop, complete the action normally first. Then try to perform the same function without dragging.</p>
<p>For example, if you can drag a task into a new position, check whether another pointer-operated control lets you move it as well.</p>
<p>That gives you a practical test for <a href="https://www.w3.org/TR/WCAG22/#dragging-movements">Success Criterion 2.5.7 Dragging Movements</a>.</p>
<p>For small controls, use browser developer tools to inspect the rendered interactive area rather than judging only the visible icon.</p>
<p>Pay particular attention to close buttons, carousel controls, pagination items, icon buttons, and densely packed toolbars when checking <a href="https://www.w3.org/TR/WCAG22/#target-size-minimum">Success Criterion 2.5.8 Target Size (Minimum)</a>.</p>
<h3 id="heading-how-to-inspect-the-accessibility-tree">How to Inspect the Accessibility Tree</h3>
<p>Modern browser developer tools expose accessibility information associated with elements.</p>
<p>Inspect important controls and compare what the accessibility tree reports with what the interface shows.</p>
<p>A button might visually say <code>Search</code> while its accessible name says something completely different. A disclosure may look open while its <code>aria-expanded</code> state remains <code>false</code>.</p>
<p>Inspecting the accessibility tree helps expose these mismatches.</p>
<h3 id="heading-how-to-test-with-assistive-technology">How to Test with Assistive Technology</h3>
<p>When testing with a screen reader, focus on complete tasks.</p>
<p>For a form, navigate to the fields, identify their labels, enter incorrect information, submit it, locate and understand the errors, correct them, and confirm the successful state.</p>
<p>The question isn't simply:</p>
<blockquote>
<p><strong>Can the screen reader read this page?</strong></p>
</blockquote>
<p>The more useful question is:</p>
<blockquote>
<p><strong>Can the user complete the task and understand what happened?</strong></p>
</blockquote>
<p>Testing with disabled users can reveal additional usability barriers that automated and standards-based evaluation may not expose.</p>
<h2 id="heading-conclusion">Conclusion</h2>
<p>WCAG becomes easier to understand when you connect its requirements to normal development decisions. The important shift is to stop treating accessibility as a final audit. Build it into the interface while you build everything else.</p>
 ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ How Attribute-Based Access Control Helps You Write Better Authorization Rules ]]>
                </title>
                <description>
                    <![CDATA[ Every application that handles user data eventually hits the same problem: not all users should see the same things. A junior nurse should not be able to access every patient record in the hospital. A ]]>
                </description>
                <link>https://www.freecodecamp.org/news/how-attribute-based-access-control-helps-you-write-better-authorization-rules/</link>
                <guid isPermaLink="false">6a21b44e09761aac249579f9</guid>
                
                    <category>
                        <![CDATA[ Security ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Web Security ]]>
                    </category>
                
                    <category>
                        <![CDATA[ authorization ]]>
                    </category>
                
                    <category>
                        <![CDATA[ access control ]]>
                    </category>
                
                    <category>
                        <![CDATA[ JavaScript ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ Aiyedogbon Abraham ]]>
                </dc:creator>
                <pubDate>Thu, 04 Jun 2026 17:22:22 +0000</pubDate>
                <media:content url="https://cdn.hashnode.com/uploads/covers/5e1e335a7a1d3fcc59028c64/1bcd9989-cf38-4375-a0ed-03cf1bd3c3b8.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>Every application that handles user data eventually hits the same problem: not all users should see the same things.</p>
<p>A junior nurse should not be able to access every patient record in the hospital. A contractor should not be able to read internal financial reports. An employee logged in from an unrecognized device at 2AM probably should not be editing production configuration files.</p>
<p>Simple role-based systems handle obvious cases well. But as applications grow and access rules become more nuanced, those systems start to crack. You end up creating more and more specific roles, like <code>finance_viewer</code>, <code>finance_viewer_us_only</code>, <code>finance_viewer_us_only_readonly</code>, until the roles themselves become unmanageable.</p>
<p>Attribute-Based Access Control (ABAC) was designed to solve exactly this problem. It shifts from "what role does this user have?" to "what do we know about this user, this resource, and this situation?" and makes access decisions based on all of those factors together.</p>
<p>In this guide, you'll learn how ABAC works, how it evolved from earlier access control models, how policies are structured, how to implement it in code, and when to use it.</p>
<h2 id="heading-table-of-contents">Table of Contents</h2>
<ul>
<li><p><a href="#heading-prerequisites">Prerequisites</a></p>
</li>
<li><p><a href="#heading-how-access-control-has-evolved">How Access Control Has Evolved</a></p>
</li>
<li><p><a href="#heading-what-is-attribute-based-access-control">What is Attribute-Based Access Control?</a></p>
</li>
<li><p><a href="#heading-the-four-building-blocks-of-abac">The Four Building Blocks of ABAC</a></p>
</li>
<li><p><a href="#heading-how-an-abac-decision-is-made">How an ABAC Decision is Made</a></p>
</li>
<li><p><a href="#heading-how-to-write-abac-policies">How to Write ABAC Policies</a></p>
</li>
<li><p><a href="#heading-how-to-implement-abac-in-code">How to Implement ABAC in Code</a></p>
</li>
<li><p><a href="#heading-abac-vs-rbac-when-to-use-which">ABAC vs RBAC: When to Use Which</a></p>
</li>
<li><p><a href="#heading-real-world-use-cases">Real-World Use Cases</a></p>
</li>
<li><p><a href="#heading-enterprise-abac-considerations">Enterprise ABAC Considerations</a></p>
</li>
<li><p><a href="#heading-limitations-and-challenges">Limitations and Challenges</a></p>
</li>
<li><p><a href="#heading-conclusion">Conclusion</a></p>
</li>
<li><p><a href="#heading-glossary">Glossary</a></p>
</li>
</ul>
<h2 id="heading-prerequisites">Prerequisites</h2>
<p>To get the most from this article, you should have:</p>
<ul>
<li><p>A basic understanding of web authentication (logins, sessions, tokens)</p>
</li>
<li><p>Familiarity with how users and resources relate in applications</p>
</li>
<li><p>Some experience reading JavaScript or pseudocode</p>
</li>
</ul>
<p>No prior knowledge of access control theory is required.</p>
<h2 id="heading-how-access-control-has-evolved">How Access Control Has Evolved</h2>
<p>To understand why ABAC exists, it helps to understand what came before it and why each generation fell short.</p>
<h3 id="heading-discretionary-and-mandatory-access-control">Discretionary and Mandatory Access Control</h3>
<p>Early access control models emerged from Department of Defense applications in the 1960s and 1970s. According to NIST Special Publication 800-162, these were Discretionary Access Control (DAC) and Mandatory Access Control (MAC).</p>
<p>In DAC, the owner of a resource decides who can access it. Think of a file on your computer where you choose who can read or edit it. In MAC, access is governed by a central authority using labels like "Classified" or "Top Secret." The system enforces these labels, not individual owners.</p>
<p>Both worked for their original purposes but didn't scale well to the complexity of modern networked systems.</p>
<h3 id="heading-identity-based-access-control-and-access-control-lists">Identity-Based Access Control and Access Control Lists</h3>
<p>As networks grew, identity-based access control (IBAC) became common. The most familiar implementation is the Access Control List (ACL), a list of users or groups attached to a resource, specifying what each can do.</p>
<p>ACLs are simple and transparent, but they create a management burden as systems grow. Every new user needs to be added to every relevant list. Every permission change means hunting through lists across multiple resources. And when someone leaves the organization, you need to find and remove them everywhere.</p>
<p>Failure to do this consistently leads to users accumulating privileges they should no longer have.</p>
<h3 id="heading-role-based-access-control">Role-Based Access Control</h3>
<p>Role-Based Access Control (RBAC) was a major step forward. Instead of assigning permissions directly to users, RBAC assigns them to roles. Users are then assigned roles. A hospital might have roles like <code>nurse</code>, <code>doctor</code>, <code>admin</code>, and <code>billing_staff</code>, each with different permissions.</p>
<p>This made administration much more manageable. Adding a new employee means assigning them appropriate roles. Removing an employee means removing their roles. Changing what nurses can do means updating the nurse role once.</p>
<p>RBAC became widely adopted and is still the right choice for many applications. But it has a structural weakness: as permission requirements become more granular, you have to create more specific roles. A nurse who can only see patients on their floor, only during their shift, or only for certain record types, needs a very specific role, or a combination of roles that interacts in complicated ways.</p>
<p>This proliferation is called "role explosion." The roles multiply until they are as difficult to manage as the individual permissions RBAC was supposed to replace.</p>
<h3 id="heading-attribute-based-access-control">Attribute-Based Access Control</h3>
<p>ABAC emerged as a response to role explosion. Instead of assigning roles that bundle fixed permissions, ABAC evaluates the actual characteristics of the user, the resource, and the context at the moment of every access request.</p>
<p>A nurse gets access to a patient record not because they have the <code>nurse</code> role, but because their job title is "Nurse Practitioner," the patient is on their assigned floor, it's currently their shift, and the record type is within their scope of care. Change any of those facts, and the access decision changes accordingly.</p>
<p>As NIST SP 800-162 defines it, ABAC is:</p>
<blockquote>
<p>"an access control method where subject requests to perform operations on objects are granted or denied based on assigned attributes of the subject, assigned attributes of the object, environment conditions, and a set of policies that are specified in terms of those attributes and conditions."</p>
</blockquote>
<h2 id="heading-what-is-attribute-based-access-control">What is Attribute-Based Access Control?</h2>
<p>ABAC is a logical access control model where every access decision is made by evaluating a set of rules against the current values of attributes. Nothing is pre-computed or cached in role assignments. Every time a user tries to do something, the system asks: given what we know about this user, this resource, and this moment, should this be allowed?</p>
<p>This makes ABAC highly precise and highly dynamic. Permissions don't accumulate over time. They don't need manual cleanup when someone's role changes. The system simply evaluates the current state of attributes every time.</p>
<p>The model is formally described in NIST's guide to ABAC as being capable of enforcing both Discretionary Access Control and Mandatory Access Control concepts, making it more expressive than models that only support one or the other.</p>
<p>Companies like Axiomatics, major government agencies, and large enterprises managing cross-organizational data sharing all rely on ABAC for its ability to scale security policies across complex environments.</p>
<h2 id="heading-the-four-building-blocks-of-abac">The Four Building Blocks of ABAC</h2>
<p>Every ABAC system is built from four types of information. Understanding these clearly is the key to understanding how ABAC works.</p>
<h3 id="heading-1-subject-attributes">1. Subject Attributes</h3>
<p>The subject is whoever or whatever is requesting access. This is usually a user, but it can also be a service, an application, or an automated system, what NIST calls a Non-Person Entity (NPE).</p>
<p>Subject attributes describe who the subject is:</p>
<pre><code class="language-plaintext">user.jobTitle         = "Nurse Practitioner"
user.department       = "Cardiology"
user.clearanceLevel   = "Confidential"
user.employmentStatus = "Active"
user.location         = "Floor 3"
user.shiftActive      = true
</code></pre>
<p>These attributes are typically sourced from an identity provider, HR system, or user directory. They're facts about the user that can be used in policies.</p>
<h3 id="heading-2-object-attributes-resource-attributes">2. Object Attributes (Resource Attributes)</h3>
<p>The object is whatever the subject is trying to access. This could be a file, a database record, an API endpoint, a service, or any other protected resource.</p>
<p>Object attributes describe what the resource is:</p>
<pre><code class="language-plaintext">record.type           = "PatientMedical"
record.floor          = "Floor 3"
record.sensitivity    = "High"
record.owner          = "Dr. Williams"
record.department     = "Cardiology"
</code></pre>
<p>Object attributes are typically assigned when a resource is created and updated throughout its lifecycle. They're facts about the resource that determine who should be able to access it.</p>
<h3 id="heading-3-action-attributes">3. Action Attributes</h3>
<p>The action is what the subject is trying to do to the object. Common actions include read, write, edit, delete, copy, execute, and share.</p>
<p>In many ABAC implementations, the action itself has attributes:</p>
<pre><code class="language-plaintext">action.type           = "read"
action.bulk           = false
</code></pre>
<p>Policies can restrict which actions are allowed independently of the other attributes. A user might be able to read a document but not delete it, even if all their other attributes match.</p>
<h3 id="heading-4-environment-conditions">4. Environment Conditions</h3>
<p>Environment conditions are contextual factors that don't belong to either the subject or the object, but that should influence the access decision. NIST describes these as "dynamic factors, independent of subject and object, that may be used as attributes at decision time to influence an access decision."</p>
<p>Examples include:</p>
<pre><code class="language-plaintext">environment.time           = "14:30"
environment.dayOfWeek      = "Wednesday"
environment.userLocation   = "Corporate Office"
environment.ipAddress      = "192.168.1.10"
environment.deviceStatus   = "compliant"
environment.threatLevel    = "low"
</code></pre>
<p>Environment conditions are what make ABAC truly dynamic. The same user, the same resource, and the same action might be allowed during business hours on a trusted device but denied at midnight from an unknown IP address.</p>
<h2 id="heading-how-an-abac-decision-is-made">How an ABAC Decision is Made</h2>
<p>When a subject tries to perform an action on an object, the ABAC system runs through a specific process:</p>
<h3 id="heading-step-1-collect-attributes">Step 1: Collect Attributes</h3>
<p>The system gathers current attributes for the subject, object, action, and environment. This might involve querying a user directory, reading resource metadata, and checking current time and location.</p>
<h3 id="heading-step-2-find-applicable-policies">Step 2: Find Applicable Policies</h3>
<p>The system identifies which policies apply to this particular request. A request to read a patient record might have several policies that apply: one about clinical staff access, one about after-hours access, and one about record sensitivity levels.</p>
<h3 id="heading-step-3-evaluate-each-policy">Step 3: Evaluate Each Policy</h3>
<p>Each applicable policy evaluates the collected attributes and returns permit or deny.</p>
<h3 id="heading-step-4-reconcile-conflicts">Step 4: Reconcile Conflicts</h3>
<p>If multiple policies apply and they conflict, the system uses predefined combining rules. Common approaches are "deny overrides" (if any policy says deny, the request is denied) or "permit overrides" (if any policy says permit, the request is permitted).</p>
<h3 id="heading-step-5-enforce-the-decision">Step 5: Enforce the Decision</h3>
<p>The system grants or denies access based on the final decision.</p>
<p>This process happens every time an access request is made. There's no caching of role assignments or pre-computed permission tables. The decision reflects the current state of all attributes at the moment of the request.</p>
<h2 id="heading-how-to-write-abac-policies">How to Write ABAC Policies</h2>
<p>Policies are the logic at the heart of ABAC. They're written as conditional rules that reference attributes. A well-written policy reads like a business rule, because that's exactly what it is.</p>
<h3 id="heading-simple-boolean-policy">Simple Boolean Policy</h3>
<p>The most basic form evaluates whether certain attributes match:</p>
<pre><code class="language-javascript">// Policy: Only active employees can access internal resources
function canAccessInternalResource(user) {
  return user.employmentStatus === "Active";
}
</code></pre>
<p><strong>What this does:</strong> Checks a single attribute, employment status, before allowing access. Any inactive, suspended, or terminated user is denied, regardless of their roles or past access history.</p>
<h3 id="heading-multi-attribute-policy">Multi-Attribute Policy</h3>
<p>Real policies typically combine multiple attributes:</p>
<pre><code class="language-javascript">// Policy: A nurse can read a patient record
// if the patient is on their assigned floor
// and during their active shift

function canReadPatientRecord(user, record, environment) {
  const isNurse = user.jobTitle === "Nurse Practitioner";
  const isAssignedFloor = user.assignedFloor === record.floor;
  const isActiveDuty = user.shiftActive === true;

  return isNurse &amp;&amp; isAssignedFloor &amp;&amp; isActiveDuty;
}
</code></pre>
<p><strong>What this does:</strong> Combines three conditions using AND logic. All three must be true for access to be granted. Change the nurse's floor assignment, and they immediately lose access to records on the previous floor, without any manual intervention.</p>
<h3 id="heading-environment-aware-policy">Environment-Aware Policy</h3>
<p>Adding environment conditions makes policies context-sensitive:</p>
<pre><code class="language-javascript">// Policy: Users can only access sensitive financial records
// during business hours from the corporate network

function canAccessSensitiveFinancialRecord(user, record, environment) {
  const isFinanceStaff = user.department === "Finance";
  const isHighSensitivity = record.sensitivity === "High";
  
  // If this is a high-sensitivity record, apply time and location controls
  if (isHighSensitivity) {
    const currentHour = new Date(environment.timestamp).getHours();
    const isBusinessHours = currentHour &gt;= 9 &amp;&amp; currentHour &lt; 17;
    const isCorporateNetwork = environment.ipRange === "corporate";

    return isFinanceStaff &amp;&amp; isBusinessHours &amp;&amp; isCorporateNetwork;
  }

  // Lower sensitivity records only require finance department membership
  return isFinanceStaff;
}
</code></pre>
<p><strong>What this does:</strong> Applies stricter controls to higher-sensitivity resources. The same user gets access to low-sensitivity records at any time, but high-sensitivity records require them to be on the corporate network during business hours. The policy logic mirrors the actual business rule: sensitive data needs more protection.</p>
<h3 id="heading-ownership-based-policy">Ownership-Based Policy</h3>
<p>ABAC can also implement discretionary ownership rules:</p>
<pre><code class="language-javascript">// Policy: A user can edit a document
// if they own it, or if they have editor permissions
// and the document isn't locked

function canEditDocument(user, document, action) {
  const isOwner = document.ownerId === user.id;
  const hasEditorPermission = user.permissions.includes("document.edit");
  const isUnlocked = document.status !== "locked";

  return (isOwner || hasEditorPermission) &amp;&amp; isUnlocked;
}
</code></pre>
<p><strong>What this does:</strong> Combines ownership (an attribute of the relationship between user and document) with explicit permissions and resource state. An editor can't edit a locked document even if they have the edit permission. An owner can edit their own documents but not locked ones.</p>
<h2 id="heading-how-to-implement-abac-in-code">How to Implement ABAC in Code</h2>
<p>Let's build a simple ABAC evaluation engine that puts these pieces together.</p>
<h3 id="heading-step-1-define-the-attribute-structure">Step 1: Define the Attribute Structure</h3>
<p>First, define clear data structures for your attributes:</p>
<pre><code class="language-javascript">// A user (subject) requesting access
const user = {
  id: "user-123",
  name: "Sarah Chen",
  department: "Cardiology",
  jobTitle: "Nurse Practitioner",
  clearanceLevel: 2,
  assignedFloor: "Floor 3",
  shiftActive: true,
  employmentStatus: "Active"
};

// A resource (object) being accessed
const patientRecord = {
  id: "record-456",
  type: "PatientMedical",
  floor: "Floor 3",
  sensitivity: 2,
  ownerId: "doctor-789",
  department: "Cardiology"
};

// Environment conditions
const environment = {
  timestamp: new Date().toISOString(),
  ipAddress: "10.0.1.25",
  ipRange: "corporate",
  deviceCompliant: true
};
</code></pre>
<h3 id="heading-step-2-write-policy-functions">Step 2: Write Policy Functions</h3>
<p>Write individual policies as pure functions that take attributes and return boolean values:</p>
<pre><code class="language-javascript">// policies/patientRecord.js

// Policy 1: User must be active and clinical staff
function isClinicalStaff(user) {
  const clinicalTitles = [
    "Nurse Practitioner",
    "Physician",
    "Resident",
    "Medical Assistant"
  ];
  return (
    user.employmentStatus === "Active" &amp;&amp;
    clinicalTitles.includes(user.jobTitle)
  );
}

// Policy 2: Record must be within the user's assigned area
function isAssignedToRecord(user, record) {
  return (
    user.department === record.department &amp;&amp;
    user.assignedFloor === record.floor
  );
}

// Policy 3: User must be on active shift
function isOnActiveShift(user) {
  return user.shiftActive === true;
}

// Policy 4: High-sensitivity records require compliant devices
function meetsDeviceRequirements(record, environment) {
  if (record.sensitivity &gt;= 3) {
    return environment.deviceCompliant === true;
  }
  return true; // No device requirement for lower sensitivity
}
</code></pre>
<p><strong>What this does:</strong> Each policy is a small, focused function. This makes policies easy to test individually, easy to read, and easy to reuse across different access decisions. A policy for "is this user clinical staff" can be applied to many different resource types.</p>
<h3 id="heading-step-3-build-an-evaluation-engine">Step 3: Build an Evaluation Engine</h3>
<p>Combine your policies into a decision engine:</p>
<pre><code class="language-javascript">// abac/engine.js

function evaluateAccess(user, resource, action, environment, policies) {
  // Collect all policy results
  const results = policies.map(policy =&gt; {
    try {
      return policy(user, resource, action, environment);
    } catch (error) {
      console.error(`Policy evaluation error: ${error.message}`);
      return false; // Fail closed: deny on error
    }
  });

  // Deny-overrides: if any policy denies, access is denied
  return results.every(result =&gt; result === true);
}

// Assemble policies for reading patient records
const readPatientRecordPolicies = [
  (user) =&gt; isClinicalStaff(user),
  (user, record) =&gt; isAssignedToRecord(user, record),
  (user) =&gt; isOnActiveShift(user),
  (user, record, action, environment) =&gt; meetsDeviceRequirements(record, environment)
];

// Make an access decision
const canRead = evaluateAccess(
  user,
  patientRecord,
  "read",
  environment,
  readPatientRecordPolicies
);

console.log(`Access ${canRead ? "granted" : "denied"}`);
// → Access granted (all conditions met)
</code></pre>
<p><strong>What this does:</strong> The engine loops through each policy function, passing in the relevant attributes. If all policies return true, access is granted. If any returns false, access is denied. This is called "deny-overrides combining". The <code>try-catch</code> ensures that if a policy throws an error, access is denied rather than granted, following the security principle of fail-closed.</p>
<h3 id="heading-step-4-add-attribute-collection">Step 4: Add Attribute Collection</h3>
<p>In a real application, attributes come from multiple sources:</p>
<pre><code class="language-javascript">// attributes/collector.js

async function collectAttributes(userId, resourceId) {
  // Collect in parallel for performance
  const [user, resource, environment] = await Promise.all([
    fetchUserAttributes(userId),      // From identity provider or HR system
    fetchResourceAttributes(resourceId), // From resource metadata store
    collectEnvironmentConditions()    // Time, IP, device status
  ]);

  return { user, resource, environment };
}

async function fetchUserAttributes(userId) {
  // This would query your user directory, LDAP, or identity provider
  const user = await userDirectory.findById(userId);
  const shift = await shiftService.getActiveShift(userId);
  
  return {
    ...user,
    shiftActive: shift !== null,
    assignedFloor: shift?.floor || null
  };
}

async function collectEnvironmentConditions() {
  return {
    timestamp: new Date().toISOString(),
    ipAddress: request.ip,
    ipRange: await networkService.classifyIP(request.ip),
    deviceCompliant: await deviceService.checkCompliance(request.deviceId)
  };
}
</code></pre>
<p><strong>What this does:</strong> Attribute collection is separated from policy evaluation. This is an important design decision: it means you can test policies with any attribute values without needing real users or resources. It also means you can swap out the source of attributes (say, moving from an on-premise directory to a cloud identity provider) without changing your policies.</p>
<h3 id="heading-step-5-integrate-with-your-api">Step 5: Integrate with Your API</h3>
<p>Use the evaluation engine in your API handlers:</p>
<pre><code class="language-javascript">// middleware/abac.js

function requireAccess(action, resourceType) {
  return async (req, res, next) =&gt; {
    try {
      const { user, resource, environment } = await collectAttributes(
        req.user.id,
        req.params.id
      );

      const policies = getPoliciesFor(resourceType, action);
      const allowed = evaluateAccess(user, resource, action, environment, policies);

      if (!allowed) {
        // Log the denial for audit purposes
        auditLog.record({
          userId: req.user.id,
          resourceId: req.params.id,
          action,
          decision: "denied",
          timestamp: new Date()
        });

        return res.status(403).json({ error: "Access denied" });
      }

      next();
    } catch (error) {
      // Fail closed: deny access on unexpected errors
      return res.status(403).json({ error: "Access denied" });
    }
  };
}

// Use in route definitions
app.get(
  "/patient-records/:id",
  authenticate(),                               // First verify identity
  requireAccess("read", "patientRecord"),       // Then evaluate ABAC
  patientRecordController.getById               // Then handle the request
);
</code></pre>
<p><strong>What this does:</strong> The ABAC check lives in middleware that runs between authentication and the route handler. Authentication establishes who the user is. ABAC decides whether that user can do what they're trying to do. This separation keeps authorization logic out of your business logic.</p>
<h2 id="heading-abac-vs-rbac-when-to-use-which">ABAC vs RBAC: When to Use Which</h2>
<p>RBAC isn't obsolete. It's genuinely the right choice for many applications. The question is which model fits your specific access requirements.</p>
<h3 id="heading-rbac-strengths">RBAC Strengths</h3>
<p>RBAC is simple to understand, simple to implement, and simple to audit. If you can describe your access requirements as a list of roles with fixed permissions, RBAC works well. Most SaaS applications start with RBAC and it serves them fine for years.</p>
<p>A typical RBAC check looks like:</p>
<pre><code class="language-javascript">// Simple RBAC: does the user have the required role?
function canAccess(user, requiredRole) {
  return user.roles.includes(requiredRole);
}
</code></pre>
<p>It's fast, clear, and easy to debug. When something goes wrong, you check which roles the user has and which roles the resource requires.</p>
<h3 id="heading-where-rbac-breaks-down">Where RBAC Breaks Down</h3>
<p>RBAC struggles when permissions need to depend on factors that aren't captured by a role. If you need to express "finance managers can view financial records, but only for their own region, and only during business hours," you're outside what a role alone can express cleanly.</p>
<p>You either need an extremely specific role (<code>finance_manager_us_east_business_hours</code>) that creates the role explosion problem, or you add conditional logic to your application code that effectively recreates ABAC, just in a less organized way.</p>
<h3 id="heading-rbac-vs-abac-comparison">RBAC vs ABAC Comparison</h3>
<table>
<thead>
<tr>
<th>Factor</th>
<th>RBAC</th>
<th>ABAC</th>
</tr>
</thead>
<tbody><tr>
<td>Logic</td>
<td>Permissions assigned to roles, roles assigned to users</td>
<td>Policies evaluate attributes at decision time</td>
</tr>
<tr>
<td>Granularity</td>
<td>Coarse-grained</td>
<td>Fine-grained and context-aware</td>
</tr>
<tr>
<td>Flexibility</td>
<td>Low, new rules require new roles</td>
<td>High, update policies without changing roles</td>
</tr>
<tr>
<td>Scalability</td>
<td>Role explosion under complexity</td>
<td>Scales with policy complexity, not role count</td>
</tr>
<tr>
<td>Auditability</td>
<td>Simple, check role assignments</td>
<td>Requires logging attributes at decision time</td>
</tr>
<tr>
<td>Complexity</td>
<td>Low</td>
<td>Higher, more moving parts</td>
</tr>
<tr>
<td>Best for</td>
<td>Simple, stable permission structures</td>
<td>Complex, dynamic, or context-dependent permissions</td>
</tr>
</tbody></table>
<h3 id="heading-combining-both-models">Combining Both Models</h3>
<p>RBAC and ABAC work well together. A common pattern is to use RBAC for coarse-grained access control (which sections of your application can this user see?) and ABAC for fine-grained control within those sections (which specific records can they access?).</p>
<p>For example, a role might grant access to the patient records section of a hospital system. Within that section, ABAC policies determine which specific records a user can view or edit based on their department, assigned floor, and active shift.</p>
<h2 id="heading-real-world-use-cases">Real-World Use Cases</h2>
<h3 id="heading-healthcare-records-management">Healthcare Records Management</h3>
<p>Healthcare is one of the clearest examples of why ABAC matters. Patient privacy regulations require precise access control, and patient care requires that the right staff can access records quickly when they need them.</p>
<p>An ABAC policy in a hospital might allow a nurse to view a patient's record only when:</p>
<ol>
<li><p>the patient is currently admitted to the nurse's assigned floor,</p>
</li>
<li><p>the nurse is on an active shift,</p>
</li>
<li><p>the access occurs from within the hospital network,</p>
</li>
<li><p>and the record type is within the nurse's care scope.</p>
</li>
</ol>
<p>According to WorkOS's ABAC analysis, in emergency situations ABAC systems can automatically expand access rights. For example, an ER doctor automatically gains broader access to patient records to provide immediate care, with this access being time-bound and closely monitored.</p>
<p>All of these rules would require dozens of roles in an RBAC system, and those roles would still struggle to handle the emergency access scenario dynamically.</p>
<h3 id="heading-corporate-data-access">Corporate Data Access</h3>
<p>Large enterprises typically have employees across departments, roles, locations, and clearance levels who need different views of the same underlying data. A document might be accessible to finance managers in the US region during business hours, accessible to executives globally at any time, but inaccessible to contractors entirely.</p>
<p>ABAC expresses all of these rules in policies. As employees change departments, go on leave, or change roles, their attributes update in the identity system and their access changes automatically, with no manual ACL updates required.</p>
<h3 id="heading-government-and-classified-information">Government and Classified Information</h3>
<p>The US federal government's adoption of ABAC is described in NIST SP 800-162, which was developed to address the Federal Identity, Credential, and Access Management (FICAM) requirements. Federal agencies deal with information shared across organizational boundaries, with varying classification levels and need-to-know requirements.</p>
<p>ABAC allows an analyst in one agency to access information from another agency without requiring the second agency to pre-provision an account for them. The analyst's clearance attributes, organizational affiliation, and project assignments are evaluated against the resource's classification and access rules at the time of the request.</p>
<h3 id="heading-multi-tenant-saas-applications">Multi-Tenant SaaS Applications</h3>
<p>SaaS applications that serve multiple organizations need to ensure strict data isolation between tenants while supporting complex permission structures within each tenant.</p>
<p>ABAC handles this naturally. A resource attribute like <code>record.tenantId</code> is evaluated against the user attribute <code>user.tenantId</code>, and no cross-tenant access is possible through policy. Within a tenant, ABAC supports as much complexity as the tenant's policies require.</p>
<h2 id="heading-enterprise-abac-considerations">Enterprise ABAC Considerations</h2>
<p>Deploying ABAC at enterprise scale introduces several challenges that don't exist in smaller implementations.</p>
<h3 id="heading-policy-administration">Policy Administration</h3>
<p>Policies need to be authored, reviewed, tested, and deployed. According to NIST SP 800-162, this requires a Policy Administration Point (PAP), an interface for creating and managing policies. Without proper tooling, policies become difficult to audit and maintain.</p>
<p>In practice, this means treating policies like code: version control, code review, and automated testing.</p>
<h3 id="heading-attribute-quality-and-freshness">Attribute Quality and Freshness</h3>
<p>ABAC is only as good as the attributes it evaluates. If user attributes are stale, for example, for a user who changed departments but whose directory entry hasn't been updated, the access decisions will be wrong.</p>
<p>NIST warns that "attributes that are not refreshed as often will ultimately be less secure than attributes that are refreshed in real time." Building reliable attribute pipelines from authoritative sources is often the hardest part of ABAC deployment.</p>
<h3 id="heading-performance">Performance</h3>
<p>Evaluating policies on every request has a performance cost. Each evaluation may require fetching attributes from multiple sources. To manage this, many implementations use attribute caching, but caching introduces the staleness problem described above.</p>
<p>The solution is to cache with appropriate TTLs (time-to-live values) based on how quickly each attribute type can change. A user's department changes rarely and can be cached for hours. A user's active shift status might change every 8 hours and needs a shorter cache. Real-time location might not be cacheable at all.</p>
<h3 id="heading-audit-logging">Audit Logging</h3>
<p>Because ABAC makes decisions dynamically, auditing requires logging the attributes used in each decision, not just the decision itself. A log entry that says "access denied" is only useful if it also captures why access was denied and which attributes failed to satisfy which policies.</p>
<p>NIST notes that without tracking attribute values at decision time, accountability requirements can't be met.</p>
<h2 id="heading-limitations-and-challenges">Limitations and Challenges</h2>
<p>ABAC is powerful, but it's not the right solution for every access control problem. It's worth being honest about its limitations before committing to an implementation.</p>
<p><strong>Complexity</strong>: According to NIST SP 800-162, "an ABAC system is more complicated, and therefore more costly to implement and maintain, than simpler access control systems." The flexibility that makes ABAC powerful also makes it harder to reason about. A user asking "why can't I access this?" requires examining all the attributes that were evaluated and which conditions weren't met.</p>
<p><strong>Policy Conflicts</strong>: In complex systems with many policies, conflicts between policies can occur. Two policies might individually seem correct but together produce unexpected results. Resolving these conflicts requires clear precedence rules and careful policy design.</p>
<p><strong>Attribute Management Overhead</strong>: Maintaining accurate attributes across large user populations requires investment in identity infrastructure. Attributes from different systems need to be normalized, validated, and kept synchronized. As NIST describes it, organizations need an entire attribute management infrastructure, not just a policy engine.</p>
<p><strong>Testing is Hard</strong>: Because access depends on the combination of potentially dozens of attributes, testing edge cases comprehensively requires thought. A policy that works correctly for typical cases might behave unexpectedly for unusual attribute combinations.</p>
<p><strong>Not Always Worth the Investment</strong>: For applications with straightforward access requirements, ABAC introduces unnecessary complexity. If your needs can be expressed cleanly as a set of roles with fixed permissions, RBAC is the better choice.</p>
<h2 id="heading-conclusion">Conclusion</h2>
<p>Attribute-Based Access Control represents a genuine evolution in how applications manage authorization. Rather than maintaining ever-growing lists of roles and permissions, ABAC evaluates the actual characteristics of users, resources, and context at the moment of every request.</p>
<p>It solves the role explosion problem that plagues complex RBAC implementations. It enables access rules that reflect real business policies rather than technical approximations of them. It handles dynamic scenarios, emergencies, time-based restrictions, and cross-organizational access that are difficult or impossible to express with static roles.</p>
<p>But ABAC isn't universally better. It's more complex to build, harder to debug, and requires investment in attribute management infrastructure that simpler models don't need. Many applications are well-served by RBAC, and some use RBAC and ABAC together.</p>
<p>The right question isn't "should I use ABAC?" It's "are my access requirements complex enough that the investment in ABAC pays off?" If your access rules change frequently, depend on resource or environment context, or need to scale across organizational boundaries, ABAC is worth serious consideration.</p>
<p>Start by identifying where your current access control model is breaking down. If you're creating roles to represent every edge case, if you're writing conditional logic inside route handlers that checks specific attribute values, or if users are accumulating permissions they should no longer have, those are signals that a more expressive model would help.</p>
<p>ABAC is the tool for when roles aren't enough.</p>
<h2 id="heading-glossary">Glossary</h2>
<p><strong>ABAC (Attribute-Based Access Control)</strong>: An access control method where authorization decisions are made by evaluating policies against the attributes of subjects, objects, actions, and environment conditions. Defined by NIST as the approach where "subject requests to perform operations on objects are granted or denied based on assigned attributes."</p>
<p><strong>Subject</strong>: The entity requesting access to a resource. Usually a human user, but can also be a service, automated process, or device. Also called the "requestor."</p>
<p><strong>Object</strong>: The resource being protected, such as a file, database record, API endpoint, service, or any system resource whose access is managed by the ABAC system.</p>
<p><strong>Attribute</strong>: A characteristic of a subject, object, action, or environment expressed as a name-value pair. For example, <code>user.department = "Finance"</code> or <code>record.sensitivity = "High"</code>.</p>
<p><strong>Subject Attributes</strong>: Properties describing the user or service making the request, such as job title, department, clearance level, or current location.</p>
<p><strong>Object Attributes</strong>: Properties describing the resource being accessed, such as its type, owner, sensitivity level, or department.</p>
<p><strong>Environment Conditions</strong>: Contextual factors independent of both subject and object that influence access decisions. Examples include time of day, day of week, IP address, device compliance status, or current threat level.</p>
<p><strong>Policy</strong>: A rule or set of rules that evaluates attribute values to determine whether a specific access request should be permitted or denied. ABAC policies are typically written as logical conditions.</p>
<p><strong>Policy Decision Point (PDP)</strong>: The component of an ABAC system that evaluates policies and attributes to compute an access decision.</p>
<p><strong>Policy Enforcement Point (PEP)</strong>: The component that intercepts access requests and enforces the decisions made by the PDP.</p>
<p><strong>Policy Information Point (PIP)</strong>: The component that retrieves attribute values needed by the PDP to make decisions.</p>
<p><strong>Policy Administration Point (PAP)</strong>: The component that provides an interface for creating, testing, and managing policies.</p>
<p><strong>RBAC (Role-Based Access Control)</strong>: An access control model that assigns permissions to roles and users to roles. Simpler than ABAC but less expressive for complex, dynamic access requirements.</p>
<p><strong>Role Explosion</strong>: The proliferation of increasingly specific roles in an RBAC system as access requirements become more granular, eventually making the roles as difficult to manage as individual permissions.</p>
<p><strong>DAC (Discretionary Access Control)</strong>: An access control model where resource owners control who can access their resources. Common in file systems.</p>
<p><strong>MAC (Mandatory Access Control)</strong>: An access control model where access is governed by a central authority using classification labels, independent of resource owner preferences.</p>
<p><strong>ACL (Access Control List)</strong>: A list associated with a resource that specifies which users or groups have which permissions. Common in identity-based access control systems.</p>
<p><strong>Non-Person Entity (NPE)</strong>: A subject that is not a human user, such as an automated service, application, or network device, that can request access to resources.</p>
<p><strong>Attribute Caching</strong>: Storing previously retrieved attribute values to improve performance, at the cost of potentially using stale data for access decisions.</p>
<p><strong>Deny-Overrides Combining</strong>: A policy combining rule where if any applicable policy returns deny, the overall decision is deny, regardless of other policies that may return permit.</p>
<p><strong>Fail-Closed</strong>: A security design principle where unexpected errors or missing information result in access being denied rather than granted, reducing the risk of unauthorized access.</p>
<p><em>Source: Definitions adapted from NIST Special Publication 800-162, Guide to Attribute Based Access Control (ABAC) Definition and Considerations, January 2014 (with updates through August 2019).</em></p>
 ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ How to Use OpenStreetMap as a Free Alternative to Google Maps ]]>
                </title>
                <description>
                    <![CDATA[ Google Maps has been the default choice for developers building location-based applications for years. But for many teams, especially those operating at scale, pricing has become a real concern. Googl ]]>
                </description>
                <link>https://www.freecodecamp.org/news/how-to-use-openstreetmap-free-alternative-to-google-maps/</link>
                <guid isPermaLink="false">69c41cdf10e664c5dacd6389</guid>
                
                    <category>
                        <![CDATA[ #LocationServices  ]]>
                    </category>
                
                    <category>
                        <![CDATA[ maps ]]>
                    </category>
                
                    <category>
                        <![CDATA[ React ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ Aiyedogbon Abraham ]]>
                </dc:creator>
                <pubDate>Wed, 25 Mar 2026 17:35:27 +0000</pubDate>
                <media:content url="https://cdn.hashnode.com/uploads/covers/5e1e335a7a1d3fcc59028c64/0ab3655a-4212-451d-93e1-5c707ed1b07e.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>Google Maps has been the default choice for developers building location-based applications for years. But for many teams, especially those operating at scale, pricing has become a real concern.</p>
<p>Google Maps provides a $200 monthly credit, but beyond that, usage is billed per request. For applications like logistics, ride-hailing, or fleet tracking – where thousands of requests are made daily – costs can grow quickly depending on which APIs you use.</p>
<p>OpenStreetMap (OSM) offers a different approach. Instead of charging for access to map APIs, it provides free, open geographic data that you can build on.</p>
<p>In this guide, you'll learn what OpenStreetMap is, how it differs from Google Maps, and how to integrate it into a React application using Leaflet.</p>
<h2 id="heading-what-well-cover">What We'll Cover:</h2>
<ol>
<li><p><a href="#heading-what-is-openstreetmap">What is OpenStreetMap?</a></p>
</li>
<li><p><a href="#heading-why-choose-openstreetmap-over-google-maps">Why Choose OpenStreetMap Over Google Maps?</a></p>
</li>
<li><p><a href="#heading-understanding-the-open-street-map-ecosystem">Understanding the OpenStreetMap Ecosystem</a></p>
<ul>
<li><p><a href="#heading-data-layer-openstreetmap">Data Layer (OpenStreetMap)</a></p>
</li>
<li><p><a href="#heading-rendering-layer-leaflet-maplibre">Rendering Layer (Leaflet, MapLibre)</a></p>
</li>
<li><p><a href="#heading-services-layer">Services Layer</a></p>
</li>
<li><p><a href="#heading-how-everything-works-together">How Everything Works Together</a></p>
</li>
</ul>
</li>
<li><p><a href="#heading-how-to-integrate-openstreetmap-in-react-with-leaflet">How to Integrate OpenStreetMap in React with Leaflet</a></p>
</li>
<li><p><a href="#heading-how-to-add-geocoding-with-nominatim">How to Add Geocoding with Nominatim</a></p>
</li>
<li><p><a href="#heading-advanced-features">Advanced Features</a></p>
</li>
<li><p><a href="#heading-when-to-choose-openstreetmap-vs-google-maps">When to Choose OpenStreetMap vs Google Maps</a></p>
</li>
<li><p><a href="#heading-wrapping-up">Wrapping Up</a></p>
</li>
</ol>
<h2 id="heading-what-is-openstreetmap">What is OpenStreetMap?</h2>
<p>OpenStreetMap is a free, open, and community-driven map of the world. Anyone can contribute to it, and anyone can use it.</p>
<p>Unlike Google Maps, which gives access through controlled APIs, OpenStreetMap gives you access to the underlying geographic data itself.</p>
<p>This data is structured in three main ways:</p>
<ol>
<li><p><strong>Nodes</strong>: single points (for example, a bus stop or a tree)</p>
</li>
<li><p><strong>Ways</strong>: lines or shapes made up of nodes (like roads or buildings)</p>
</li>
<li><p><strong>Relations</strong>: groups of nodes and ways that define more complex things (like routes or boundaries)</p>
</li>
</ol>
<p>Each of these elements includes tags (key-value pairs), such as:</p>
<pre><code class="language-plaintext">highway=residential
name=Allen Avenue
</code></pre>
<p>So instead of just displaying a map, OpenStreetMap lets you work with structured geographic data.</p>
<h3 id="heading-the-open-database-license-odbl">The Open Database License (ODbL)</h3>
<p>OpenStreetMap data is licensed under the ODbL. This means:</p>
<ul>
<li><p>You can use it for commercial or personal projects</p>
</li>
<li><p>You must give proper attribution</p>
</li>
</ul>
<p>This makes it especially useful for developers who want clarity around data ownership.</p>
<h2 id="heading-why-choose-openstreetmap-over-google-maps">Why Choose OpenStreetMap Over Google Maps?</h2>
<h3 id="heading-cost">Cost</h3>
<p>OpenStreetMap data is free to use. But it's important to be precise here: <strong>OpenStreetMap removes licensing costs, but not infrastructure costs.</strong></p>
<p>You may still need to pay for:</p>
<ul>
<li><p>Tile hosting</p>
</li>
<li><p>Geocoding services</p>
</li>
<li><p>Routing engines</p>
</li>
</ul>
<h3 id="heading-control">Control</h3>
<p>With Google Maps, you can't modify the data, and you rely entirely on Google's APIs</p>
<p>But with OpenStreetMap, you can download and store the data, modify it, and build custom solutions on top of it.</p>
<h3 id="heading-customization">Customization</h3>
<p>OpenStreetMap gives you more flexibility:</p>
<ul>
<li><p>You control how maps are rendered</p>
</li>
<li><p>You can choose or build your own map styles</p>
</li>
<li><p>You can create domain-specific maps</p>
</li>
</ul>
<h3 id="heading-adoption">Adoption</h3>
<p>OpenStreetMap is widely used. Companies like Meta and Microsoft contribute to it, and many platforms rely on it directly or indirectly.</p>
<p>This shows that the ecosystem is mature and reliable.</p>
<h2 id="heading-understanding-the-openstreetmap-ecosystem">Understanding the OpenStreetMap Ecosystem</h2>
<p>A common mistake is to think that OpenStreetMap works like a single API. It doesn't.</p>
<p>Instead, it works as a set of layers, where each layer handles a different responsibility.</p>
<h3 id="heading-data-layer-openstreetmap">Data Layer (OpenStreetMap)</h3>
<p>This is the foundation. It contains all the raw geographic data:</p>
<ul>
<li><p>Roads</p>
</li>
<li><p>Buildings</p>
</li>
<li><p>Landmarks</p>
</li>
<li><p>Boundaries</p>
</li>
</ul>
<p>This is what you are ultimately working with.</p>
<h3 id="heading-rendering-layer-leaflet-maplibre">Rendering Layer (Leaflet, MapLibre)</h3>
<p>Raw data isn't visual. It needs to be turned into something users can see.</p>
<p>There are two main approaches:</p>
<ol>
<li><p><strong>Raster tiles</strong> (used by Leaflet): pre-rendered images</p>
</li>
<li><p><strong>Vector tiles</strong> (used by MapLibre): raw geometry styled in the browser</p>
</li>
</ol>
<p>Leaflet uses raster tiles by default, which makes it simple and fast to start with.</p>
<h3 id="heading-services-layer">Services Layer</h3>
<p>This is what makes your map interactive. <strong>Geocoding</strong> converts addresses into coordinates, while <strong>reverse geocoding</strong> converts coordinates into addresses.</p>
<p><strong>Routing</strong> calculates directions between points, and <strong>tile servers</strong> provide the actual map visuals.</p>
<h3 id="heading-how-everything-works-together">How Everything Works Together</h3>
<p>When a user searches for a place:</p>
<ol>
<li><p>The user enters a location</p>
</li>
<li><p>A geocoding service converts it into coordinates</p>
</li>
<li><p>The map updates its position</p>
</li>
<li><p>A tile server provides the visual map</p>
</li>
</ol>
<p>Each part is separate, but they work together to create the full experience.</p>
<h2 id="heading-how-to-integrate-openstreetmap-in-react-with-leaflet">How to Integrate OpenStreetMap in React with Leaflet</h2>
<p>Let's build a simple map.</p>
<h3 id="heading-step-1-create-a-react-app">Step 1: Create a React App</h3>
<pre><code class="language-bash">npm create vite@latest osm-app -- --template react
cd osm-app
npm install
</code></pre>
<h3 id="heading-step-2-install-dependencies">Step 2: Install Dependencies</h3>
<pre><code class="language-bash">npm install leaflet react-leaflet
npm install --save-dev @types/leaflet
</code></pre>
<h3 id="heading-step-3-import-leaflet-css">Step 3: Import Leaflet CSS</h3>
<pre><code class="language-javascript">import 'leaflet/dist/leaflet.css';
</code></pre>
<p>This is required for the map to display correctly.</p>
<h3 id="heading-step-4-create-a-map-component">Step 4: Create a Map Component</h3>
<pre><code class="language-javascript">import { MapContainer, TileLayer, Marker, Popup } from 'react-leaflet';

function Map() {
  const position = [51.505, -0.09]; // latitude, longitude

  return (
    &lt;MapContainer
      center={position}
      zoom={13}
      style={{ height: '100vh' }}
    &gt;
      &lt;TileLayer
        attribution='&amp;copy; &lt;a href="https://www.openstreetmap.org/copyright"&gt;OpenStreetMap&lt;/a&gt; contributors'
        url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
      /&gt;
      &lt;Marker position={position}&gt;
        &lt;Popup&gt;Hello from OpenStreetMap&lt;/Popup&gt;
      &lt;/Marker&gt;
    &lt;/MapContainer&gt;
  );
}

export default Map;
</code></pre>
<p>Let's break down the important parts here:</p>
<p><code>MapContainer</code> initializes the map.</p>
<ul>
<li><p><code>center</code> is where the map starts</p>
</li>
<li><p><code>zoom</code> is how close the view is</p>
</li>
<li><p><code>style</code> must include height, or the map won't show</p>
</li>
</ul>
<p><code>TileLayer</code> defines where the map visuals come from.</p>
<ul>
<li><p><code>{z}</code> is the zoom level</p>
</li>
<li><p><code>{x}</code>, <code>{y}</code> are the tile coordinates</p>
</li>
<li><p><code>{s}</code> is the subdomain</p>
</li>
</ul>
<p>Each tile is a small image (usually 256×256 pixels), and Leaflet combines them to form the full map.</p>
<p><code>Marker</code> adds a point on the map at a specific coordinate.</p>
<p><code>Popup</code> displays information when the marker is clicked.</p>
<h4 id="heading-important-note">Important note:</h4>
<p>The default OpenStreetMap tile server:</p>
<pre><code class="language-plaintext">https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png
</code></pre>
<p>is meant for learning, demos, and low-traffic apps. For production, you should use a dedicated provider or your own tile server.</p>
<h2 id="heading-how-to-add-geocoding-with-nominatim">How to Add Geocoding with Nominatim</h2>
<p>Nominatim is OpenStreetMap's geocoding service. It allows you to convert addresses into coordinates and coordinates into readable locations.</p>
<h3 id="heading-custom-hook-for-geocoding">Custom Hook for Geocoding</h3>
<pre><code class="language-javascript">import { useState } from 'react';

export function useGeocoding() {
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState(null);

  const searchAddress = async (query) =&gt; {
    setLoading(true);
    setError(null);

    try {
      const response = await fetch(
        `https://nominatim.openstreetmap.org/search?q=${encodeURIComponent(query)}&amp;format=json&amp;limit=5`,
        {
          headers: {
            'User-Agent': 'YourAppName/1.0'
          }
        }
      );

      if (!response.ok) {
        throw new Error('Request failed');
      }

      const data = await response.json();
      setLoading(false);
      return data;
    } catch (err) {
      setError(err.message);
      setLoading(false);
      return [];
    }
  };

  return { searchAddress, loading, error };
}
</code></pre>
<p>In this code:</p>
<ul>
<li><p><code>useState</code> manages loading and error states</p>
</li>
<li><p><code>encodeURIComponent</code> ensures safe URLs</p>
</li>
<li><p><code>User-Agent</code> is required by Nominatim</p>
</li>
<li><p><code>response.json()</code> converts response into usable data</p>
</li>
</ul>
<p>Nominatim returns coordinates as strings, so you have to convert them before using them.</p>
<h3 id="heading-important-usage-rules">Important Usage Rules</h3>
<p>The public Nominatim service:</p>
<ul>
<li><p>Allows about 1 request per second</p>
</li>
<li><p>Requires proper identification</p>
</li>
<li><p>May block excessive usage</p>
</li>
</ul>
<p>You should debounce user input, cache results, and avoid repeated requests.</p>
<h3 id="heading-creating-a-search-component">Creating a Search Component</h3>
<p>The search component lets users type an address or place name and get matching locations via Nominatim. It includes a text input and a submit button.</p>
<p>When the form is submitted, it calls our <code>searchAddress</code> function (from the <code>useGeocoding</code> hook), which fetches up to 5 address results. These results are displayed below the input as clickable items.</p>
<p>When the user clicks a result, the component parses the returned latitude and longitude into numbers and passes them (along with a display name) up to the parent component via the <code>onLocationSelect</code> callback. This will allow the parent (for example, the map) to update its center based on the chosen location.</p>
<pre><code class="language-javascript">function SearchBox({ onLocationSelect }) {
  const [query, setQuery] = useState('');
  const [results, setResults] = useState([]);
  const { searchAddress, loading } = useGeocoding();

  const handleSearch = async (e) =&gt; {
    e.preventDefault();
    if (!query.trim()) return;

    const data = await searchAddress(query);
    setResults(data);
  };

  const selectLocation = (result) =&gt; {
    onLocationSelect({
      lat: parseFloat(result.lat),
      lon: parseFloat(result.lon),
      name: result.display_name
    });
  };

  return (
    &lt;div&gt;
      &lt;form onSubmit={handleSearch}&gt;
        &lt;input
          value={query}
          onChange={(e) =&gt; setQuery(e.target.value)}
          placeholder="Search location"
        /&gt;
        &lt;button type="submit"&gt;
          {loading ? 'Searching...' : 'Search'}
        &lt;/button&gt;
      &lt;/form&gt;

      &lt;div&gt;
        {results.map((result) =&gt; (
          &lt;div key={result.place_id} onClick={() =&gt; selectLocation(result)}&gt;
            {result.display_name}
          &lt;/div&gt;
        ))}
      &lt;/div&gt;
    &lt;/div&gt;
  );
}
</code></pre>
<p>Key concepts here:</p>
<ul>
<li><p><code>useState</code> stores the current input (<code>query</code>) and the array of search <code>results</code>.</p>
</li>
<li><p><code>e.preventDefault()</code> stops the form submission from reloading the page.</p>
</li>
<li><p>Calling <code>searchAddress(query)</code> fetches geocoding results from Nominatim.</p>
</li>
<li><p><code>parseFloat()</code> converts the returned <code>lat</code>/<code>lon</code> strings into JavaScript numbers before using them.</p>
</li>
<li><p><code>onLocationSelect</code> is a callback prop that sends the selected coordinates and name back to the parent component (for example to update the map).</p>
</li>
</ul>
<h2 id="heading-advanced-features">Advanced Features</h2>
<p>We can further extend the map app by adding more advanced functionality. For example:</p>
<h3 id="heading-routing-osrm-graphhopper">Routing (OSRM, GraphHopper)</h3>
<p>You can integrate turn-by-turn routing on your map. A common solution is to use a library like <a href="https://www.liedman.net/leaflet-routing-machine/">Leaflet Routing Machine</a>, which supports OSRM out of the box and has plugins for GraphHopper. This adds a route UI control where users enter start and end points, and the library fetches a route from one of these engines to draw on the map.</p>
<h3 id="heading-custom-tile-providers-carto-maptiler-and-so-on"><strong>Custom Tile Providers (Carto, MapTiler, and so on)</strong></h3>
<p>Instead of the standard <a href="http://tile.openstreetmap.org"><code>tile.openstreetmap.org</code></a>, you can use hosted tile services that offer OSM-based maps. For example, Carto and MapTiler both provide tile APIs (often with custom style options and higher usage limits).</p>
<p>Carto, MapTiler, and similar services are listed among the providers that allow free usage of OSM tiles. By using a custom tile provider, you gain flexibility in map design and avoid hitting the public server’s limits.</p>
<h3 id="heading-vector-maps-maplibre-gl-js">Vector Maps (MapLibre GL JS)</h3>
<p>You can switch from raster tiles to vector tiles for even richer interactivity. Vector tiles send raw map data (geometries and attributes) to the client, which are then rendered in the browser. This allows dynamic styling and advanced features: for instance, you can change the map’s theme on the fly (for example, switch to a “dark mode” style at night) or highlight certain features like bike lanes more prominently.</p>
<p>Libraries like MapLibre GL JS (the open-source successor to Mapbox GL) can display OSM vector tiles with highly customizable styles and smooth zooming/rotation. This makes your map more responsive and adaptable to different use cases.</p>
<h2 id="heading-when-to-choose-openstreetmap-vs-google-maps">When to Choose OpenStreetMap vs Google Maps</h2>
<h3 id="heading-choose-openstreetmap-when">Choose OpenStreetMap when:</h3>
<ul>
<li><p>You need flexibility</p>
</li>
<li><p>You want to reduce costs at scale</p>
</li>
<li><p>You want control over data</p>
</li>
</ul>
<h3 id="heading-choose-google-maps-when">Choose Google Maps when:</h3>
<ul>
<li><p>You want an all-in-one solution</p>
</li>
<li><p>You need features like Street View</p>
</li>
<li><p>You want minimal setup</p>
</li>
</ul>
<h2 id="heading-wrapping-up">Wrapping Up</h2>
<p>OpenStreetMap offers a powerful alternative to Google Maps for developers who need cost control, data ownership, and customization. While it requires understanding different components, the flexibility it provides is worth the learning curve.</p>
 ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ How to Test React Applications with Vitest ]]>
                </title>
                <description>
                    <![CDATA[ Testing is one of those things that every developer knows they should do, but many put off until problems start appearing in production. If you’re building React applications with Vite, there's a testing framework that fits so naturally into your wor... ]]>
                </description>
                <link>https://www.freecodecamp.org/news/how-to-test-react-applications-with-vitest/</link>
                <guid isPermaLink="false">698bb499f3de8b702a26aec1</guid>
                
                    <category>
                        <![CDATA[ unit testing ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Testing ]]>
                    </category>
                
                    <category>
                        <![CDATA[ vitest ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ Aiyedogbon Abraham ]]>
                </dc:creator>
                <pubDate>Tue, 10 Feb 2026 22:43:37 +0000</pubDate>
                <media:content url="https://cdn.hashnode.com/res/hashnode/image/upload/v1770763375195/82544dec-aec2-4de9-b7f8-f90349394e81.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>Testing is one of those things that every developer knows they should do, but many put off until problems start appearing in production. If you’re building React applications with Vite, there's a testing framework that fits so naturally into your workflow that you might actually enjoy writing tests. That framework is Vitest.</p>
<p>In this tutorial, you’ll learn how to set up Vitest in a React project, write effective tests for your components and hooks, and understand the testing patterns that will help you build more reliable applications.</p>
<h2 id="heading-table-of-contents">Table of Contents</h2>
<ul>
<li><p><a class="post-section-overview" href="#heading-what-is-vitest-and-why-should-you-use-it">What is Vitest and Why Should You Use It?</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-prerequisites">Prerequisites</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-how-to-set-up-vitest-in-your-react-project">How to Set Up Vitest in Your React Project</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-how-to-write-your-first-test">How to Write Your First Test</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-how-to-test-react-components">How to Test React Components</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-how-to-test-user-interactions">How to Test User Interactions</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-how-to-test-custom-hooks">How to Test Custom Hooks</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-how-to-mock-api-calls">How to Mock API Calls</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-best-practices-for-testing-react-components">Best Practices for Testing React Components</a></p>
</li>
</ul>
<h2 id="heading-what-is-vitest-and-why-should-you-use-it">What is Vitest and Why Should You Use It?</h2>
<p>Vitest is a testing framework built on top of Vite. It uses Vite’s development server and plugin pipeline to transform and load files during testing. This means your tests use the same configuration and plugins as your app (for example, the React plugin, TypeScript support,and so on), so you don’t need a separate build or compile step.</p>
<p>Vitest runs tests in parallel across worker threads for maximum speed, and it automatically enables an instant “watch” mode (similar to Vite’s HMR) that reruns only the tests related to changed files. Vitest also has first-class support for modern JavaScript out of the box: it handles ESM, TypeScript, and JSX natively via Vite’s transformer (powered by Oxc).</p>
<p>Because Vitest provides a Jest-compatible API, you can continue to use familiar testing libraries (for example, React Testing Library, jest-dom matchers, user-event, and so on) without extra setup.</p>
<p>In short, Vitest tightly integrates with your Vite-powered stack (or can even run standalone) and lets you plug in existing testing tools seamlessly.</p>
<p>Here is why Vitest has become popular in the React ecosystem:</p>
<ul>
<li><p><strong>Speed</strong>: Vitest can run tests more than four times faster than Jest in many scenarios. This speed comes from Vite's fast Hot Module Replacement and efficient caching capabilities.</p>
</li>
<li><p><strong>Zero configuration</strong>: Unlike Jest, which required Babel integration, TSJest setup, and multiple dependencies, Vitest works out of the box. It reuses your existing Vite configuration, eliminating the need to configure a separate test pipeline.</p>
</li>
<li><p><strong>Native TypeScript support</strong>: Vitest handles TypeScript and JSX natively through ESBuild, with no additional configuration needed.</p>
</li>
<li><p><strong>Modern JavaScript</strong>: Vitest offers native support for ES modules out of the box, making it ideal for modern JavaScript stacks.</p>
</li>
<li><p><strong>Familiar API</strong>: If you know Jest, you already know most of Vitest. The API is intentionally compatible, making migration straightforward.</p>
</li>
</ul>
<h2 id="heading-prerequisites">Prerequisites</h2>
<p>To follow along with this tutorial, you should have:</p>
<ul>
<li><p>Basic knowledge of React and JavaScript</p>
</li>
<li><p>Understanding of React Hooks</p>
</li>
<li><p>Node.js installed (version 14 or higher)</p>
</li>
<li><p>A React project created with Vite (or you can create one as we go)</p>
</li>
</ul>
<h2 id="heading-how-to-set-up-vitest-in-your-react-project">How to Set Up Vitest in Your React Project</h2>
<p>Let's start by creating a new React project with Vite and setting up Vitest.</p>
<h3 id="heading-step-1-create-a-react-project-with-vite">Step 1: Create a React Project with Vite</h3>
<p>If you don't have an existing project, create one with the following command:</p>
<pre><code class="lang-bash">npm create vite@latest my-react-app -- --template react
<span class="hljs-built_in">cd</span> my-react-app
npm install
</code></pre>
<p>This creates a React project with Vite as the build tool.</p>
<h3 id="heading-step-2-install-vitest-and-testing-dependencies">Step 2: Install Vitest and Testing Dependencies</h3>
<p>Install Vitest along with the React Testing Library and other necessary dependencies:</p>
<pre><code class="lang-bash">npm install --save-dev vitest @testing-library/react @testing-library/jest-dom @testing-library/user-event jsdom
</code></pre>
<p>Here's what each package does:</p>
<ul>
<li><p><strong>vitest</strong>: The testing framework itself</p>
</li>
<li><p><strong>@testing-library/react</strong>: Provides utilities for testing React components</p>
</li>
<li><p><strong>@testing-library/jest-dom</strong>: Adds custom matchers for DOM assertions</p>
</li>
<li><p><strong>@testing-library/user-event</strong>: Simulates user interactions</p>
</li>
<li><p><strong>jsdom</strong>: Provides a DOM environment for testing</p>
</li>
</ul>
<h3 id="heading-step-3-configure-vitest">Step 3: Configure Vitest</h3>
<p>Create a <code>vitest.config.js</code> file in your project root:</p>
<pre><code class="lang-javascript"><span class="hljs-keyword">import</span> { defineConfig } <span class="hljs-keyword">from</span> <span class="hljs-string">'vitest/config'</span>;
<span class="hljs-keyword">import</span> react <span class="hljs-keyword">from</span> <span class="hljs-string">'@vitejs/plugin-react'</span>;

<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> defineConfig({
  <span class="hljs-attr">plugins</span>: [react()],
  <span class="hljs-attr">test</span>: {
    <span class="hljs-attr">globals</span>: <span class="hljs-literal">true</span>,
    <span class="hljs-attr">environment</span>: <span class="hljs-string">'jsdom'</span>,
    <span class="hljs-attr">setupFiles</span>: <span class="hljs-string">'./src/test/setup.js'</span>,
  },
});
</code></pre>
<p>Setting <code>globals: true</code> exposes the <code>describe</code> and <code>it</code> functions on the global object, so you don't need to import them in every test file. The <code>environment: 'jsdom'</code> setting tells Vitest to use jsdom for simulating a browser environment.</p>
<h3 id="heading-step-4-create-the-test-setup-file">Step 4: Create the Test Setup File</h3>
<p>Create a file at <code>src/test/setup.js</code>:</p>
<pre><code class="lang-javascript"><span class="hljs-keyword">import</span> { expect, afterEach } <span class="hljs-keyword">from</span> <span class="hljs-string">'vitest'</span>;
<span class="hljs-keyword">import</span> { cleanup } <span class="hljs-keyword">from</span> <span class="hljs-string">'@testing-library/react'</span>;
<span class="hljs-keyword">import</span> <span class="hljs-string">'@testing-library/jest-dom'</span>;

afterEach(<span class="hljs-function">() =&gt;</span> {
  cleanup();
});
</code></pre>
<p>The <code>cleanup()</code> function runs after each test to clean up the DOM, ensuring tests don't interfere with each other.</p>
<h3 id="heading-step-5-add-test-scripts">Step 5: Add Test Scripts</h3>
<p>Add the following script to your <code>package.json</code>:</p>
<pre><code class="lang-json">{
  <span class="hljs-attr">"scripts"</span>: {
    <span class="hljs-attr">"dev"</span>: <span class="hljs-string">"vite"</span>,
    <span class="hljs-attr">"build"</span>: <span class="hljs-string">"vite build"</span>,
    <span class="hljs-attr">"test"</span>: <span class="hljs-string">"vitest"</span>,
    <span class="hljs-attr">"test:ui"</span>: <span class="hljs-string">"vitest --ui"</span>,
    <span class="hljs-attr">"coverage"</span>: <span class="hljs-string">"vitest --coverage"</span>
  }
}
</code></pre>
<p>Now you can run tests with <code>npm test</code>.</p>
<h2 id="heading-how-to-write-your-first-test">How to Write Your First Test</h2>
<p>Let's write a simple test to make sure everything is working. Create a file called <code>sum.test.js</code> in your <code>src</code> directory:</p>
<pre><code class="lang-javascript"><span class="hljs-keyword">import</span> { expect, test } <span class="hljs-keyword">from</span> <span class="hljs-string">'vitest'</span>;

<span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">sum</span>(<span class="hljs-params">a, b</span>) </span>{
  <span class="hljs-keyword">return</span> a + b;
}

test(<span class="hljs-string">'adds 1 + 2 to equal 3'</span>, <span class="hljs-function">() =&gt;</span> {
  expect(sum(<span class="hljs-number">1</span>, <span class="hljs-number">2</span>)).toBe(<span class="hljs-number">3</span>);
});
</code></pre>
<p>Run <code>npm test</code> and you should see your test pass. A test in Vitest passes if it doesn't throw an error.</p>
<h2 id="heading-how-to-test-react-components">How to Test React Components</h2>
<p>Now let's test an actual React component. We'll start with a simple component and gradually build up to more complex scenarios.</p>
<h3 id="heading-testing-a-simple-component">Testing a Simple Component</h3>
<p>Create a component called <code>Greeting.jsx</code>:</p>
<pre><code class="lang-javascript"><span class="hljs-keyword">export</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">Greeting</span>(<span class="hljs-params">{ name }</span>) </span>{
  <span class="hljs-keyword">return</span> (
    <span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">div</span>&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">h1</span>&gt;</span>Hello, {name}!<span class="hljs-tag">&lt;/<span class="hljs-name">h1</span>&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">p</span>&gt;</span>Welcome to our application<span class="hljs-tag">&lt;/<span class="hljs-name">p</span>&gt;</span>
    <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span></span>
  );
}
</code></pre>
<p>Now create a test file <code>Greeting.test.jsx</code>:</p>
<pre><code class="lang-javascript"><span class="hljs-keyword">import</span> { render, screen } <span class="hljs-keyword">from</span> <span class="hljs-string">'@testing-library/react'</span>;
<span class="hljs-keyword">import</span> { Greeting } <span class="hljs-keyword">from</span> <span class="hljs-string">'./Greeting'</span>;

describe(<span class="hljs-string">'Greeting Component'</span>, <span class="hljs-function">() =&gt;</span> {
  it(<span class="hljs-string">'should render the greeting with the provided name'</span>, <span class="hljs-function">() =&gt;</span> {
    render(<span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">Greeting</span> <span class="hljs-attr">name</span>=<span class="hljs-string">"Alice"</span> /&gt;</span></span>);

    <span class="hljs-keyword">const</span> heading = screen.getByRole(<span class="hljs-string">'heading'</span>, { <span class="hljs-attr">level</span>: <span class="hljs-number">1</span> });
    expect(heading).toHaveTextContent(<span class="hljs-string">'Hello, Alice!'</span>);
  });

  it(<span class="hljs-string">'should render the welcome message'</span>, <span class="hljs-function">() =&gt;</span> {
    render(<span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">Greeting</span> <span class="hljs-attr">name</span>=<span class="hljs-string">"Bob"</span> /&gt;</span></span>);

    <span class="hljs-keyword">const</span> paragraph = screen.getByText(<span class="hljs-string">'Welcome to our application'</span>);
    expect(paragraph).toBeInTheDocument();
  });
});
</code></pre>
<p>The <code>describe</code> function groups related tests into a single describe block. Each <code>it</code> function contains one test case.</p>
<p>The <code>render</code> function from React Testing Library renders your component in a test environment. The <code>screen</code> object provides query methods to find elements in the rendered output.</p>
<h3 id="heading-understanding-query-functions">Understanding Query Functions</h3>
<p>React Testing Library provides three types of query functions: <code>get</code>, <code>query</code>, and <code>find</code>.</p>
<p><strong>getBy queries</strong>: Throw an error if the element isn't found. Use these when you expect the element to be present.</p>
<pre><code class="lang-javascript"><span class="hljs-keyword">const</span> button = screen.getByRole(<span class="hljs-string">'button'</span>, { <span class="hljs-attr">name</span>: <span class="hljs-regexp">/click me/i</span> });
</code></pre>
<p><strong>queryBy queries</strong>: Return <code>null</code> if the element isn't found. Use these when you want to assert that an element doesn't exist.</p>
<pre><code class="lang-javascript"><span class="hljs-keyword">const</span> errorMessage = screen.queryByText(<span class="hljs-string">'Error'</span>);
expect(errorMessage).not.toBeInTheDocument();
</code></pre>
<p><strong>findBy queries</strong>: Return a promise and wait for the element to appear. Use these for asynchronous operations.</p>
<pre><code class="lang-javascript"><span class="hljs-keyword">const</span> loadedData = <span class="hljs-keyword">await</span> screen.findByText(<span class="hljs-string">'Data loaded'</span>);
</code></pre>
<h3 id="heading-testing-a-counter-component">Testing a Counter Component</h3>
<p>Let's test a more interactive component. Create <code>Counter.jsx</code>:</p>
<pre><code class="lang-javascript"><span class="hljs-keyword">import</span> { useState } <span class="hljs-keyword">from</span> <span class="hljs-string">'react'</span>;

<span class="hljs-keyword">export</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">Counter</span>(<span class="hljs-params">{ initialCount = <span class="hljs-number">0</span> }</span>) </span>{
  <span class="hljs-keyword">const</span> [count, setCount] = useState(initialCount);

  <span class="hljs-keyword">return</span> (
    <span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">div</span>&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">p</span>&gt;</span>Count: {count}<span class="hljs-tag">&lt;/<span class="hljs-name">p</span>&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">button</span> <span class="hljs-attr">onClick</span>=<span class="hljs-string">{()</span> =&gt;</span> setCount(count + 1)}&gt;Increment<span class="hljs-tag">&lt;/<span class="hljs-name">button</span>&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">button</span> <span class="hljs-attr">onClick</span>=<span class="hljs-string">{()</span> =&gt;</span> setCount(count - 1)}&gt;Decrement<span class="hljs-tag">&lt;/<span class="hljs-name">button</span>&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">button</span> <span class="hljs-attr">onClick</span>=<span class="hljs-string">{()</span> =&gt;</span> setCount(0)}&gt;Reset<span class="hljs-tag">&lt;/<span class="hljs-name">button</span>&gt;</span>
    <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span></span>
  );
}
</code></pre>
<p>Create the test file <code>Counter.test.jsx</code>:</p>
<pre><code class="lang-javascript"><span class="hljs-keyword">import</span> { render, screen } <span class="hljs-keyword">from</span> <span class="hljs-string">'@testing-library/react'</span>;
<span class="hljs-keyword">import</span> userEvent <span class="hljs-keyword">from</span> <span class="hljs-string">'@testing-library/user-event'</span>;
<span class="hljs-keyword">import</span> { Counter } <span class="hljs-keyword">from</span> <span class="hljs-string">'./Counter'</span>;

describe(<span class="hljs-string">'Counter Component'</span>, <span class="hljs-function">() =&gt;</span> {
  it(<span class="hljs-string">'should render with initial count of 0'</span>, <span class="hljs-function">() =&gt;</span> {
    render(<span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">Counter</span> /&gt;</span></span>);

    expect(screen.getByText(<span class="hljs-string">'Count: 0'</span>)).toBeInTheDocument();
  });

  it(<span class="hljs-string">'should render with custom initial count'</span>, <span class="hljs-function">() =&gt;</span> {
    render(<span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">Counter</span> <span class="hljs-attr">initialCount</span>=<span class="hljs-string">{5}</span> /&gt;</span></span>);

    expect(screen.getByText(<span class="hljs-string">'Count: 5'</span>)).toBeInTheDocument();
  });

  it(<span class="hljs-string">'should increment count when increment button is clicked'</span>, <span class="hljs-keyword">async</span> () =&gt; {
    <span class="hljs-keyword">const</span> user = userEvent.setup();
    render(<span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">Counter</span> /&gt;</span></span>);

    <span class="hljs-keyword">const</span> incrementButton = screen.getByRole(<span class="hljs-string">'button'</span>, { <span class="hljs-attr">name</span>: <span class="hljs-regexp">/increment/i</span> });
    <span class="hljs-keyword">await</span> user.click(incrementButton);

    expect(screen.getByText(<span class="hljs-string">'Count: 1'</span>)).toBeInTheDocument();
  });

  it(<span class="hljs-string">'should decrement count when decrement button is clicked'</span>, <span class="hljs-keyword">async</span> () =&gt; {
    <span class="hljs-keyword">const</span> user = userEvent.setup();
    render(<span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">Counter</span> <span class="hljs-attr">initialCount</span>=<span class="hljs-string">{5}</span> /&gt;</span></span>);

    <span class="hljs-keyword">const</span> decrementButton = screen.getByRole(<span class="hljs-string">'button'</span>, { <span class="hljs-attr">name</span>: <span class="hljs-regexp">/decrement/i</span> });
    <span class="hljs-keyword">await</span> user.click(decrementButton);

    expect(screen.getByText(<span class="hljs-string">'Count: 4'</span>)).toBeInTheDocument();
  });

  it(<span class="hljs-string">'should reset count to 0 when reset button is clicked'</span>, <span class="hljs-keyword">async</span> () =&gt; {
    <span class="hljs-keyword">const</span> user = userEvent.setup();
    render(<span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">Counter</span> <span class="hljs-attr">initialCount</span>=<span class="hljs-string">{10}</span> /&gt;</span></span>);

    <span class="hljs-keyword">const</span> resetButton = screen.getByRole(<span class="hljs-string">'button'</span>, { <span class="hljs-attr">name</span>: <span class="hljs-regexp">/reset/i</span> });
    <span class="hljs-keyword">await</span> user.click(resetButton);

    expect(screen.getByText(<span class="hljs-string">'Count: 0'</span>)).toBeInTheDocument();
  });
});
</code></pre>
<p>In these Counter tests, we first use <code>render(&lt;Counter /&gt;)</code> to mount the component in a virtual DOM. We then query the output using Testing Library’s <code>screen</code> object. For example, <code>screen.getByText('Count: 0')</code> finds the element displaying the initial count of 0, and <code>expect(...).toBeInTheDocument()</code> asserts that it is present. The <code>getByText</code> query will throw an error if the text isn’t found, immediately failing the test.</p>
<p>For interactive tests, we create a <code>user</code> with <code>const user = userEvent.setup()</code> and then call <code>await user.click(...)</code> on the increment/decrement/reset buttons. The <code>userEvent.click</code> method simulates a real user click (dispatching the sequence of events a browser would fire). We locate buttons by their accessible role and name (for example, <code>getByRole('button', { name: /increment/i })</code>), following best practices for accessible queries.</p>
<p>After each click, we assert that the DOM updates accordingly (for example, the count text changes to “Count: 1”). Using <code>async/await</code> with <code>user.click</code> ensures the test waits for any state changes. In this way, each test checks the user-visible behavior: that clicking the Increment button increases the count, the Decrement button decreases it, and the Reset button sets it back to zero, without depending on the component’s internal implementation.</p>
<h2 id="heading-how-to-test-user-interactions">How to Test User Interactions</h2>
<p>User interactions are a critical part of testing React applications. The <code>@testing-library/user-event</code> library provides a more realistic simulation of user behaviour than simple event dispatching.</p>
<h3 id="heading-testing-form-inputs">Testing Form Inputs</h3>
<p>Create a <code>LoginForm.jsx</code> component:</p>
<pre><code class="lang-javascript"><span class="hljs-keyword">import</span> { useState } <span class="hljs-keyword">from</span> <span class="hljs-string">'react'</span>;

<span class="hljs-keyword">export</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">LoginForm</span>(<span class="hljs-params">{ onSubmit }</span>) </span>{
  <span class="hljs-keyword">const</span> [email, setEmail] = useState(<span class="hljs-string">''</span>);
  <span class="hljs-keyword">const</span> [password, setPassword] = useState(<span class="hljs-string">''</span>);
  <span class="hljs-keyword">const</span> [error, setError] = useState(<span class="hljs-string">''</span>);

  <span class="hljs-keyword">const</span> handleSubmit = <span class="hljs-function">(<span class="hljs-params">e</span>) =&gt;</span> {
    e.preventDefault();

    <span class="hljs-keyword">if</span> (!email || !password) {
      setError(<span class="hljs-string">'Both fields are required'</span>);
      <span class="hljs-keyword">return</span>;
    }

    setError(<span class="hljs-string">''</span>);
    onSubmit({ email, password });
  };

  <span class="hljs-keyword">return</span> (
    <span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">form</span> <span class="hljs-attr">onSubmit</span>=<span class="hljs-string">{handleSubmit}</span>&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">div</span>&gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">label</span> <span class="hljs-attr">htmlFor</span>=<span class="hljs-string">"email"</span>&gt;</span>Email<span class="hljs-tag">&lt;/<span class="hljs-name">label</span>&gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">input</span>
          <span class="hljs-attr">id</span>=<span class="hljs-string">"email"</span>
          <span class="hljs-attr">type</span>=<span class="hljs-string">"email"</span>
          <span class="hljs-attr">value</span>=<span class="hljs-string">{email}</span>
          <span class="hljs-attr">onChange</span>=<span class="hljs-string">{(e)</span> =&gt;</span> setEmail(e.target.value)}
        /&gt;
      <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">div</span>&gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">label</span> <span class="hljs-attr">htmlFor</span>=<span class="hljs-string">"password"</span>&gt;</span>Password<span class="hljs-tag">&lt;/<span class="hljs-name">label</span>&gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">input</span>
          <span class="hljs-attr">id</span>=<span class="hljs-string">"password"</span>
          <span class="hljs-attr">type</span>=<span class="hljs-string">"password"</span>
          <span class="hljs-attr">value</span>=<span class="hljs-string">{password}</span>
          <span class="hljs-attr">onChange</span>=<span class="hljs-string">{(e)</span> =&gt;</span> setPassword(e.target.value)}
        /&gt;
      <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
      {error &amp;&amp; <span class="hljs-tag">&lt;<span class="hljs-name">p</span> <span class="hljs-attr">role</span>=<span class="hljs-string">"alert"</span>&gt;</span>{error}<span class="hljs-tag">&lt;/<span class="hljs-name">p</span>&gt;</span>}
      <span class="hljs-tag">&lt;<span class="hljs-name">button</span> <span class="hljs-attr">type</span>=<span class="hljs-string">"submit"</span>&gt;</span>Log In<span class="hljs-tag">&lt;/<span class="hljs-name">button</span>&gt;</span>
    <span class="hljs-tag">&lt;/<span class="hljs-name">form</span>&gt;</span></span>
  );
}
</code></pre>
<p>Create the test file <code>LoginForm.test.jsx</code>:</p>
<pre><code class="lang-javascript"><span class="hljs-keyword">import</span> { render, screen } <span class="hljs-keyword">from</span> <span class="hljs-string">'@testing-library/react'</span>;
<span class="hljs-keyword">import</span> userEvent <span class="hljs-keyword">from</span> <span class="hljs-string">'@testing-library/user-event'</span>;
<span class="hljs-keyword">import</span> { LoginForm } <span class="hljs-keyword">from</span> <span class="hljs-string">'./LoginForm'</span>;

describe(<span class="hljs-string">'LoginForm Component'</span>, <span class="hljs-function">() =&gt;</span> {
  it(<span class="hljs-string">'should render email and password inputs'</span>, <span class="hljs-function">() =&gt;</span> {
    render(<span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">LoginForm</span> <span class="hljs-attr">onSubmit</span>=<span class="hljs-string">{()</span> =&gt;</span> {}} /&gt;</span>);

    expect(screen.getByLabelText(<span class="hljs-regexp">/email/i</span>)).toBeInTheDocument();
    expect(screen.getByLabelText(<span class="hljs-regexp">/password/i</span>)).toBeInTheDocument();
  });

  it(<span class="hljs-string">'should update input values when user types'</span>, <span class="hljs-keyword">async</span> () =&gt; {
    <span class="hljs-keyword">const</span> user = userEvent.setup();
    render(<span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">LoginForm</span> <span class="hljs-attr">onSubmit</span>=<span class="hljs-string">{()</span> =&gt;</span> {}} /&gt;</span>);

    <span class="hljs-keyword">const</span> emailInput = screen.getByLabelText(<span class="hljs-regexp">/email/i</span>);
    <span class="hljs-keyword">const</span> passwordInput = screen.getByLabelText(<span class="hljs-regexp">/password/i</span>);

    <span class="hljs-keyword">await</span> user.type(emailInput, <span class="hljs-string">'test@example.com'</span>);
    <span class="hljs-keyword">await</span> user.type(passwordInput, <span class="hljs-string">'password123'</span>);

    expect(emailInput).toHaveValue(<span class="hljs-string">'test@example.com'</span>);
    expect(passwordInput).toHaveValue(<span class="hljs-string">'password123'</span>);
  });

  it(<span class="hljs-string">'should show error when form is submitted empty'</span>, <span class="hljs-keyword">async</span> () =&gt; {
    <span class="hljs-keyword">const</span> user = userEvent.setup();
    <span class="hljs-keyword">const</span> mockSubmit = vi.fn();
    render(<span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">LoginForm</span> <span class="hljs-attr">onSubmit</span>=<span class="hljs-string">{mockSubmit}</span> /&gt;</span></span>);

    <span class="hljs-keyword">const</span> submitButton = screen.getByRole(<span class="hljs-string">'button'</span>, { <span class="hljs-attr">name</span>: <span class="hljs-regexp">/log in/i</span> });
    <span class="hljs-keyword">await</span> user.click(submitButton);

    expect(screen.getByRole(<span class="hljs-string">'alert'</span>)).toHaveTextContent(<span class="hljs-string">'Both fields are required'</span>);
    expect(mockSubmit).not.toHaveBeenCalled();
  });

  it(<span class="hljs-string">'should call onSubmit with form data when valid'</span>, <span class="hljs-keyword">async</span> () =&gt; {
    <span class="hljs-keyword">const</span> user = userEvent.setup();
    <span class="hljs-keyword">const</span> mockSubmit = vi.fn();
    render(<span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">LoginForm</span> <span class="hljs-attr">onSubmit</span>=<span class="hljs-string">{mockSubmit}</span> /&gt;</span></span>);

    <span class="hljs-keyword">await</span> user.type(screen.getByLabelText(<span class="hljs-regexp">/email/i</span>), <span class="hljs-string">'test@example.com'</span>);
    <span class="hljs-keyword">await</span> user.type(screen.getByLabelText(<span class="hljs-regexp">/password/i</span>), <span class="hljs-string">'password123'</span>);
    <span class="hljs-keyword">await</span> user.click(screen.getByRole(<span class="hljs-string">'button'</span>, { <span class="hljs-attr">name</span>: <span class="hljs-regexp">/log in/i</span> }));

    expect(mockSubmit).toHaveBeenCalledWith({
      <span class="hljs-attr">email</span>: <span class="hljs-string">'test@example.com'</span>,
      <span class="hljs-attr">password</span>: <span class="hljs-string">'password123'</span>,
    });
  });
});
</code></pre>
<p>The LoginForm tests similarly use <code>render</code> and <code>screen</code> to interact with the component. We use <code>screen.getByLabelText(/email/i)</code> and <code>screen.getByLabelText(/password/i)</code> to find the input fields by their associated labels, mimicking how users identify form fields.</p>
<p>To simulate typing, we use <code>await user.type(input, text)</code>, which sends real keyboard events to the input (via user-event). After typing, we assert the input’s value with <code>expect(input).toHaveValue(...)</code> (a custom matcher from jest-dom).</p>
<p>When submitting the form empty, clicking the <strong>Log In</strong> button triggers the form’s validation and displays an error message. We find this error by querying <code>getByRole('alert')</code> and check its text content. We also assert that the mock <code>onSubmit</code> handler was <em>not</em> called.</p>
<p>In the valid submission test, we fill both fields and click <strong>Log In</strong>; then <code>expect(mockSubmit).toHaveBeenCalledWith({...})</code> verifies the submit handler received the correct <code>{ email, password }</code> object.</p>
<p>These tests focus on user actions and outcomes: typing and clicking drive the form logic, and our assertions confirm the expected outputs (visible error text or the callback arguments).</p>
<h2 id="heading-how-to-test-custom-hooks">How to Test Custom Hooks</h2>
<p>Custom hooks encapsulate reusable logic, and they need testing just like components. React Testing Library provides a <code>renderHook</code> function specifically for this purpose.</p>
<h3 id="heading-creating-and-testing-a-custom-hook">Creating and Testing a Custom Hook</h3>
<p>Create a custom hook <code>useFetch.js</code>:</p>
<pre><code class="lang-javascript"><span class="hljs-keyword">import</span> { useState, useEffect } <span class="hljs-keyword">from</span> <span class="hljs-string">'react'</span>;

<span class="hljs-keyword">export</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">useFetch</span>(<span class="hljs-params">url</span>) </span>{
  <span class="hljs-keyword">const</span> [data, setData] = useState(<span class="hljs-literal">null</span>);
  <span class="hljs-keyword">const</span> [loading, setLoading] = useState(<span class="hljs-literal">true</span>);
  <span class="hljs-keyword">const</span> [error, setError] = useState(<span class="hljs-literal">null</span>);

  useEffect(<span class="hljs-function">() =&gt;</span> {
    <span class="hljs-keyword">const</span> fetchData = <span class="hljs-keyword">async</span> () =&gt; {
      <span class="hljs-keyword">try</span> {
        setLoading(<span class="hljs-literal">true</span>);
        <span class="hljs-keyword">const</span> response = <span class="hljs-keyword">await</span> fetch(url);

        <span class="hljs-keyword">if</span> (!response.ok) {
          <span class="hljs-keyword">throw</span> <span class="hljs-keyword">new</span> <span class="hljs-built_in">Error</span>(<span class="hljs-string">'Network response was not ok'</span>);
        }

        <span class="hljs-keyword">const</span> json = <span class="hljs-keyword">await</span> response.json();
        setData(json);
        setError(<span class="hljs-literal">null</span>);
      } <span class="hljs-keyword">catch</span> (err) {
        setError(err.message);
        setData(<span class="hljs-literal">null</span>);
      } <span class="hljs-keyword">finally</span> {
        setLoading(<span class="hljs-literal">false</span>);
      }
    };

    fetchData();
  }, [url]);

  <span class="hljs-keyword">return</span> { data, loading, error };
}
</code></pre>
<p>Create the test file <code>useFetch.test.js</code>:</p>
<pre><code class="lang-javascript"><span class="hljs-keyword">import</span> { renderHook, waitFor } <span class="hljs-keyword">from</span> <span class="hljs-string">'@testing-library/react'</span>;
<span class="hljs-keyword">import</span> { useFetch } <span class="hljs-keyword">from</span> <span class="hljs-string">'./useFetch'</span>;

describe(<span class="hljs-string">'useFetch Hook'</span>, <span class="hljs-function">() =&gt;</span> {
  beforeEach(<span class="hljs-function">() =&gt;</span> {
    <span class="hljs-built_in">global</span>.fetch = vi.fn();
  });

  afterEach(<span class="hljs-function">() =&gt;</span> {
    vi.restoreAllMocks();
  });

  it(<span class="hljs-string">'should return loading state initially'</span>, <span class="hljs-function">() =&gt;</span> {
    <span class="hljs-built_in">global</span>.fetch.mockImplementation(<span class="hljs-function">() =&gt;</span> 
      <span class="hljs-built_in">Promise</span>.resolve({
        <span class="hljs-attr">ok</span>: <span class="hljs-literal">true</span>,
        <span class="hljs-attr">json</span>: <span class="hljs-keyword">async</span> () =&gt; ({ <span class="hljs-attr">data</span>: <span class="hljs-string">'test'</span> }),
      })
    );

    <span class="hljs-keyword">const</span> { result } = renderHook(<span class="hljs-function">() =&gt;</span> useFetch(<span class="hljs-string">'https://api.example.com/data'</span>));

    expect(result.current.loading).toBe(<span class="hljs-literal">true</span>);
    expect(result.current.data).toBe(<span class="hljs-literal">null</span>);
    expect(result.current.error).toBe(<span class="hljs-literal">null</span>);
  });

  it(<span class="hljs-string">'should return data when fetch succeeds'</span>, <span class="hljs-keyword">async</span> () =&gt; {
    <span class="hljs-keyword">const</span> mockData = { <span class="hljs-attr">id</span>: <span class="hljs-number">1</span>, <span class="hljs-attr">title</span>: <span class="hljs-string">'Test Post'</span> };

    <span class="hljs-built_in">global</span>.fetch.mockImplementation(<span class="hljs-function">() =&gt;</span>
      <span class="hljs-built_in">Promise</span>.resolve({
        <span class="hljs-attr">ok</span>: <span class="hljs-literal">true</span>,
        <span class="hljs-attr">json</span>: <span class="hljs-keyword">async</span> () =&gt; mockData,
      })
    );

    <span class="hljs-keyword">const</span> { result } = renderHook(<span class="hljs-function">() =&gt;</span> useFetch(<span class="hljs-string">'https://api.example.com/posts/1'</span>));

    <span class="hljs-keyword">await</span> waitFor(<span class="hljs-function">() =&gt;</span> expect(result.current.loading).toBe(<span class="hljs-literal">false</span>));

    expect(result.current.data).toEqual(mockData);
    expect(result.current.error).toBe(<span class="hljs-literal">null</span>);
  });

  it(<span class="hljs-string">'should return error when fetch fails'</span>, <span class="hljs-keyword">async</span> () =&gt; {
    <span class="hljs-built_in">global</span>.fetch.mockImplementation(<span class="hljs-function">() =&gt;</span>
      <span class="hljs-built_in">Promise</span>.resolve({
        <span class="hljs-attr">ok</span>: <span class="hljs-literal">false</span>,
      })
    );

    <span class="hljs-keyword">const</span> { result } = renderHook(<span class="hljs-function">() =&gt;</span> useFetch(<span class="hljs-string">'https://api.example.com/posts/1'</span>));

    <span class="hljs-keyword">await</span> waitFor(<span class="hljs-function">() =&gt;</span> expect(result.current.loading).toBe(<span class="hljs-literal">false</span>));

    expect(result.current.data).toBe(<span class="hljs-literal">null</span>);
    expect(result.current.error).toBe(<span class="hljs-string">'Network response was not ok'</span>);
  });
});
</code></pre>
<p>The <code>renderHook</code> function from React Testing Library renders custom hooks, and <code>waitFor</code> is used to wait for asynchronous state updates in the hook.</p>
<h2 id="heading-how-to-mock-api-calls">How to Mock API Calls</h2>
<p>When testing components that make API calls, you don't want to hit real endpoints. Mocking ensures your tests are fast, reliable, and don't depend on network conditions.</p>
<h3 id="heading-mocking-with-vitest">Mocking with Vitest</h3>
<p>Vitest doesn’t auto-mock modules like Jest does, so you need to manually mock them. Let's see how to mock an Axios call.</p>
<p>Create a <code>PostsList.jsx</code> component:</p>
<pre><code class="lang-javascript"><span class="hljs-keyword">import</span> { useState, useEffect } <span class="hljs-keyword">from</span> <span class="hljs-string">'react'</span>;
<span class="hljs-keyword">import</span> axios <span class="hljs-keyword">from</span> <span class="hljs-string">'axios'</span>;

<span class="hljs-keyword">export</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">PostsList</span>(<span class="hljs-params"></span>) </span>{
  <span class="hljs-keyword">const</span> [posts, setPosts] = useState([]);
  <span class="hljs-keyword">const</span> [loading, setLoading] = useState(<span class="hljs-literal">true</span>);
  <span class="hljs-keyword">const</span> [error, setError] = useState(<span class="hljs-literal">null</span>);

  useEffect(<span class="hljs-function">() =&gt;</span> {
    <span class="hljs-keyword">const</span> fetchPosts = <span class="hljs-keyword">async</span> () =&gt; {
      <span class="hljs-keyword">try</span> {
        <span class="hljs-keyword">const</span> response = <span class="hljs-keyword">await</span> axios.get(<span class="hljs-string">'https://api.example.com/posts'</span>);
        setPosts(response.data);
      } <span class="hljs-keyword">catch</span> (err) {
        setError(err.message);
      } <span class="hljs-keyword">finally</span> {
        setLoading(<span class="hljs-literal">false</span>);
      }
    };

    fetchPosts();
  }, []);

  <span class="hljs-keyword">if</span> (loading) <span class="hljs-keyword">return</span> <span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">p</span>&gt;</span>Loading...<span class="hljs-tag">&lt;/<span class="hljs-name">p</span>&gt;</span></span>;
  <span class="hljs-keyword">if</span> (error) <span class="hljs-keyword">return</span> <span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">p</span>&gt;</span>Error: {error}<span class="hljs-tag">&lt;/<span class="hljs-name">p</span>&gt;</span></span>;

  <span class="hljs-keyword">return</span> (
    <span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">ul</span>&gt;</span>
      {posts.map((post) =&gt; (
        <span class="hljs-tag">&lt;<span class="hljs-name">li</span> <span class="hljs-attr">key</span>=<span class="hljs-string">{post.id}</span>&gt;</span>{post.title}<span class="hljs-tag">&lt;/<span class="hljs-name">li</span>&gt;</span>
      ))}
    <span class="hljs-tag">&lt;/<span class="hljs-name">ul</span>&gt;</span></span>
  );
}
</code></pre>
<p>Create the test file <code>PostsList.test.jsx</code>:</p>
<pre><code class="lang-javascript"><span class="hljs-keyword">import</span> { render, screen, waitFor } <span class="hljs-keyword">from</span> <span class="hljs-string">'@testing-library/react'</span>;
<span class="hljs-keyword">import</span> axios <span class="hljs-keyword">from</span> <span class="hljs-string">'axios'</span>;
<span class="hljs-keyword">import</span> { PostsList } <span class="hljs-keyword">from</span> <span class="hljs-string">'./PostsList'</span>;

vi.mock(<span class="hljs-string">'axios'</span>);

describe(<span class="hljs-string">'PostsList Component'</span>, <span class="hljs-function">() =&gt;</span> {
  beforeEach(<span class="hljs-function">() =&gt;</span> {
    vi.clearAllMocks();
  });

  it(<span class="hljs-string">'should display loading state initially'</span>, <span class="hljs-function">() =&gt;</span> {
    axios.get.mockImplementation(<span class="hljs-function">() =&gt;</span> <span class="hljs-keyword">new</span> <span class="hljs-built_in">Promise</span>(<span class="hljs-function">() =&gt;</span> {}));
    render(<span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">PostsList</span> /&gt;</span></span>);

    expect(screen.getByText(<span class="hljs-string">'Loading...'</span>)).toBeInTheDocument();
  });

  it(<span class="hljs-string">'should display posts when API call succeeds'</span>, <span class="hljs-keyword">async</span> () =&gt; {
    <span class="hljs-keyword">const</span> mockPosts = [
      { <span class="hljs-attr">id</span>: <span class="hljs-number">1</span>, <span class="hljs-attr">title</span>: <span class="hljs-string">'First Post'</span> },
      { <span class="hljs-attr">id</span>: <span class="hljs-number">2</span>, <span class="hljs-attr">title</span>: <span class="hljs-string">'Second Post'</span> },
    ];

    axios.get.mockResolvedValue({ <span class="hljs-attr">data</span>: mockPosts });
    render(<span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">PostsList</span> /&gt;</span></span>);

    <span class="hljs-keyword">await</span> waitFor(<span class="hljs-function">() =&gt;</span> {
      expect(screen.queryByText(<span class="hljs-string">'Loading...'</span>)).not.toBeInTheDocument();
    });

    expect(screen.getByText(<span class="hljs-string">'First Post'</span>)).toBeInTheDocument();
    expect(screen.getByText(<span class="hljs-string">'Second Post'</span>)).toBeInTheDocument();
  });

  it(<span class="hljs-string">'should display error when API call fails'</span>, <span class="hljs-keyword">async</span> () =&gt; {
    axios.get.mockRejectedValue(<span class="hljs-keyword">new</span> <span class="hljs-built_in">Error</span>(<span class="hljs-string">'Network error'</span>));
    render(<span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">PostsList</span> /&gt;</span></span>);

    <span class="hljs-keyword">await</span> waitFor(<span class="hljs-function">() =&gt;</span> {
      expect(screen.queryByText(<span class="hljs-string">'Loading...'</span>)).not.toBeInTheDocument();
    });

    expect(screen.getByText(<span class="hljs-regexp">/error/i</span>)).toBeInTheDocument();
  });
});
</code></pre>
<p>In these tests, we verify specific UI states: the “loading” test checks that a loading indicator shows while data is being fetched, the “success” test confirms that post items render when the API returns data, and the “error” test makes sure an error message appears if the call fails.</p>
<p>We mock Axios by calling <code>vi.mock('axios')</code> and then using methods like <code>mockResolvedValue(...)</code> on <code>axios.get</code> to simulate a successful response (and <code>mockRejectedValue(...)</code> to simulate a failure). This kind of mocking isolates our tests from real network calls (making them fast and reliable) and lets us control exactly what data or error the hook receives.</p>
<p>We use <code>await waitFor(...)</code> to pause the test until those asynchronous updates complete before making assertions. Finally, we use <code>screen.getByText(...)</code> to find elements that should be present (it will throw an error if they’re missing) and <code>screen.queryByText(...)</code> to check that elements aren’t present (it returns null if the element is not in the DOM).</p>
<h3 id="heading-mocking-specific-module-functions">Mocking Specific Module Functions</h3>
<p>Sometimes you only want to mock specific functions while keeping the rest of a module's behaviour intact. Here's how to do that:</p>
<pre><code class="lang-javascript">vi.mock(<span class="hljs-string">'date-fns'</span>, <span class="hljs-keyword">async</span> () =&gt; {
  <span class="hljs-keyword">const</span> original = <span class="hljs-keyword">await</span> vi.importActual(<span class="hljs-string">'date-fns'</span>);
  <span class="hljs-keyword">return</span> {
    ...original,
    <span class="hljs-attr">format</span>: vi.fn(<span class="hljs-function">() =&gt;</span> <span class="hljs-string">'2025-01-01'</span>),
  };
});
</code></pre>
<p>In Vitest, you use <code>vi.importActual</code> to retain all original methods while mocking only the <code>format</code> method.</p>
<h2 id="heading-best-practices-for-testing-react-components">Best Practices for Testing React Components</h2>
<p>Now that you know how to write tests, let's talk about how to write good tests.</p>
<h3 id="heading-test-user-behaviour-not-implementation">Test User Behaviour, Not Implementation</h3>
<p>Focus on testing what users see and do, not internal component details. If you refactor your component's implementation without changing its behaviour, your tests shouldn't break.</p>
<p><strong>Bad test (testing implementation):</strong></p>
<pre><code class="lang-javascript">it(<span class="hljs-string">'should set isOpen state to true'</span>, <span class="hljs-function">() =&gt;</span> {
  <span class="hljs-keyword">const</span> { result } = renderHook(<span class="hljs-function">() =&gt;</span> useState(<span class="hljs-literal">false</span>));
  <span class="hljs-comment">// Testing internal state directly</span>
});
</code></pre>
<p><strong>Good test (testing behaviour):</strong></p>
<pre><code class="lang-javascript">it(<span class="hljs-string">'should show menu when button is clicked'</span>, <span class="hljs-keyword">async</span> () =&gt; {
  <span class="hljs-keyword">const</span> user = userEvent.setup();
  render(<span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">Menu</span> /&gt;</span></span>);

  <span class="hljs-keyword">await</span> user.click(screen.getByRole(<span class="hljs-string">'button'</span>, { <span class="hljs-attr">name</span>: <span class="hljs-regexp">/menu/i</span> }));
  expect(screen.getByRole(<span class="hljs-string">'navigation'</span>)).toBeVisible();
});
</code></pre>
<h3 id="heading-use-accessible-queries">Use Accessible Queries</h3>
<p>React Testing Library encourages you to query elements the way users do. Prefer queries that mirror user interaction:</p>
<ol>
<li><p><code>getByRole</code> (best for interactive elements)</p>
</li>
<li><p><code>getByLabelText</code> (for form fields)</p>
</li>
<li><p><code>getByPlaceholderText</code></p>
</li>
<li><p><code>getByText</code></p>
</li>
<li><p><code>getByTestId</code> (last resort)</p>
</li>
</ol>
<h3 id="heading-keep-tests-simple-and-focused">Keep Tests Simple and Focused</h3>
<p>Each test should verify one thing. If your test needs a lot of setup or has many assertions, consider splitting it into multiple tests.</p>
<h3 id="heading-clean-up-between-tests">Clean Up Between Tests</h3>
<p>Use <code>afterEach</code> to clean up the DOM after each test run, ensuring tests don't interfere with each other. This is already handled if you followed the setup steps earlier.</p>
<h3 id="heading-use-descriptive-test-names">Use Descriptive Test Names</h3>
<p>Test names should clearly describe what they're testing and what the expected outcome is.</p>
<p>Good test names:</p>
<pre><code class="lang-javascript">it(<span class="hljs-string">'should display error message when form is submitted empty'</span>);
it(<span class="hljs-string">'should call onSubmit with email and password when form is valid'</span>);
it(<span class="hljs-string">'should disable submit button while request is pending'</span>);
</code></pre>
<h3 id="heading-mock-external-dependencies">Mock External Dependencies</h3>
<p>Always mock API calls, timers, and other external dependencies. Your tests should be isolated and not depend on network conditions or external services.</p>
<h2 id="heading-conclusion">Conclusion</h2>
<p>Now, you have learned how to set up Vitest in a React project and write effective tests for components, user interactions, custom hooks, and API calls. Vitest provides a powerful and efficient way to test React applications, especially when combined with modern tools like Vite.</p>
<p>Testing is about building confidence in your code, documenting expected behaviour, and enabling safe refactoring. Vitest's speed makes testing feel less like a chore and more like a natural part of development.</p>
<p>Start small. Add tests for critical user flows. Test the components that change frequently. As you build the habit, you will find that tests actually make development faster, not slower. The code will still be there tomorrow. But the bugs you catch today won't be.</p>
 ]]>
                </content:encoded>
            </item>
        
    </channel>
</rss>
