<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Praise Agbabiaka</title>
    <description>The latest articles on DEV Community by Praise Agbabiaka (@praiztech).</description>
    <link>https://dev.to/praiztech</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F1177654%2F3dda3c81-77fc-4fc1-8a05-93d4488c0e1e.jpg</url>
      <title>DEV Community: Praise Agbabiaka</title>
      <link>https://dev.to/praiztech</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/praiztech"/>
    <language>en</language>
    <item>
      <title>Explicit accessibility contracts make React components more portable</title>
      <dc:creator>Praise Agbabiaka</dc:creator>
      <pubDate>Sun, 13 Sep 2026 22:23:37 +0000</pubDate>
      <link>https://dev.to/praiztech/explicit-accessibility-contracts-make-react-components-more-portable-p2b</link>
      <guid>https://dev.to/praiztech/explicit-accessibility-contracts-make-react-components-more-portable-p2b</guid>
      <description>&lt;p&gt;A component that works in a React app doesn't automatically work when you reuse it elsewhere in the React ecosystem. I moved a design-system layout component from a React app into a Next project and it broke. The problem wasn't that Next couldn't render the component; it was that the component assumed a particular ownership and composition model. That assumption affected its accessibility too.&lt;/p&gt;

&lt;h2&gt;
  
  
  The component and its contract
&lt;/h2&gt;

&lt;p&gt;The component was a layout component. It provided a skip link, a header with a main menu, and a &lt;code&gt;&amp;lt;main&amp;gt;&lt;/code&gt; containing the page title and body.&lt;/p&gt;

&lt;p&gt;Its accessibility depended on a concrete relationship. The skip link targeted the page's &lt;code&gt;h1&lt;/code&gt;, the &lt;code&gt;h1&lt;/code&gt; had a matching &lt;code&gt;id&lt;/code&gt; and &lt;code&gt;tabIndex={-1}&lt;/code&gt; so it could receive programmatic focus, and the heading sat inside &lt;code&gt;&amp;lt;main&amp;gt;&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The skip link was intentionally targeting the page heading rather than the &lt;code&gt;&amp;lt;main&amp;gt;&lt;/code&gt; element. The desired focus destination was the beginning of the page's meaningful content, where the page title provided an immediate orientation point.&lt;/p&gt;

&lt;p&gt;Activate the skip link and focus lands on the page title, past the persistent header and menu. A clear page-level heading, the correct landmark, and focus where it should be.&lt;/p&gt;

&lt;p&gt;That worked, and it kept working until I tried to reuse the component.&lt;/p&gt;

&lt;h2&gt;
  
  
  Reuse exposed the composition assumption
&lt;/h2&gt;

&lt;p&gt;In the original React app, the layout owned everything in one composition. The persistent chrome, skip link, header, and &lt;code&gt;&amp;lt;main&amp;gt;&lt;/code&gt;, and the per-page content, the &lt;code&gt;h1&lt;/code&gt; and body, lived together.&lt;/p&gt;

&lt;p&gt;That is perfectly reasonable when the application controls how the whole tree is composed.&lt;/p&gt;

&lt;p&gt;Next doesn't compose that way. In Next's App Router, the persistent layout and the route-specific page have different ownership and lifecycle boundaries. The layout persists while the page content changes between routes. The layout receives that route-specific content through &lt;code&gt;children&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;So I couldn't use the component unchanged as the Next layout because it assumed it owned both sides of that boundary.&lt;/p&gt;

&lt;p&gt;Trying to work around that assumption created an awkward choice. Either the persistent layout had to know about the page-specific heading, or the page content had to somehow reach back into the layout to establish the accessibility relationship.&lt;/p&gt;

&lt;p&gt;Neither was a good component contract.&lt;/p&gt;

&lt;p&gt;The problem wasn't simply that the component was "incompatible with Next." Its composition assumptions didn't survive a different rendering model.&lt;/p&gt;

&lt;p&gt;And when composition assumptions include accessibility relationships, those relationships can break along with the composition.&lt;/p&gt;

&lt;h2&gt;
  
  
  The contract has to become an interface
&lt;/h2&gt;

&lt;p&gt;The fix was to stop treating the layout as one indivisible thing.&lt;/p&gt;

&lt;p&gt;It became two components: a shell and the content that fills it.&lt;/p&gt;

&lt;p&gt;The shell owns the skip link, the header, and the &lt;code&gt;&amp;lt;main&amp;gt;&lt;/code&gt; landmark. The content owns the page heading and body.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Shell: owns the skip link and the &amp;lt;main&amp;gt; landmark, and renders a slot.&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;Layout&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="nx"&gt;children&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;mainMenu&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;headerActions&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;headingId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;content-heading&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt; &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;styles&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;layout&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;SkipLink&lt;/span&gt;
      &lt;span class="na"&gt;label&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"Skip to Content"&lt;/span&gt;
      &lt;span class="na"&gt;targetId&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;headingId&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
      &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;styles&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;skip&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;

    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;header&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;mainMenu&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
      &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;headerActions&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;header&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;

    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;main&lt;/span&gt; &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;styles&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;main&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;children&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;main&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// Content: owns the focusable heading the skip link resolves to.&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;PageContent&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="nx"&gt;pageTitle&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;children&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;headingId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;content-heading&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt; &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;styles&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;content_container&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;h1&lt;/span&gt;
      &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;headingId&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
      &lt;span class="na"&gt;tabIndex&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
      &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;styles&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;content_heading&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;pageTitle&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;h1&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;

    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt; &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;styles&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;content_body&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;children&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The important change isn't just that there are now two components. It's that the boundary between them is explicit.&lt;/p&gt;

&lt;p&gt;The skip link needs a focus target.&lt;/p&gt;

&lt;p&gt;The focus target needs a stable identity.&lt;/p&gt;

&lt;p&gt;The target needs to be the page heading.&lt;/p&gt;

&lt;p&gt;And that heading needs to be inside the main content.&lt;/p&gt;

&lt;p&gt;Those are accessibility requirements of the composition, not implementation details hidden inside one component.&lt;/p&gt;

&lt;p&gt;The shared ID used to be a magic string hardcoded in both places. Now that ID represents the interface between the two halves, so it belongs in the API.&lt;/p&gt;

&lt;p&gt;Both components use the same default:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="nx"&gt;headingId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;content-heading&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;which means they line up without additional configuration. If a consumer needs a different ID, it can provide one to both components.&lt;/p&gt;

&lt;p&gt;That makes the relationship visible rather than relying on a convention that consumers have to discover.&lt;/p&gt;

&lt;p&gt;There is still a limitation: an API that exposes a contract doesn't necessarily enforce it. A consumer can pass different IDs to the two components, or render multiple instances with the same default ID.&lt;/p&gt;

&lt;p&gt;A more sophisticated design-system implementation could enforce the relationship structurally, for example, by generating the ID in the shell and providing it to the content through context. But even when the contract remains a consumer responsibility, making it explicit is a significant improvement over hiding it in two components that happen to agree on a magic string.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why this makes the component more portable
&lt;/h2&gt;

&lt;p&gt;React-based frameworks ultimately compose components into a rendered DOM tree, but they differ in how they establish the boundaries between persistent UI and route-specific content.&lt;/p&gt;

&lt;p&gt;That's where the refactoring helps.&lt;/p&gt;

&lt;p&gt;In a plain React application, you can compose the components yourself:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Layout&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;PageContent&lt;/span&gt; &lt;span class="na"&gt;pageTitle&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"Dashboard"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
    ...
  &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;PageContent&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;Layout&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In Next's App Router, the persistent shell can live in &lt;code&gt;layout.tsx&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Layout&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@/design-system&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;RootLayout&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;children&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Layout&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;children&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;Layout&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;while the route-specific content lives in &lt;code&gt;page.tsx&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;Page&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;PageContent&lt;/span&gt; &lt;span class="na"&gt;pageTitle&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"Dashboard"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      ...
    &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;PageContent&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The same separation works with React Router. A parent route can own the persistent shell and render an &lt;code&gt;&amp;lt;Outlet /&amp;gt;&lt;/code&gt;, while the child route provides the page-specific content.&lt;/p&gt;

&lt;p&gt;The composition mechanism is different, but the boundary is the same: the shell owns the persistent structure, and the route supplies the content that fills it.&lt;/p&gt;

&lt;p&gt;That is the boundary the original component was missing. Once the component is split along that boundary, the framework can decide how the content gets there without changing the accessibility relationship between the skip link and the page heading.&lt;/p&gt;

&lt;p&gt;The accessibility travels with the composition. The skip link still resolves to the heading, whether the two halves are composed directly in React, through &lt;code&gt;children&lt;/code&gt; in Next, or through an &lt;code&gt;&amp;lt;Outlet /&amp;gt;&lt;/code&gt; in React Router.&lt;/p&gt;

&lt;h2&gt;
  
  
  Accessibility is part of the composition contract
&lt;/h2&gt;

&lt;p&gt;This is the part that is easy to miss when designing reusable components.&lt;/p&gt;

&lt;p&gt;Accessibility isn't always something contained entirely within a single component.&lt;/p&gt;

&lt;p&gt;Sometimes it is a relationship between components.&lt;/p&gt;

&lt;p&gt;A label and its form control.&lt;/p&gt;

&lt;p&gt;A button and the dialog it opens.&lt;/p&gt;

&lt;p&gt;A tab and its tabpanel.&lt;/p&gt;

&lt;p&gt;A skip link and the content it moves focus to.&lt;/p&gt;

&lt;p&gt;When two components participate in one of those relationships, the relationship is part of their API whether the component author documents it or not.&lt;/p&gt;

&lt;p&gt;If it remains implicit, reuse becomes fragile.&lt;/p&gt;

&lt;p&gt;A consumer has to know that one component renders an element with a particular ID. They have to know that another component expects that ID. They have to preserve the relationship when they change the composition.&lt;/p&gt;

&lt;p&gt;Making the relationship explicit turns an implementation detail into a contract.&lt;/p&gt;

&lt;p&gt;And once the contract is explicit, the framework has more freedom to determine how the components are composed.&lt;/p&gt;

&lt;p&gt;That's what makes the components portable.&lt;/p&gt;

&lt;h2&gt;
  
  
  The point
&lt;/h2&gt;

&lt;p&gt;The lesson isn't that React components need special versions for every framework.&lt;/p&gt;

&lt;p&gt;It's that reusable components shouldn't unnecessarily assume who owns the composition seam.&lt;/p&gt;

&lt;p&gt;A persistent shell and route-specific content may be composed directly in one React application, through &lt;code&gt;children&lt;/code&gt; in Next's App Router, or through an &lt;code&gt;&amp;lt;Outlet /&amp;gt;&lt;/code&gt; in React Router. Those mechanisms differ, but the underlying architectural relationship is similar.&lt;/p&gt;

&lt;p&gt;Design the component around that relationship rather than around one particular way of composing it.&lt;/p&gt;

&lt;p&gt;And make the accessibility relationships explicit while you're doing it.&lt;/p&gt;

&lt;p&gt;A component's accessibility isn't separate from its reusability. If its accessible behavior depends on assumptions about how its pieces are composed, those assumptions are part of its contract.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A component is only as portable as the accessibility contract that survives being composed differently.&lt;/strong&gt;&lt;/p&gt;

</description>
      <category>a11y</category>
      <category>react</category>
      <category>nextjs</category>
      <category>designsystems</category>
    </item>
    <item>
      <title>A design system fixes accessibility once, if it's the only source</title>
      <dc:creator>Praise Agbabiaka</dc:creator>
      <pubDate>Sun, 30 Aug 2026 22:26:01 +0000</pubDate>
      <link>https://dev.to/praiztech/a-design-system-fixes-accessibility-once-if-its-the-only-source-1i07</link>
      <guid>https://dev.to/praiztech/a-design-system-fixes-accessibility-once-if-its-the-only-source-1i07</guid>
      <description>&lt;p&gt;Do accessibility remediation long enough and you arrive at the same conclusion every time. Stop fixing instances. Fix the thing that generates them.&lt;/p&gt;

&lt;p&gt;The natural end of that logic is the design system. Fix the button component once, and every button in the product inherits the fix. One change, every instance. It's the most leveraged accessibility work you can do.&lt;/p&gt;

&lt;p&gt;It also rests on an assumption that can quietly fail.&lt;/p&gt;

&lt;h2&gt;
  
  
  "Fixed at the source" assumes there is one source
&lt;/h2&gt;

&lt;p&gt;When you fix the component, you fix every instance that goes through the component. That's the promise, and it's real.&lt;/p&gt;

&lt;p&gt;But somewhere in the codebase there may be two or three buttons that don't go through it. A div with a click handler someone wrote in a hurry. A one-off in a corner of the app the component never reached. A copy made before the component existed and was never migrated.&lt;/p&gt;

&lt;p&gt;The canonical component gets corrected. The hand-rolled copies don't, because they were never using the source.&lt;/p&gt;

&lt;p&gt;So your fix at the source covers the instances that use it and misses the ones that bypass it. The re-audit passes on the pages built from the component and fails on the pages that reinvented it. Same fix, same product, different result, and nothing in the component diff tells you why.&lt;/p&gt;

&lt;h2&gt;
  
  
  The closing step isn't diffing the component
&lt;/h2&gt;

&lt;p&gt;It's grepping the pattern.&lt;/p&gt;

&lt;p&gt;The real end of a source-level fix isn't confirming the component is right. It's searching the codebase for every place the pattern was reimplemented, and deciding, for each one, whether to fold it back into the component or fix it where it sits.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A source-level fix isn't done when the source is right. It's done when nothing bypasses it.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  This is the real argument for design systems, and the real work of one
&lt;/h2&gt;

&lt;p&gt;A design system is the honest answer to "fix the system, not the instance." It's the one place an accessible pattern can live so that using it is the default.&lt;/p&gt;

&lt;p&gt;But a design system isn't a folder of accessible components. It's a commitment that the accessible pattern is the canonical path, and the easiest one.&lt;/p&gt;

&lt;p&gt;Those two conditions carry all the weight. If the accessible menu is harder to adopt than hand-rolling a dropdown, people hand-roll the dropdown, and you are back to instances, now scattered across a codebase that looks like it has a design system. The component exists. It just isn't the source.&lt;/p&gt;

&lt;h2&gt;
  
  
  So it's two disciplines, not one
&lt;/h2&gt;

&lt;p&gt;Building the pattern accessibly is the part everyone pictures. It's also the smaller part.&lt;/p&gt;

&lt;p&gt;Making it the single source is the work. That means making it the path of least resistance, so bypassing it is harder than adopting it. It means catching the reinventions in code review, before they become the copies nobody knows about. And it means, every so often, grepping for the pattern and folding the strays back in.&lt;/p&gt;

&lt;p&gt;An accessible component fixes every instance that uses it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The work is making sure every instance uses it.&lt;/strong&gt;&lt;/p&gt;

</description>
      <category>a11y</category>
      <category>webdev</category>
      <category>designsystems</category>
      <category>architecture</category>
    </item>
    <item>
      <title>Accessibility remediation is software engineering, not a checklist</title>
      <dc:creator>Praise Agbabiaka</dc:creator>
      <pubDate>Sun, 23 Aug 2026 22:18:46 +0000</pubDate>
      <link>https://dev.to/praiztech/accessibility-remediation-is-software-engineering-not-a-checklist-3d64</link>
      <guid>https://dev.to/praiztech/accessibility-remediation-is-software-engineering-not-a-checklist-3d64</guid>
      <description>&lt;p&gt;An accessibility audit is invaluable. It identifies problems that might otherwise go unnoticed, provides a structured view of a product's accessibility, and gives a team a concrete starting point for remediation.&lt;/p&gt;

&lt;p&gt;But an audit report is a starting point, not a remediation plan.&lt;/p&gt;

&lt;p&gt;It's tempting to treat the findings as the job: work through the list, close each issue, ship. But a finding tells you what went wrong in the rendered experience. It doesn't necessarily tell you why it happened, where it should be fixed, or how to prevent it from happening again.&lt;/p&gt;

&lt;p&gt;That's where accessibility remediation becomes software engineering.&lt;/p&gt;

&lt;h2&gt;
  
  
  A finding is a symptom, not a location
&lt;/h2&gt;

&lt;p&gt;An audit tells you what is wrong with the output. It doesn't tell you where in the system to fix it.&lt;/p&gt;

&lt;p&gt;If the same failure shows up on ten pages, fixing ten pages may address the reported instances without addressing the cause. The more useful question is where the failure is produced.&lt;/p&gt;

&lt;p&gt;A shared component. A template. A design-system primitive. The content. A third-party script.&lt;/p&gt;

&lt;p&gt;The right fix often changes structure, not just an attribute.&lt;/p&gt;

&lt;p&gt;The problem isn't working through findings one by one. That's often necessary. The problem is treating each finding as an isolated task without asking what produced it and whether the fix will hold up.&lt;/p&gt;

&lt;p&gt;Fix only the output, and the failure can return the next time that output is generated. Fix the thing that generates it, and the fix has a much better chance of surviving the next change.&lt;/p&gt;

&lt;h2&gt;
  
  
  Remediation carries technical debt too
&lt;/h2&gt;

&lt;p&gt;There is always a faster path. A line of JavaScript that rewrites the DOM after render. A one-off ARIA patch. A duplicated component variant that's "accessible this time."&lt;/p&gt;

&lt;p&gt;Each can close a finding. Each can also leave the codebase worse: a script maintaining server-rendered markup indefinitely, conflicting ARIA, or a fix that breaks when the markup underneath it changes.&lt;/p&gt;

&lt;p&gt;None of these approaches are always wrong. Sometimes they are the right solution given the constraints of the system.&lt;/p&gt;

&lt;p&gt;But they should be deliberate engineering decisions.&lt;/p&gt;

&lt;p&gt;A remediation should leave the code in a better state, not simply move the problem somewhere harder to see.&lt;/p&gt;

&lt;h2&gt;
  
  
  Regression is part of the problem
&lt;/h2&gt;

&lt;p&gt;A fix applied only to the output is vulnerable to regression when that output is regenerated. Reuse a component in a new context. Copy a template. Take an upstream update. Onboard a developer who copies the existing pattern without knowing it's broken.&lt;/p&gt;

&lt;p&gt;A fix built into the system is more resilient. When correctness is built into the component, template, design system, or development process, it doesn't have to be re-decided every time the same pattern is used.&lt;/p&gt;

&lt;p&gt;That's the difference between fixing an instance and fixing the system that produced it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A fix that doesn't survive the next change was never really a fix.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  This is engineering, not a separate track
&lt;/h2&gt;

&lt;p&gt;I don't treat accessibility remediation as separate from software engineering.&lt;/p&gt;

&lt;p&gt;The requirement comes from the Web Content Accessibility Guidelines (WCAG). The failure surfaces in an audit. Deciding how to fix it is an engineering problem.&lt;/p&gt;

&lt;p&gt;It means understanding the architecture, the rendering model, the component boundaries, the dependencies, the upgrade path, the tests, and the people who maintain the code after you leave.&lt;/p&gt;

&lt;p&gt;There isn't always a universally correct code change. There is a correct engineering approach for the system in front of you.&lt;/p&gt;

&lt;p&gt;The same finding needs a different fix in a React design system than in a Liquid theme, and knowing which is the job.&lt;/p&gt;

&lt;h2&gt;
  
  
  Good remediation leaves the system better
&lt;/h2&gt;

&lt;p&gt;The measure of good remediation isn't simply whether the reported issues are gone. It's whether the software is better equipped to remain accessible after the work is finished.&lt;/p&gt;

&lt;p&gt;Can the next developer understand and maintain the fix? Does the component or design system make the accessible pattern the default path? Is the implementation resilient to platform and dependency updates? Can new features build on the same accessible foundation instead of recreating the problem?&lt;/p&gt;

&lt;p&gt;This is what makes accessibility remediation software engineering.&lt;/p&gt;

&lt;p&gt;You're not just changing markup until it passes a test. You're making decisions about architecture, abstractions, maintainability, dependencies, and how the software will evolve.&lt;/p&gt;

&lt;p&gt;The audit identifies the problem. The remediation is the engineering work of solving it well.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Closing a finding is the easy part. Fixing the system that produced it is the work.&lt;/strong&gt;&lt;/p&gt;

</description>
      <category>a11y</category>
      <category>webdev</category>
      <category>architecture</category>
      <category>remediation</category>
    </item>
    <item>
      <title>AI Can Find the Failure. Who Decides What the Fix Should Be?</title>
      <dc:creator>Praise Agbabiaka</dc:creator>
      <pubDate>Sun, 16 Aug 2026 23:14:58 +0000</pubDate>
      <link>https://dev.to/praiztech/ai-can-find-the-failure-who-decides-what-the-fix-should-be-18o5</link>
      <guid>https://dev.to/praiztech/ai-can-find-the-failure-who-decides-what-the-fix-should-be-18o5</guid>
      <description>&lt;p&gt;AI is changing accessibility remediation fast. It can find common accessibility failures, explain them, and increasingly write the fix. That's real, and it's useful.&lt;/p&gt;

&lt;p&gt;But finding a failure is not the same as deciding what the fix should be.&lt;/p&gt;

&lt;p&gt;The gap is easy to miss because it doesn't always produce an obviously wrong answer. Sometimes it produces a confident, plausible fix that is wrong for reasons that only become apparent when you understand the interaction in context.&lt;/p&gt;

&lt;h2&gt;
  
  
  A real example
&lt;/h2&gt;

&lt;p&gt;I recently worked through an accessible predictive search for a Shopify storefront with an AI assistant. A combobox with a listbox, screen-reader support, the usual.&lt;/p&gt;

&lt;p&gt;In places, it was genuinely good. It identified the right interaction pattern and the roles and interaction model it depends on. It reviewed the theme's existing JavaScript and caught real bugs, ones I'd have taken longer to find by hand.&lt;/p&gt;

&lt;p&gt;Keyboard navigation that didn't exist.&lt;/p&gt;

&lt;p&gt;An Escape handler that wiped the user's typed query.&lt;/p&gt;

&lt;p&gt;A live region toggled with &lt;code&gt;aria-hidden&lt;/code&gt; instead of being left in the page, so it announced inconsistently across screen readers.&lt;/p&gt;

&lt;p&gt;That's detection working well.&lt;/p&gt;

&lt;p&gt;Then it started proposing fixes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Plausible, and wrong
&lt;/h2&gt;

&lt;p&gt;It moved the search suggestions into a separate region, reached with the Tab key.&lt;/p&gt;

&lt;p&gt;Reasonable-looking. Also wrong.&lt;/p&gt;

&lt;p&gt;In a combobox, Tab moves focus out of the widget. A user navigating the suggestions with arrow keys shouldn't have to Tab into a separate region to reach them. The model had identified the right pattern and then proposed an interaction that contradicted it.&lt;/p&gt;

&lt;p&gt;I had to catch that.&lt;/p&gt;

&lt;p&gt;Another fix involved two different types of actions in the same listbox. Some options navigated to a product page and displayed the product image and name. Others rewrote the query and contained only the suggested query.&lt;/p&gt;

&lt;p&gt;Visually, the distinction was obvious. A sighted user could immediately tell a product result from a suggested query.&lt;/p&gt;

&lt;p&gt;But the interaction model wasn't.&lt;/p&gt;

&lt;p&gt;A listbox presents its children as options within a uniform selection model. When those options perform fundamentally different actions, the interaction model no longer matches what the options actually do.&lt;/p&gt;

&lt;p&gt;The better solution was to rethink the interaction rather than try to make the existing one more accessible.&lt;/p&gt;

&lt;p&gt;Again, the proposed fix was technically plausible. It just didn't survive contact with the actual interaction.&lt;/p&gt;

&lt;h2&gt;
  
  
  The part that's easy to miss
&lt;/h2&gt;

&lt;p&gt;Here's what made this risky.&lt;/p&gt;

&lt;p&gt;The wrong fixes arrived with exactly the same fluency and confidence as the right ones. Nothing in the response flagged "this one is probably wrong."&lt;/p&gt;

&lt;p&gt;If I hadn't understood how focus moves through a combobox, or thought carefully about what information actually reaches a screen-reader user through the accessibility tree, I could have shipped both.&lt;/p&gt;

&lt;p&gt;That's the real gap.&lt;/p&gt;

&lt;p&gt;The model had understood the visual UI, the ARIA pattern, and the relevant standards, but it hadn't reliably reasoned through what the resulting interaction communicated to the person using it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Detection is not judgment. Fluency is not judgment.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Where the corrections came from
&lt;/h2&gt;

&lt;p&gt;The important part wasn't simply knowing the WCAG success criteria or looking up the relevant ARIA pattern.&lt;/p&gt;

&lt;p&gt;It was understanding how the interaction actually works for people using assistive technology.&lt;/p&gt;

&lt;p&gt;That understanding comes from accessibility expertise, testing, standards, technical experience, and importantly, lived experience.&lt;/p&gt;

&lt;h2&gt;
  
  
  So how do we use AI?
&lt;/h2&gt;

&lt;p&gt;Not by pretending it's useless.&lt;/p&gt;

&lt;p&gt;It flagged real bugs, explained the standards, and drafted code faster than I would have. I'll keep using it for that.&lt;/p&gt;

&lt;p&gt;But AI should remain inside the remediation loop, not become the end of it.&lt;/p&gt;

&lt;p&gt;AI can detect. AI can explain. AI can propose.&lt;/p&gt;

&lt;p&gt;Engineers still need to evaluate the proposed solution in the context of the actual application. And disabled users still need to tell us whether the resulting experience actually works.&lt;/p&gt;

&lt;p&gt;The goal isn't to keep AI out of accessibility.&lt;/p&gt;

&lt;p&gt;It's to use it where it is strong without handing it the parts of the process that require judgment, context, and human experience.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;AI can often tell you what's broken. It can even suggest how to fix it. But it can't be the final authority on whether we've actually made the experience better for the people we're building it for.&lt;/strong&gt;&lt;/p&gt;

</description>
      <category>a11y</category>
      <category>ai</category>
      <category>webdev</category>
      <category>shopify</category>
    </item>
    <item>
      <title>A broken heading hierarchy is an architecture problem, not a markup one</title>
      <dc:creator>Praise Agbabiaka</dc:creator>
      <pubDate>Sun, 09 Aug 2026 21:59:34 +0000</pubDate>
      <link>https://dev.to/praiztech/a-broken-heading-hierarchy-is-an-architecture-problem-not-a-markup-one-489n</link>
      <guid>https://dev.to/praiztech/a-broken-heading-hierarchy-is-an-architecture-problem-not-a-markup-one-489n</guid>
      <description>&lt;p&gt;On a recent BigCommerce remediation, an audit flagged the heading hierarchy. Levels skipped and jumped across the page, so the structure a screen-reader user navigates by didn't hold together.&lt;/p&gt;

&lt;p&gt;The checklist fix is obvious. Open each template, retag the headings, close the finding.&lt;/p&gt;

&lt;p&gt;But the levels weren't the bug. They were the symptom.&lt;/p&gt;

&lt;p&gt;Each heading was hardcoded into its own template. A section rendered an &lt;code&gt;&amp;lt;h2&amp;gt;&lt;/code&gt; because whoever wrote that template picked &lt;code&gt;&amp;lt;h2&amp;gt;&lt;/code&gt;, with no view of where it would sit on the assembled page. Nothing owned the hierarchy. It emerged, wrongly, from dozens of local decisions.&lt;/p&gt;

&lt;p&gt;That's the real problem, and it's architectural.&lt;/p&gt;

&lt;p&gt;A reusable heading component has no inherent level. The same block might be an &lt;code&gt;&amp;lt;h2&amp;gt;&lt;/code&gt; on one page and an &lt;code&gt;&amp;lt;h3&amp;gt;&lt;/code&gt; nested inside a region on another. Its correct level depends on where it sits, not on what it is.&lt;/p&gt;

&lt;p&gt;Any component that bakes in a fixed level is going to be wrong somewhere because it can't see its own context.&lt;/p&gt;

&lt;p&gt;So the fix isn't to hardcode a different level. It's to stop hardcoding heading levels altogether and render each heading from one place that receives its level from whatever owns the page structure.&lt;/p&gt;

&lt;p&gt;The principle is portable. The idiom changes per stack:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;BigCommerce (Handlebars): a single partial that receives the level as a parameter, replacing heading markup repeated across a dozen templates.&lt;/li&gt;
&lt;li&gt;Shopify (Liquid): a snippet that takes the level through &lt;code&gt;render&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;React: pass the level as a prop, or use context when the hierarchy needs to be derived from component composition.&lt;/li&gt;
&lt;li&gt;Astro: let the parent own the structure and pass the level through props, rather than having the child commit to a level it can't know from its own context.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Different mechanisms, one move.&lt;/p&gt;

&lt;p&gt;Headings render in a single place, and the level is an input from context, not a constant.&lt;/p&gt;

&lt;p&gt;It's also the fix that lasts. A hardcoded level regresses the moment a section is reused or a template is copied. A contextual one stays correct because the decision lives with the page structure instead of being re-decided everywhere.&lt;/p&gt;

&lt;p&gt;That's the difference between closing a finding and fixing the system that produced it.&lt;/p&gt;

&lt;p&gt;The accessibility failure was in the markup. The fix was in the architecture.&lt;/p&gt;

</description>
      <category>a11y</category>
      <category>webdev</category>
      <category>bigcommerce</category>
      <category>react</category>
    </item>
    <item>
      <title>You don't have to choose between Shopify updates and accessibility fixes</title>
      <dc:creator>Praise Agbabiaka</dc:creator>
      <pubDate>Mon, 03 Aug 2026 01:23:41 +0000</pubDate>
      <link>https://dev.to/praiztech/you-dont-have-to-choose-between-shopify-updates-and-accessibility-fixes-fm8</link>
      <guid>https://dev.to/praiztech/you-dont-have-to-choose-between-shopify-updates-and-accessibility-fixes-fm8</guid>
      <description>&lt;p&gt;A while back, a developer told me something that stuck with me. He said fixing Shopify Liquid files is a hassle, because customizations don't scale with theme updates. The moment you start editing a theme, you've effectively forked it, and every future update has to be diffed and re-applied by hand.&lt;/p&gt;

&lt;p&gt;He's not wrong. That's effectively how maintaining a customized Shopify theme works.&lt;/p&gt;

&lt;p&gt;It's also why many teams avoid deep changes to a live theme, accessibility remediation included.&lt;/p&gt;

&lt;p&gt;Accessibility fixes often require changes that only exist in Liquid templates: adding semantic HTML, correcting heading hierarchy, exposing labels to assistive technology, or fixing landmark structure. Those aren't changes you can reliably layer on afterward with CSS or JavaScript.&lt;/p&gt;

&lt;p&gt;But "that's just how Shopify works" is where the conversation usually stops. I don't think it should stop there. The fork isn't a Shopify problem. It's a process problem. And process problems have process answers.&lt;/p&gt;

&lt;p&gt;Solving the update problem comes down to two ideas: isolate changes so most of them never collide with an update, and keep a clean, greppable record of the changes that can't be isolated so the manual diff stays small and predictable.&lt;/p&gt;

&lt;h2&gt;
  
  
  The core problem
&lt;/h2&gt;

&lt;p&gt;Two facts about Shopify themes create the pain:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Custom edits don't carry through theme updates.&lt;/strong&gt; When Shopify ships a new version of the base theme, it doesn't merge your changes in. You get the new version, and reconciling your customizations is on you.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A GitHub-connected theme stops receiving Shopify's theme updates.&lt;/strong&gt; Once a theme is connected to GitHub, Shopify's built-in "Update available" workflow no longer applies to that theme. If you want upstream theme updates, you need to bring them into your Git workflow yourself.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So you want two things at once: version control and the ability to take upstream updates. Do you have to choose one? No. You keep them in separate places on purpose. That's the core idea, and everything below is just the mechanics of it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Setup, before any remediation starts
&lt;/h2&gt;

&lt;p&gt;This groundwork is what makes the update painless later. Do it first.&lt;/p&gt;

&lt;h3&gt;
  
  
  Isolate CSS and JavaScript into custom files
&lt;/h3&gt;

&lt;p&gt;Any styling or scripting change goes into its own custom asset files instead of being edited into the theme's existing files. These almost never conflict with an update. Because these are new assets rather than modifications to the theme's files, upstream theme updates rarely touch them, so Git treats them as independent additions rather than conflicting edits. The one exception is the line that loads them: the &lt;code&gt;stylesheet_tag&lt;/code&gt; or &lt;code&gt;script_tag&lt;/code&gt; that enqueues these custom assets lives in Liquid, usually in &lt;code&gt;theme.liquid&lt;/code&gt;, so that single hookup is a Liquid edit an update can collide with. Tag it with your &lt;code&gt;A11Y-REMEDIATION&lt;/code&gt; marker like any other Liquid change. With that one line accounted for, most of your work is gone from the diff before the diff even starts.&lt;/p&gt;

&lt;h3&gt;
  
  
  Tag every Liquid change and keep a list
&lt;/h3&gt;

&lt;p&gt;Liquid is the part you can't fully isolate. Sometimes you have to edit a section or snippet directly. For every one of those edits, leave a clear comment describing what changed, and maintain a running list of every Liquid file you touched.&lt;/p&gt;

&lt;p&gt;One refinement pays off later. Use a consistent, greppable marker in those comments:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight liquid"&gt;&lt;code&gt;&lt;span class="cp"&gt;{%&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;comment&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="cp"&gt;%}&lt;/span&gt;&lt;span class="c"&gt; A11Y-REMEDIATION: added aria-label + visible focus state to search toggle &lt;/span&gt;&lt;span class="cp"&gt;{%&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;endcomment&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="cp"&gt;%}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now your "list of changed Liquid files" isn't something you maintain by hand and hope is complete. It's one command:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-rl&lt;/span&gt; &lt;span class="s2"&gt;"A11Y-REMEDIATION"&lt;/span&gt; &lt;span class="nb"&gt;.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Designate an unconnected copy of the live theme to receive updates
&lt;/h3&gt;

&lt;p&gt;Before you start, duplicate the live theme and leave that copy &lt;em&gt;disconnected&lt;/em&gt; from GitHub. Because it isn't connected, it stays eligible for Shopify's updates. This copy is your update pipeline. The connected repo is your source of truth. Keeping them apart is the point.&lt;/p&gt;

&lt;h2&gt;
  
  
  The update workflow
&lt;/h2&gt;

&lt;p&gt;When Shopify releases a new version of the base theme, this is how you take it without losing remediation work.&lt;/p&gt;

&lt;h3&gt;
  
  
  Phase 1: bring the update into version control
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;Update the theme copy in the Shopify admin. Shopify generates a new &lt;em&gt;Updated Copy&lt;/em&gt; of the theme with the new version applied.&lt;/li&gt;
&lt;li&gt;Create a backup branch from the connected repo's &lt;code&gt;main&lt;/code&gt;. If anything goes sideways, you have a clean restore point.&lt;/li&gt;
&lt;li&gt;Create an &lt;code&gt;update&lt;/code&gt; branch and pull the code from the &lt;em&gt;Updated Copy&lt;/em&gt; into it, not from the live theme. This gets the upstream changes into Git where you can actually diff them.&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Phase 2: reconcile, with a focus on every Liquid change
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;Merge &lt;code&gt;update&lt;/code&gt; into &lt;code&gt;dev&lt;/code&gt; using &lt;code&gt;--no-commit&lt;/code&gt; (&lt;code&gt;git merge --no-commit --no-ff update&lt;/code&gt;). This leaves the merge staged but uncommitted, giving you a chance to inspect both conflicting and automatically merged changes before creating the merge commit. The &lt;code&gt;--no-ff&lt;/code&gt; flag forces an actual merge commit so you always land in that reviewable staged state, even when Git could have fast-forwarded.&lt;/li&gt;
&lt;li&gt;Diff every Liquid file you modified during remediation, file by file. This is the one deliberately manual step, and it's where your tagged comments and file list earn their keep. You know exactly which files to check, so you're reviewing a short list, not the whole theme. Keep, adapt, or discard each incoming change so accessibility fixes are preserved without missing legitimate improvements in the updated theme.&lt;/li&gt;
&lt;li&gt;Commit to &lt;code&gt;dev&lt;/code&gt; once the diff is clean, then merge &lt;code&gt;dev&lt;/code&gt; into &lt;code&gt;main&lt;/code&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Phase 3: push back to Shopify, in the right order
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;Push &lt;code&gt;main&lt;/code&gt; to the Updated Copy, not to Live yet. Order matters here, to avoid schema errors: push the configuration files first, &lt;code&gt;config/settings_schema.json&lt;/code&gt; and &lt;code&gt;config/settings_data.json&lt;/code&gt;, then push the remaining theme files. Pushing the config first means that if there's a schema problem, the sync fails here, before the rest of the theme is touched, so you fix the JSON discrepancy and retry instead of discovering the problem after everything else has synced.&lt;/li&gt;
&lt;li&gt;Verify the Updated Copy on Shopify before publishing. Alongside the usual schema and functional checks, re-run your accessibility checks here: an upstream update can change base-theme files you never touched and quietly regress accessibility along the way, so confirm the copy still holds up before it goes live. Only then publish it as the Live theme.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Why this holds up
&lt;/h2&gt;

&lt;p&gt;Notice what each piece is doing. The unconnected copy keeps you eligible for updates. Version control keeps your work reviewable and reversible. Isolated CSS and JavaScript mean most of your changes never enter the diff at all. Tagged Liquid comments turn the unavoidable manual step into a bounded, predictable one. The config-first push order fails loudly and early instead of quietly and late.&lt;/p&gt;

&lt;p&gt;None of this makes Shopify merge your changes. That was never on the table. What it does is shrink the manual work to the smallest surface possible and make every risky step recoverable.&lt;/p&gt;

&lt;p&gt;The theme stays maintainable. Updates stay boring. "We customized it" stops meaning "we're stuck on this version forever."&lt;/p&gt;

&lt;p&gt;That developer's complaint was fair. It just wasn't the end of the story. Shopify sets the constraint. A disciplined workflow keeps it manageable.&lt;/p&gt;

</description>
      <category>shopify</category>
      <category>a11y</category>
      <category>webdev</category>
      <category>liquid</category>
    </item>
    <item>
      <title>Building a Component Library: From First Component to Fully Automated Pipeline</title>
      <dc:creator>Praise Agbabiaka</dc:creator>
      <pubDate>Tue, 19 May 2026 21:20:19 +0000</pubDate>
      <link>https://dev.to/praiztech/building-a-component-library-from-first-component-to-fully-automated-pipeline-4dg4</link>
      <guid>https://dev.to/praiztech/building-a-component-library-from-first-component-to-fully-automated-pipeline-4dg4</guid>
      <description>&lt;p&gt;I was asked to architect and build a React component library from the ground up.&lt;/p&gt;

&lt;p&gt;The problem was clear. We kept rebuilding the same components across different applications. The same buttons, inputs, modals, and layouts, recreated from scratch every time. Someone needed to own a component library and that someone ended up being me.&lt;/p&gt;

&lt;p&gt;I nodded like I had a plan. I didn't.&lt;/p&gt;

&lt;p&gt;I'd never built a component library before. I didn't know how to structure one, how to distribute one, or honestly how to scope one. But I figured the best way to learn was to start.&lt;/p&gt;

&lt;p&gt;So I started small. Buttons. Inputs. The boring stuff that touches everything.&lt;/p&gt;

&lt;p&gt;I focused on getting the foundations right, and I made the decision very early on that accessibility would be baked in from day one. Every component designed to be usable by keyboard users, screen readers, and other assistive technologies from the start. Not as a future improvement. Not as a "nice to have." Just part of the standard.&lt;/p&gt;

&lt;p&gt;Then, the library kept growing.&lt;/p&gt;

&lt;p&gt;Today, it's documented and hosted in Storybook, published as an npm package, and fully automated with CI/CD pipelines that handle Storybook deployments and npm publishing on every release.&lt;/p&gt;

&lt;p&gt;What started as a vague ask turned into real infrastructure the team relies on daily.&lt;/p&gt;

&lt;p&gt;A few things I'd tell anyone starting from zero:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;You don't need to know everything upfront. Start with the smallest components and let the architecture reveal itself.&lt;/li&gt;
&lt;li&gt;Treat accessibility like functionality. If it doesn't work with a keyboard, it's not done.&lt;/li&gt;
&lt;li&gt;Automate early. Setting up CI/CD for publishing and deployment saved me more time than I expected.&lt;/li&gt;
&lt;li&gt;Document everything. Storybook isn't just a demo tool. It's how the team actually adopts the library.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The gap between "I have no idea what to do" and "this is running in production" is smaller than you think. It's just a lot of small steps.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>react</category>
      <category>a11y</category>
      <category>cicd</category>
    </item>
    <item>
      <title>Building My First Wagtail Site</title>
      <dc:creator>Praise Agbabiaka</dc:creator>
      <pubDate>Mon, 30 Oct 2023 04:09:01 +0000</pubDate>
      <link>https://dev.to/praiztech/building-my-first-wagtail-site-3hj7</link>
      <guid>https://dev.to/praiztech/building-my-first-wagtail-site-3hj7</guid>
      <description>&lt;p&gt;Software development is an ever-changing field. Often, one has to pick up new technologies rather quickly. This was the case for me with Wagtail. And thanks to its simplicity, I was able to do so in no time.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Wagtail?
&lt;/h2&gt;

&lt;p&gt;An opportunity to improve the accessibility of websites built with Wagtail opened up recently. And I immediately jumped on it. I was so excited at the prospect of putting my accessibility skills to use that I didn’t give much thought to the fact that I had no experience with Wagtail. I figured I could pick up Wagtail quickly enough if I put my mind to it. And since I learn best by doing, I decided to build a blog site to gain hands-on experience with Wagtail.&lt;/p&gt;

&lt;h2&gt;
  
  
  What’s Wagtail?
&lt;/h2&gt;

&lt;p&gt;Wagtail is an open-source Content Management System built on Django. So, much of the process for building websites with Django translates directly to Wagtail. But Wagtail abstracts the need to define &lt;code&gt;urls&lt;/code&gt;, &lt;code&gt;views&lt;/code&gt; and &lt;code&gt;admin&lt;/code&gt; functionality. In Wagtail, emphasis is placed on page and page content. Every page is built from a Django model that inherits from Wagtail’s &lt;code&gt;Page&lt;/code&gt; model. Wagtail also provides a number of custom template tags and filters that extend the capabilities of the Django templating engine.&lt;/p&gt;

&lt;h2&gt;
  
  
  Building the Blog Site
&lt;/h2&gt;

&lt;p&gt;I decided that my blog site would have a home page, a blog index page, blog post pages, and an about page. Since a home app comes out of the box with Wagtail, I only had to create a base and blog app for my Wagtail project.&lt;/p&gt;

&lt;p&gt;In the home app, I updated the &lt;code&gt;HomePage&lt;/code&gt; model to include fields for a hero image, heading, introduction and link to the blog index page. I defined the image and link fields as a &lt;code&gt;Foreignkey&lt;/code&gt; to Wagtail’s &lt;code&gt;Image&lt;/code&gt; and &lt;code&gt;Page&lt;/code&gt; models, respectively.&lt;/p&gt;

&lt;p&gt;As the name suggests, the base app defined functionalities shared across apps within the project. It contained &lt;code&gt;snippets&lt;/code&gt; for the site logo, main menu and breadcrumb navigation. Within this app, I constructed a &lt;code&gt;ContentBlock&lt;/code&gt; that allowed me to dynamically add subheading, image, list and rich text content to a page. This &lt;code&gt;ContentBlock&lt;/code&gt; inherited from &lt;code&gt;StreamBlock&lt;/code&gt;. It was used to define a &lt;code&gt;StreamField&lt;/code&gt; to populate the body field of the about and blog post pages. Because the about page didn’t belong in the home or blog app, I defined a &lt;code&gt;StandardPage&lt;/code&gt; model to create it.&lt;/p&gt;

&lt;p&gt;The blog app held models and templates for the blog index page and blog post pages. I defined the &lt;code&gt;BlogPage&lt;/code&gt; model as child page of the &lt;code&gt;BlogIndexPage&lt;/code&gt;. And I populated the blog index page with meta data for each blog post. Thus, a new blog site was built.&lt;/p&gt;

&lt;p&gt;I’m glad that I can now add Wagtail to my toolkit as a web developer. If you’d like to see my code, check out the &lt;a href="https://github.com/praiztech/praiztech_blog" rel="noopener noreferrer"&gt;praiztech_blog repository on GitHub&lt;/a&gt;. I'm still working on the front-end, though.&lt;/p&gt;

</description>
      <category>softwaredevelopment</category>
      <category>webdev</category>
      <category>python</category>
      <category>django</category>
    </item>
    <item>
      <title>Growing My Skill in Web Accessibility</title>
      <dc:creator>Praise Agbabiaka</dc:creator>
      <pubDate>Fri, 27 Oct 2023 12:21:44 +0000</pubDate>
      <link>https://dev.to/praiztech/growing-my-skill-in-web-accessibility-2bij</link>
      <guid>https://dev.to/praiztech/growing-my-skill-in-web-accessibility-2bij</guid>
      <description>&lt;p&gt;As a self-taught web developer, my first impulse whenever I encounter a new concept is to search the Web. There's almost always a blog post or video that explains it in enough detail to help me understand. For the most part, this has been no different for web accessibility.&lt;/p&gt;

&lt;h2&gt;
  
  
  Using Online Resources
&lt;/h2&gt;

&lt;p&gt;Once I learnt about &lt;a href="https://developer.mozilla.org/en-US/docs/Learn/Accessibility" rel="noopener noreferrer"&gt;web accessibility on Mozilla&lt;/a&gt;, it was quite easy to find other related posts. I found several excellent posts to expand my knowledge of accessible web development. I learnt to write accessible code with semantic Hypertext Markup Language (HTML) elements. I also learnt to test my websites with automated testing tools such as WAVE, axe DevTools and ARC Toolkit. To understand how blind users experience my websites, I learnt to test with NVDA and TalkBack screen readers. I also learnt to enhance the output of screen readers with Accessible Rich Internet Applications (ARIA) when there are no HTML alternatives.&lt;/p&gt;

&lt;h2&gt;
  
  
  Learning from a Great Mentor
&lt;/h2&gt;

&lt;p&gt;As my knowledge of web accessibility grew, I realized that I was lacking an important skill. I didn't know how to perform a manual accessibility audit. I searched the Web but I couldn't find a simple guide on the process of manual accessibility testing. Yet, I knew I had to gain this skill. Manual auditing is the only way to identify all accessibility issues on a website.&lt;/p&gt;

&lt;p&gt;So, when Rachele DiTullio posted on LinkedIn that they had created &lt;a href="https://racheleditullio.com/projects/accessibility-testing/" rel="noopener noreferrer"&gt;manual accessibility audit videos&lt;/a&gt;, I was excited. First, I watched the videos to understand their testing process. Then, I sent them an email requesting their mentorship. They were very receptive and agreed to mentor me. They agreed to provide weekly testing exercises and give feedback on the results. Thus began my journey into manual web accessibility auditing.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fwzthbqwtwsfibuj8v6gp.PNG" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fwzthbqwtwsfibuj8v6gp.PNG" alt="A worksheet in a manual accessibility testing workbook showing the scope of the test" width="800" height="326"&gt;&lt;/a&gt;&lt;/p&gt;&lt;br&gt;&lt;strong&gt;My first manual accessibility testing workbook&lt;/strong&gt;
  &lt;p&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The Journey So Far
&lt;/h2&gt;

&lt;p&gt;As I've performed manual accessibility audits, my skill have evolved over time. I've learnt to break down each webpage into smaller sections to test. I'm learning to better interpret the Web Content Accessibility Guidelines (WCAG). I'm learning how WCAG failures impact the web experience of users with disabilities. It's a journey. I'm getting better at it. &lt;/p&gt;

</description>
      <category>webdev</category>
      <category>webaccessibility</category>
      <category>accessiblecode</category>
      <category>a11y</category>
    </item>
  </channel>
</rss>
