<?xml version="1.0" encoding="utf-8"?>
  <rss version="2.0"
    xmlns:content="http://purl.org/rss/1.0/modules/content/"
    xmlns:wfw="http://wellformedweb.org/CommentAPI/"
    xmlns:dc="http://purl.org/dc/elements/1.1/"
    xmlns:atom="http://www.w3.org/2005/Atom"
    xmlns:sy="http://purl.org/rss/1.0/modules/syndication/"
    xmlns:slash="http://purl.org/rss/1.0/modules/slash/"
    xmlns:georss="http://www.georss.org/georss"
    xmlns:geo="http://www.w3.org/2003/01/geo/wgs84_pos#"
  >
    <channel>
      <title>Piccalilli - Articles</title>
      <link>https://piccalil.li/</link>
      <atom:link href="https://piccalil.li/articles.xml" rel="self" type="application/rss+xml" />
      <description>We are Piccalilli. A publication dedicated to providing high quality educational content to level up your front-end skills.</description>
      <language>en-GB</language>
      <copyright>Piccalilli - Articles 2026</copyright>
      <docs>https://www.rssboard.org/rss-specification</docs>
      <pubDate>Mon, 10 Aug 2026 04:09:07 GMT</pubDate>
      <lastBuildDate>Mon, 10 Aug 2026 04:09:07 GMT</lastBuildDate>

      
      <item>
        <title>Working with ::highlight() using progressive enhancement </title>
        <link>https://piccalil.li/blog/working-with-highlight-using-progressive-enhancement/?ref=articles-rss-feed</link>
        <dc:creator><![CDATA[Sunkanmi Fafowora]]></dc:creator>
        <pubDate>Thu, 06 Aug 2026 11:55:00 GMT</pubDate>
        <guid isPermaLink="true">https://piccalil.li/blog/working-with-highlight-using-progressive-enhancement/?ref=articles-rss-feed</guid>
        <description><![CDATA[<p><a href="https://developer.mozilla.org/en-US/docs/Web/CSS/Reference/Selectors/Pseudo-elements#highlight_pseudo-elements">Highlighting in CSS</a> has been beneficial for applying a highlight on specific text or text fragments during user selection, emphasizing a piece of information on a website, or visually emphasizing a text for the sake of branding. Particularly, this is pretty helpful when users want to scan your document from top to bottom because the <a href="https://www.nngroup.com/articles/concise-scannable-and-objective-how-to-write-for-the-web/">majority of people don’t read your document initially; they scan</a>.</p>
<p>On the web, text highlights are a good way to lay emphasis on text fragments through good ol’ CSS. From <code>::selection</code> for styling selected text to <code>::target-text</code> which styles highlighted text from Google searches, and in my opinion, CSS’ most powerful pseudo-element for highlighting: <code>::highlight()</code> which applies a custom highlight to a text fragment.</p>
<p>In this article, we will look into how the <code>::highlight()</code> pseudo-element works, the API behind this pseudo-element, and explore a fallback feature for this technology because it relies <strong>heavily on JavaScript (JS).</strong></p>
<p></p><p>See the Pen <a href="https://codepen.io/piccalilli/pen/XJjwNap">::highlight() demo: pure text-shadows</a> by Andy Bell (<a href="https://codepen.io/piccalilli/">@piccalilli</a>) on <a href="https://codepen.io">CodePen</a>.</p><p></p>
<p></p>
<h1>The CSS Custom Highlight API</h1>
<p>Typically, a highlight or highlighted text is what you’d see during web searches or when you what to select a text fragment to copy, or even when you make a mistake in a word processor (the squiggly red underlines)<em>.</em> In CSS, you can achieve these through the highlight pseudo-elements like<code>::search-text</code>, <code>::selection</code>, <code>::spelling-error</code> , and <code>::grammar-error</code> . But, what about plain highlights like the demo above? That’s where the CSS Custom Highlight API comes in.</p>
<p>The <a href="https://developer.mozilla.org/en-US/docs/Web/API/CSS_Custom_Highlight_API">CSS Custom Highlight API</a> is an API for text highlighting on a range of text using JavaScript and CSS. It extends the pseudo-elements for highlighting (<code>::search-text</code> , <code>::selection</code>, <code>::spelling-error</code> ) and lets you customize text fragments with <code>::highlight()</code> and JavaScript. <code>::highlight()</code> is what will be our focus for this article, and how we can programmatically highlight text and its fragments using JavaScript and CSS.</p>
<h1>How ::highlight() works</h1>
<p>To create highlighted text like in the initial example, you need to know 4 total steps, which include:</p>
<ol>
<li>Creating the highlight buckets by creating instances of the <code>Highlight()</code> class</li>
<li>Register each highlight instance into <code>CSS.highlights</code></li>
<li>Create <code>Range()</code> objects each with different points on the text for highlighting and add <code>Range()</code> objects to its highlight instances</li>
<li>Style with <code>::highlight()</code> pseudo-element</li>
</ol>
<p>Before we move on with an example, let me explain something because this is where it might get tricky. What we do around here is <strong>build with <a href="https://piccalil.li/blog/its-about-time-i-tried-to-explain-what-progressive-enhancement-actually-is/">progressive enhancement</a> in mind first</strong>. So the first question you should ask before anything is “what happens <a href="https://piccalil.li/blog/a-handful-of-reasons-javascript-wont-be-available/"><em>when</em> JS fails</a>?”. Well, I’m glad you asked.</p>
<p>Let’s say, for our example, we want to create two highlight objects and use that to style a simple poetic text that reads “<em>fire and ice live inside every word.</em>” Because we care about our users, we set each text for highlight with the <code>&lt;mark&gt;</code> HTML tag setting the <code>class</code> attribute to either <code>gold</code> or <code>ice</code> , depending on the highlighting style we wish the text to have. This acts as a fallback highlight style in case our JavaScript fails or the browser doesn’t support custom highlighting.</p>
<p>We also apply an <code>id</code> to them in case JS is available too for our CSS Custom Highlight API. With all that in mind, our HTML would look like this:</p>
<pre><code>&lt;main&gt;
  &lt;h1&gt;Two Highlights Demo&lt;/h1&gt;
  &lt;p id="line"&gt;
    &lt;mark id="fire" class="gold"&gt;fire&lt;/mark&gt; and
    &lt;mark id="ice" class="ice"&gt;ice&lt;/mark&gt; live inside 
    &lt;mark id="every" class="ice"&gt;every&lt;/mark&gt; 
    &lt;mark id="word" class="gold"&gt;word&lt;/mark&gt;
  &lt;/p&gt;
&lt;/main&gt;
</code></pre>
<p>Then, we proceed to query each tagged word in our JS applying the <code>firstChild</code> property to each of them of get the element’s first child node which we will use later:</p>
<pre><code>const fireNode = document.querySelector("#fire").firstChild;
const everyNode = document.querySelector("#every").firstChild;
const iceNode = document.querySelector("#ice").firstChild;
const wordNode = document.querySelector("#word").firstChild;
</code></pre>
<p>Finally, we can proceed with the steps on creating a custom highlight in CSS.</p>
<p></p>
<h2>Create instances of <code>Highlight()</code> class</h2>
<p>In order to create a custom highlight, the first step is to create an instance of the <code>Highlight()</code> class which will house the highlight styling we want a text or text fragment to have. For our demo, we’ll be creating two highlight objects named using the <code>Highlight()</code> class. One to give a golden color representing fire and the other to give a blue color representing ice:</p>
<pre><code>const goldHL = new Highlight();
const iceHL = new Highlight();
</code></pre>
<h2>Register each highlight instance in CSS.highlights</h2>
<p>Next, we register the created highlight instances in the <code>HighlightRegistry</code> via <code>CSS.highlights</code> <code>set()</code> method. We map a valid CSS identifier to the instance for CSS styling later.</p>
<pre><code>CSS.highlights.set("hl-gold", goldHL);
CSS.highlights.set("hl-ice", iceHL);
</code></pre>
<h2>Create Range() objects each with different points on the text for highlighting</h2>
<p>In this step, we will be creating a <code>Range()</code> object for each text fragment we queried earlier for highlighting. We will then apply the highlight we want on each selected text. For the first word “fire”, we create a <code>Range()</code> object called <code>r1</code> , and we set the start node to the first letter “f” using <code>setStart</code> on <code>r1</code> . <code>setStart()</code> accepts two values. It accepts the node we’re targeting (in our case for “fire”, its <code>fireNode</code> ) and the index of the text on the node.</p>
<p>Now, because we want to target the whole text “fire”, we have to also set where the range will stop. And this will be set using <code>setEnd()</code> . <code>setEnd()</code> accepts two values like <code>setStart()</code> on the range object (<code>r1</code>). It accepts the node we’re targeting (<code>fireNode</code> ) and the index of the end text “e” (as in the “e” in “fire”) using <code>fireNode.textContent.length</code> - 1 which gives us the last index of the text.</p>
<p>Finally, we add the range object into the set instance.</p>
<pre><code>const r1 = new Range();
r1.setStart(fireNode, 0);
r1.setEnd(fireNode, fireNode.textContent.length - 1);
goldHL.add(r1);
</code></pre>
<p>This step is repeated for <code>everyNode</code>, <code>iceNode</code>, and <code>wordNode</code>. Typically, you’d want to use a loop for this, but because this is really small, writing it in a specific manner will suffice, especially to help you understand how this works too.</p>
<pre><code>const r2 = new Range();
r2.setStart(wordNode, 0);
r2.setEnd(wordNode, wordNode.textContent.length - 1);
goldHL.add(r2);

const r3 = new Range();
r3.setStart(iceNode, 0);
r3.setEnd(iceNode, iceNode.textContent.length - 1);
iceHL.add(r3);

const r4 = new Range();
r4.setStart(everyNode, 0);
r4.setEnd(everyNode, everyNode.textContent.length - 1);
iceHL.add(r4);
</code></pre>
<h2>Style with ::highlight() pseudo-element</h2>
<p>Remember how we said we should <strong>think progressive enhancement first?</strong> Well, in order to achieve that for this demo in particular, we need to style the <code>&lt;mark&gt;</code>ed highlighted text first, then, we style the <code>::highlight()</code> pseudo-element. For that to work, we styled text marked with the <code>gold</code> class to be golden in <code>oklch()</code> with a glowy text shadow of similar color. We style text <code>&lt;mark&gt;</code> ed with the <code>ice</code> class to be blueish in <code>oklch()</code> with a glowy text shadow of similar color:</p>
<pre><code>mark {
  background: none;
}

mark.gold {
  color: oklch(88% 0.16 75);
  text-shadow: 0 0 40px oklch(65% 0.22 75 / 0.4);
}

mark.ice {
  color: oklch(82% 0.1 215);
  text-shadow: 0 0 40px oklch(60% 0.18 215 / 0.4);
}
</code></pre>
<p></p><p>See the Pen <a href="https://codepen.io/piccalilli/pen/azBoBBo">::highlight() demo: two highlights without `::highlight()`</a> by Andy Bell (<a href="https://codepen.io/piccalilli/">@piccalilli</a>) on <a href="https://codepen.io">CodePen</a>.</p><p></p>
<p>Viola! (or how do they say it?) It looks amazing! 🤩</p>
<p>Even without the styled <code>::highlight()</code> , it works out pretty well. But, that’s not our only aim though. We still need to add the styling for both our highlight objects.</p>
<pre><code>::highlight(hl-gold) {
  color: oklch(88% 0.16 75);
  text-shadow: 0 0 40px oklch(65% 0.22 75 / 0.4);
}

::highlight(hl-ice) {
  color: oklch(82% 0.1 215);
  text-shadow: 0 0 40px oklch(60% 0.18 215 / 0.4);
}
</code></pre>
<p>Done! It’s pretty much the same styling we did for our <code>&lt;mark&gt;</code> tag classes and in case JS fails or the browser doesn’t support it, we wrap the <code>::higlight()</code> pseudo-elements in a <code>@supports</code> container:</p>
<pre><code>@supports selector(::highlight(h1-gold)) {
  ::highlight(hl-gold) {
    color: oklch(88% 0.16 75);
    text-shadow: 0 0 40px oklch(65% 0.22 75 / 0.4);
  }

  ::highlight(hl-ice) {
    color: oklch(82% 0.1 215);
    text-shadow: 0 0 40px oklch(60% 0.18 215 / 0.4);
  }
}
</code></pre>
<p></p><p>See the Pen <a href="https://codepen.io/piccalilli/pen/qEqWZYN">::highlight() demo: two highlights with fallback</a> by Andy Bell (<a href="https://codepen.io/piccalilli/">@piccalilli</a>) on <a href="https://codepen.io">CodePen</a>.</p><p></p>
<p></p>
<h2>Will you be using ::highlight()?</h2>
<p>I know for sure I will be using this feature in my work. This article explained highlighting in CSS, the CSS Custom Highlight API, and how <code>::higlight()</code> works in CSS with a fallback provision, for when JavaScript isn’t available or the browser doesn’t support it.</p>
<p>If you’re looking to read up more about the <a href="https://developer.mozilla.org/en-US/docs/Web/API/CSS_Custom_Highlight_API">CSS Custom Highlight API</a>, MDN has a good guide on the topic for you.</p>
        
        ]]></description>
        
      </item>
    
      <item>
        <title>Use cases for aria-expanded</title>
        <link>https://piccalil.li/blog/use-cases-for-aria-expanded/?ref=articles-rss-feed</link>
        <dc:creator><![CDATA[Steve Frenzel]]></dc:creator>
        <pubDate>Thu, 16 Jul 2026 11:55:00 GMT</pubDate>
        <guid isPermaLink="true">https://piccalil.li/blog/use-cases-for-aria-expanded/?ref=articles-rss-feed</guid>
        <description><![CDATA[<p>Communication can be difficult. Not only when we humans try to communicate with one another, but also in web development.</p>
<p>While it is easy for sighted people with full mental and motor abilities to click a button with a mouse to display more information, users of Assistive Technology (AT) may have a completely different experience with the same action.</p>
<p>The information an expandable button conveys to AT depends heavily on the context. This context also dictates which ARIA attributes should (or should not) be used. Often, the patterns are very similar in their functionality and interpretation and can be interpreted differently by me than by you.</p>
<p>When I conduct accessibility audits, I usually can’t avoid having to check an element that has the <code>aria-expanded</code> attribute. One of the challenges I faced was determining what kind of pattern this was, whether <code>aria-expanded</code> was appropriate here, or if it could even be replaced by a native HTML element.</p>
<p>This article is intended to help you more easily determine whether <code>aria-expanded</code> has been used correctly or not. It’s not about how to implement every pattern discussed here in a production environment! However, in each section, I’ve included additional links so you can dive deeper into the topic.</p>
<div><h2>FYI</h2>
<p>In the following, I will refer to the <a href="https://www.w3.org/WAI/ARIA/apg/patterns/"><em>ARIA Authoring Practices Guide</em> (APG)</a>. It is important to note that the implementations presented there should be understood as proof-of-concept and <em>not</em> ready-to-use accessible patterns! In <a href="https://adrianroselli.com/2019/02/uncanny-a11y.html#APG"><em>Uncanny A11y</em></a>, Adrian Roselli explains in detail why you should be cautious to use these patterns (without testing them thoroughly).</p>
</div>
<p></p>
<h2>The two categories</h2>
<p>As I understand it, collapsible widgets can be divided into two categories: collapsible sections and collapsible interactive elements.</p>
<p>These two categories include patterns that are sometimes very similar, but not identical. In addition, there are patterns that can also reveal and hide content, but these do not require the <code>aria-expanded</code> attribute.</p>
<h2>Collapsible sections</h2>
<p>This category contains two patterns: Accordion and disclosure widget. The former is based on the latter, so let’s take a closer look at the disclosure widget first.</p>
<h3>Disclosure widget</h3>
<pre><code>&lt;button
	aria-controls="content"
	aria-expanded="false"
	type="button"
&gt;
	Show more
&lt;/button&gt;
&lt;div id="content" &gt;
	&lt;p&gt;I am hidden no more!&lt;/p&gt;
&lt;/div&gt;
</code></pre>
<p>This is a basic disclosure widget that requires JavaScript to dynamically change <code>aria-expanded</code> from <code>true</code> to <code>false</code> and to add keyboard support.</p>
<p><code>aria-controls</code> serves as optional support here to establish a direct link between the button and the content. For more information on implementation, see Adrian Roselli’s article <a href="https://adrianroselli.com/2020/05/disclosure-widgets.html"><em>Disclosure Widgets</em></a>.</p>
<p>However, if that is all it needs to do, it is recommended to use the <a href="https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/details"><code>&lt;details&gt;</code> and <code>&lt;summary&gt;</code></a> elements. These two native HTML elements work in all major browsers and are also recognised by AT.</p>
<h3>Accordion</h3>
<p>The accordion pattern is more complex because it combines multiple disclosure widgets. Here, too, <code>aria-expanded</code> is required to communicate the current state to assistive technologies.</p>
<p>There is also often the “exclusive accordion,” which only allows one disclosure widget to be open at a time and closes the others. Not only has Eric Eggert expressed legitimate doubts about this in his article <em><a href="https://yatil.net/blog/exclusive-accordions">Exclusive accordions exclude</a></em>, but Steven Hoober also explains in <a href="https://www.uxmatters.com/mt/archives/2020/05/designing-for-progressive-disclosure.php"><em>Designing for Progressive Disclosure</em></a> why this is a user-hostile pattern.</p>
<p>If you still need to build one, it’s recommended to try out Alexander Lehner’s solution in <a href="https://www.oidaisdes.org/blog/lets-play-accordion/"><em>Let’s Play Accordion with the HTML details element</em></a>. Alternatively, Heydon Pickering also has a suggestion on how <a href="https://inclusive-components.design/collapsible-sections/">collapsible sections</a> could be implemented.</p>
<h2>Collapsible interactive elements</h2>
<p>A collapsible section can also contain interactive elements such as links, but this pattern is most often used to hide large amounts of text in order to save vertical space. You can read about why this can be user-hostile in the linked articles from the previous section.</p>
<div><h2>FYI</h2>
<p>While it is technically possible to nest interactive elements within one another, this should be avoided due to potential accessibility issues. Links within the body text of a disclosure widget are perfectly fine. However, you should avoid placing complex interactive patterns within the disclosure widget. For an introduction to this issue, see the article <a href="https://adrianroselli.com/2016/12/be-wary-of-nesting-roles.html"><em>Be Wary of Nesting Roles</em></a> by Adrian Roselli.</p>
</div>
<p>I was able to identify four patterns for collapsible interactive elements that would require <code>aria-expanded</code>: menus, navigation, tree views and combo box.</p>
<h3>Navigation</h3>
<p>Perhaps because menus and navigation behave so similar, they are often confused with one another. That’s why, in my article <a href="https://www.stevefrenzel.dev/posts/menu-and-navigation-the-difference/"><em>Menu and Navigation: The Difference</em></a>, I explain how to tell them apart. What they do have in common, however, is that when the content becomes extensive, so-called <a href="https://www.w3.org/WAI/tutorials/menus/flyout/">fly-out menus</a> are used to show and hide content:</p>
<pre><code>&lt;nav aria-labelledby="main-nav"&gt;
	&lt;span hidden id="main-nav"&gt;Main&lt;/span&gt;
	&lt;ul&gt;
		&lt;li&gt;&lt;a href="…"&gt;Home&lt;/a&gt;&lt;/li&gt;
		&lt;li&gt;&lt;a href="…"&gt;Shop&lt;/a&gt;&lt;/li&gt;
		&lt;li class="has-submenu"&gt;
			&lt;a href="…" aria-expanded="false"&gt;
				Space Bears
			&lt;/a&gt;
			&lt;ul&gt;
				&lt;li&gt;&lt;a href="…"&gt;Space Bear 6&lt;/a&gt;&lt;/li&gt;
				&lt;li&gt;&lt;a href="…"&gt;Space Bear 6 Plus&lt;/a&gt;&lt;/li&gt;
			&lt;/ul&gt;
		&lt;/li&gt;
		&lt;li&gt;&lt;a href="…"&gt;Mars Cars&lt;/a&gt;&lt;/li&gt;
		&lt;li&gt;&lt;a href="…"&gt;Contact&lt;/a&gt;&lt;/li&gt;
	&lt;/ul&gt;
&lt;/nav&gt;
</code></pre>
<p>In this slightly altered W3C example of a navigation we can see <a href="https://piccalil.li/blog/its-about-time-i-tried-to-explain-what-progressive-enhancement-actually-is/">progressive enhancement</a> in action:</p>
<ul>
<li>If CSS and JavaScript fail to load, this navigation would still work because semantic HTML was used and none of the list items are hidden.</li>
<li><code>aria-expanded</code> indicates that the submenu is currently collapsed.</li>
<li>No additional ARIA is needed to describe the relationships between the list items and their respective parent lists, as this is communicated by the semantic HTML elements <code>&lt;ul&gt;</code> and <code>&lt;li&gt;</code>.</li>
</ul>
<div><h2>FYI</h2>
<p>There is currently no native solution for this pattern that works entirely without JavaScript. However, the proposed <a href="https://www.stevefrenzel.dev/posts/my-thoughts-on-the-focusgroup-attribute-proposal/"><code>focusgroup</code> attribute</a> might eventually fill this gap.</p>
</div>
<p>There is another element related to this pattern that requires the <code>aria-expanded</code> attribute: the so-called hamburger button!</p>
<h3>Menu</h3>
<p>It is very important here to distinguish whether this button is intended to reveal a navigation bar or a menu. If it is a navigation bar, <code>aria-expanded</code> is sufficient. If it is a menu, <a href="https://w3c.github.io/aria/#aria-haspopup"><code>aria-haspopup</code></a> is also required!</p>
<pre><code>&lt;button
  aria-controls="menu"
  aria-haspopup="true"
  aria-expanded="false"
  id="menu-button"
  type="button"
&gt;
  Menu
&lt;/button&gt;
&lt;ul
  aria-activedescendant="mi1"
  aria-labelledby="menu-button"
  id="menu"
  role="menu"
  tabindex="-1"
&gt;
  &lt;li id="mi1" role="menuitem"&gt;Action 1&lt;/li&gt;
  &lt;li id="mi2" role="menuitem"&gt;Action 2&lt;/li&gt;
  &lt;li id="mi3" role="menuitem"&gt;Action 3&lt;/li&gt;
  &lt;li id="mi4" role="menuitem"&gt;Action 4&lt;/li&gt;
&lt;/ul&gt;
</code></pre>
<p>Depending on the scope of the menu, the following ARIA roles may also be necessary:</p>
<ul>
<li><code>aria-labelledby</code> or <code>aria-label</code>: If a menubar has a visible label, the element with role <code>menu</code> or <code>menubar</code> needs to have one of these attributes set to a value that refers to the labelling element.</li>
<li><code>aria-activedescendant</code>: Indicates the relationship between the selected menu item and the parent element in which it is located.</li>
<li><code>aria-checked</code>: If it is possible to select multiple menu items, the corresponding value must be <code>true</code> or <code>false</code>.</li>
<li><code>aria-controls</code>: Establishes a programmatic relationship between the respective menu button and the contained menu items. As mentioned earlier, browser support is sparse, and this feature should be considered more of a nice-to-have.</li>
</ul>
<p>A very good guide to creating progressively enhanced menus is Heydon’s article <a href="https://inclusive-components.design/menus-menu-buttons/"><em>Menus &amp; Menu Buttons</em></a>. The APG explainer for the <a href="https://www.w3.org/WAI/ARIA/apg/patterns/menu-button/"><em>Menu Button Pattern</em></a> is a good place to start if you want to get an overview of the necessary ARIA roles.</p>
<h3>Tree view</h3>
<p>APG distinguishes between two patterns here that are essentially the same but can vary greatly in complexity depending on the implementation:</p>
<ul>
<li><a href="https://www.w3.org/WAI/ARIA/apg/patterns/treeview/">Tree view</a>: “A tree view widget presents a hierarchical list.”</li>
<li><a href="https://www.w3.org/WAI/ARIA/apg/patterns/treegrid/">Tree grid</a>: “A tree grid widget presents a hierarchical data grid consisting of tabular information that is editable or interactive.”</li>
</ul>
<p>In both cases, the <code>aria-expanded</code> attribute is required for the interactive element, which can show or hide additional elements. Here, too, other ARIA roles may be necessary depending on the implementation. This simplified example shows a possible HTML structure. It would also need a significant amount of CSS and JavaScript to convey visual information, as well as information communicated to AT:</p>
<pre><code>&lt;ul role="tree" aria-labelledby="tree-label"&gt;
  &lt;span hidden id="tree-label"&gt;Menu&lt;/span&gt;
  &lt;li
    aria-expanded="false"
    aria-level="1"
    aria-posinset="1"
    aria-selected="false"
    aria-setsize="2"
    role="treeitem"
  &gt;
    &lt;ul role="group"&gt;
      &lt;li
        aria-level="2"
        aria-posinset="1"
        aria-selected="false"
        aria-setsize="2"
        role="treeitem"
      &gt;
        Content 1
      &lt;/li&gt;
      &lt;li
        aria-level="2"
        aria-posinset="2"
        aria-selected="false"
        aria-setsize="2"
        role="treeitem"
      &gt;
        Content 2
      &lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
&lt;/ul&gt;
</code></pre>
<ul>
<li><a href="https://www.w3.org/TR/wai-aria-1.2/#aria-multiselectable"><code>aria-multiselectable</code></a>: Necessary if more than one node can be selected.</li>
<li><code>aria-selected</code> or <code>aria-checked</code>: One of the two is required if more than one node can be selected.</li>
<li><code>aria-labelledby</code> or <code>aria-label</code>: The element with role <code>tree</code> has either a visible label referenced by <code>aria-labelledby</code> or a value specified for <code>aria-label</code>.</li>
<li><a href="https://www.w3.org/TR/wai-aria-1.2/#aria-orientation"><code>aria-orientation</code></a>: If the <code>tree</code> element is horizontally oriented, it has the value <code>aria-orientation="horizontal"</code>.</li>
<li><a href="https://www.w3.org/TR/wai-aria-1.2/#aria-level"><code>aria-level</code></a>, <a href="https://www.w3.org/TR/wai-aria-1.2/#aria-setsize"><code>aria-setsize</code></a> and <a href="https://www.w3.org/TR/wai-aria-1.2/#aria-posinset"><code>aria-posinset</code></a>: These might be necessary, if “the complete set of available nodes is not present in the DOM due to dynamic loading as the user moves focus in or scrolls the tree”.</li>
<li><code>tabindex</code>: Depending on your implementation, you might need to implement a <a href="https://webaim.org/techniques/keyboard/tabindex#zero-negative-one">roving <code>tabindex</code></a>.</li>
</ul>
<p>If you need a starting point for creating one of these components, check out <a href="https://blog.pope.tech/2023/07/06/create-an-accessible-tree-view-widget-using-aria/"><em>Create an accessible tree view widget using ARIA</em></a> by Pope Tech.</p>
<h3>Combo box</h3>
<p><a href="https://nerdy.dev/nice-select">The native <code>&lt;select&gt;</code> element can now be styled freely</a> and also offers keyboard support, as well as robust accessibility support by default, so there should be no need to build this element yourself.</p>
<pre><code>&lt;label for="pet-select"&gt;Choose a pet:&lt;/label&gt;

&lt;select id="pet-select" name="pets"&gt;
  &lt;option value=""&gt;Please choose an option:&lt;/option&gt;
  &lt;option value="dog"&gt;Dog&lt;/option&gt;
  &lt;option value="cat"&gt;Cat&lt;/option&gt;
&lt;/select&gt;
</code></pre>
<p>If it is necessary after all, the <code>aria-expanded</code> attribute is required here as well. In addition, <code>aria-haspopup</code>, <code>aria-activedescendant</code>, and <code>aria-selected</code> may also be necessary, depending on the implementation. Furthermore, <code>aria-autocomplete</code> and <code>aria-labelledby</code> or <code>aria-label</code> may also be required.</p>
<p>Make sure to give the <a href="https://w3c.github.io/aria/#combobox">specs of</a> <a href="https://www.w3.org/TR/wai-aria-1.2/#combobox"><code>combobox</code></a> <a href="https://w3c.github.io/aria/#combobox">role</a> a good read before building it. Although, why go through all that trouble when the web platform provides an element that can already do all of this? 🤗</p>
<p></p>
<h2>Similar but different</h2>
<p>The following patterns can also reveal content at the touch of a button. However, this isn't so much a “fold-out” as it is a “pop-up”! Instead of using <code>aria-expanded</code> to communicate the state of an interactive element, you would rather use <code>aria-haspopup</code> oder <code>aria-modal</code>.</p>
<h3>Dialog (Modal)</h3>
<p><a href="https://www.w3.org/WAI/ARIA/apg/patterns/dialog-modal/">According to APG, the dialog pattern requires the <code>aria-modal</code> attribute</a>, but let me stop you right there. <a href="https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/dialog">The native <code>&lt;dialog&gt;</code> element</a> has had solid browser support for quite some time and is supported by assistive technologies. In addition, we now have <a href="https://developer.mozilla.org/en-US/docs/Web/API/Invoker_Commands_API">invoker commands</a> at our disposal, which means you <em>could</em> implement this pattern without using any JavaScript! <a href="https://www.scottohara.me/blog/2023/01/26/use-the-dialog-element.html">Use the dialog element (reasonably)</a> to save time and frustration and to keep your users happy.</p>
<p>In this example, we’re using invoker commands. If you want to play it safe, you could also add a fallback using JavaScript, in case invoker commands are not supported yet in the browser of your choice.</p>
<pre><code>&lt;button command="show-modal" commandfor="my-dialog"&gt;
	Open dialog
&lt;/button&gt;

&lt;dialog id="my-dialog"&gt;
	&lt;h2&gt;Progressive enhancement&lt;/h2&gt;
  &lt;p&gt;This dialog uses no JavaScript!&lt;/p&gt;
  &lt;button commandfor="my-dialog" command="close"&gt;
	  Close
  &lt;/button&gt;
&lt;/dialog&gt;
</code></pre>
<h3>Tabbed interfaces</h3>
<p>Unfortunately, there is no native, JavaScript-free solution for this pattern yet, so you’ll either have to build it yourself using <code>aria-haspopup</code> or use a ready-made solution from a third-party provider.</p>
<p>Personally, I prefer the first option so you know what’s going on under the hood. A good starting point for this is <a href="https://inclusive-components.design/tabbed-interfaces/"><em>Tabbed Interfaces</em></a> by Heydon Pickering.</p>
<p>Additionally (<a href="https://www.w3.org/WAI/ARIA/apg/patterns/tabs/">according to the APG</a>), other ARIA roles such as <code>aria-controls</code>, <code>aria-selected</code>, <code>aria-orientation</code>, <code>aria-label</code>, or <code>aria-labelledby</code> may be added, depending on how you implement it.</p>
<h3>Tooltip</h3>
<p>This pattern is an interesting case, as there is <a href="https://www.w3.org/WAI/ARIA/apg/patterns/tooltip/">no specific example of it in the APG</a>. Nevertheless, Heydon has taken on the challenge here as well and explains in <a href="https://inclusive-components.design/tooltips-toggletips/"><em>Tooltips &amp; Toggletips</em></a> how to create an accessible tooltip on your own.</p>
<p>Alternatively, you could experiment with how the <a href="https://developer.mozilla.org/en-US/docs/Web/API/Popover_API">Popover API</a> and invoker commands work together with AT to implement this pattern natively and without JavaScript:</p>
<pre><code>&lt;button command="toggle-popover" commandfor="info"&gt;
  What is two-factor authentication?
&lt;/button&gt;

&lt;div id="info" popover="auto" role="tooltip"&gt;
  Two-factor authentication adds a second verification step (like a code
  from your phone) when you log in.
&lt;/div&gt;
</code></pre>
<p>Once again, we need very little to get a lot done! Let’s break down what’s happening here:</p>
<ul>
<li><code>command="toggle-popover"</code>: The Popover API provides to show or hide a popover, aka toggling it.</li>
<li><code>commandfor="info"</code>: Here we’re using the Popover API to target the element with <code>id="info"</code> in order to connect the button with this particular element.</li>
<li><code>popover="auto"</code>: This enables the so-called “light-dismiss”, meaning that clicking outside of the tooltip or pressing <kbd>ESC</kbd> will close it.</li>
<li><code>role="tooltip"</code>: Without explicitly specifying the role, it would have a role of “group”.</li>
</ul>
<h2>Wrapping up</h2>
<p>Originally, this article was supposed to be about expandable buttons. Then I realised it would make more sense to write specifically about the ARIA role <code>aria-expanded</code>. After further research, I discovered that this attribute is no longer absolutely necessary for some patterns because the web platform has evolved significantly in recent years!</p>
<p>Thanks to native solutions like <code>&lt;detail&gt;</code>, <code>&lt;summary&gt;</code>, and <code>&lt;dialog&gt;</code>, as well as the Popover and Invoker Commands API, it’s possible to implement many of the patterns discussed here with minimal effort and even without JavaScript.</p>
<p>Nevertheless, it’s very important that not only these native (and in some cases very new) solutions are thoroughly tested with assistive technology, but also those you’ve created yourself.</p>
<p>It’s even more important to note that the APG patterns are not suitable for production use but should be understood solely as illustrations of how to use various ARIA roles. That’s why I’ve added an alternative example to each pattern presented, in which it was implemented with progressive enhancement in mind.</p>
        
        ]]></description>
        
      </item>
    
      <item>
        <title>Proxy and Reflect</title>
        <link>https://piccalil.li/blog/proxy-and-reflect/?ref=articles-rss-feed</link>
        <dc:creator><![CDATA[Mat Marquis]]></dc:creator>
        <pubDate>Thu, 09 Jul 2026 11:55:00 GMT</pubDate>
        <guid isPermaLink="true">https://piccalil.li/blog/proxy-and-reflect/?ref=articles-rss-feed</guid>
        <description><![CDATA[<div><h2>FYI</h2>
I'm Mat, author of Piccalilli's very own [JavaScript for Everyone](https://piccalil.li/javascript-for-everyone), a course designed to help you make the jump from junior- to senior developer. As ever, I'm here to teach you JavaScript — not just the _what_, but the _how_ and the _why_ of JavaScript.
<p>In this <em>specific</em> instance, I'm here to teach you about using the <code>Proxy</code> constructor and <code>Reflect</code> object. These are features of the language worth having in your toolbox, naturally — but just as importantly they allow us to graze up against some of JavaScript's innermost workings, and in doing so, better learn the shape of the mechanisms that power the language. That's the kind of know-how that makes a <em>senior</em> developer.</p>
<p>Now, the keen-eyed among you may notice that this article is shaped conspicuously like an excerpted lesson from said course — and yet, nowhere on the <a href="https://piccalil.li/javascript-for-everyone/lessons">lesson listing page</a> does it appear. "Whatever could <em>that</em> mean," you might ask.</p>
<p>Well, <a href="https://piccalil.li/javascript-for-everyone#sign-up">stay tuned</a>.
</p>
<p>An object is a collection of properti— <em>hey</em>! Don't you roll your eyes at me! Listen, I know that phrase is approaching "mitochondria is the powerhouse of the cell"-level here, but I'm going somewhere new with this, I promise.</p>
<p>Ahem. <em>An object is a collection of properties</em>, internal slots, and internal methods that allow us to interact with those properties. When you punch <code>({}).theProperty</code> into your developer console, you do so expecting the following choose-your-own-adventure operation to kick off:</p>
<ul>
<li>Is that key somewhere along the object's prototype chain?
<ul>
<li>Yes.
<ul>
<li>Is it a data property?
<ul>
<li>Result in the property descriptor's <code>value</code>.</li>
</ul>
</li>
<li>Is it an accessor property?
<ul>
<li>Invoke the getter method, and result in the value returned by the that method.</li>
</ul>
</li>
</ul>
</li>
<li>No.
<ul>
<li>Result in <code>undefined</code>.</li>
</ul>
</li>
</ul>
</li>
</ul>
<p>This use of property accessor syntax doesn't <em>itself</em> represent all those steps — rather, dot notation is the API that kicks off an <a href="https://tc39.es/ecma262/#sec-ordinary-object-internal-methods-and-internal-slots-get-p-receiver">internal <code>[[Get]]</code> operation defined by the specification</a>, and the steps taken by that <code>[[Get]]</code> operation determine the result.</p>
<p>We can't get in there and tinker with the specific steps taken by an object's internal methods, nor would we likely want to — that's JavaScript engine turf. What we can do is <em>intercept</em> those operations by way of a <strong>proxy</strong> object, and in doing so we can alter, expand, or wholesale <em>redefine</em> the way that an object works, at its most fundamental levels.</p>

<p>See? And I bet you thought this one was gonna be boring.</p>

<p>The <code>Proxy</code> constructor can be used to create an object that acts as a proxy for a target object, allowing you to intercept and redefine operations performed on the latter by using the former as an intermediary.</p>
<p>When invoked with <code>new</code>, <code>Proxy</code> results in an object — no surprises there. It accepts two arguments: a <strong>target object</strong> and a <strong>handler object</strong>:</p>
<pre><code>const targetObject = {
  theProperty: "A string."
};
const handlerObject = {};
const theProxyObject = new Proxy( targetObject, handlerObject );

console.log( theProxyObject );

/* Result (Firefox, expanded):
Proxy { &lt;target&gt;: {…}, &lt;handler&gt;: {} }
  &lt;target&gt;: Object { theProperty: "A string." }
  &lt;handler&gt;: Object {  }
*/

/* Result (Chrome, expanded):
Proxy(Object) {theProperty: 'A string.'}
  [[Handler]]: Object
  [[Target]]: Object
  [[IsRevoked]]: false
*/
</code></pre>
<p>Nothing <em>too</em> surprising in the console, here. Our newly-minted proxy object contains a reference to our target object and a set of internal slots, using that <code>&lt;&gt;</code> or <code>[[]]</code> notation — depending on the browser — which makes it clear that we're not meant to interact with these slots <em>directly</em>, the way we would interact with a string-based property key. <code>[[Target]]</code> is an internal slot representing the the object we want to act on, complete with the property we defined. <code>[[Handler]]</code> represents the handler object that acts as an intermediary for interacting with the target object.</p>

<p>I bet that <code>[[isRevoked]]</code> in Chrome jumped out at you right away. Don't worry, we haven't hit upon some big 2010-style discrepancy in browser behavior — just a difference in how an internal slot is surfaced. We’ll get there in a bit.</p>

<p>If we change the value associated with the property we've defined on the target object, that change is reflected by the proxy object's reference to it:</p>
<pre><code>const targetObject = {
  theProperty: "A string."
};
const handlerObject = {};
const theProxyObject = new Proxy( targetObject, handlerObject );

targetObject.theProperty = "Something else.";

console.log( theProxyObject );
/* Result (expanded):
Proxy { &lt;target&gt;: {…}, &lt;handler&gt;: {} }
  &lt;target&gt;: Object { theProperty: "Something else." }
  &lt;handler&gt;: Object {  }
*/
</code></pre>
<p>At a glance, this feels like some classic "by-reference" stuff — "object values are stored by reference," "objects are a collection of properties," "objects are the powerhouse of the script," <em>et cetera</em>. Remember, however, that we're not talking about variables or properties — in this case, the proxy object is <em>itself</em> a reference to the target object. That proxy object can be used in place of the target object, wholesale:</p>
<pre><code>const targetObject = {
  theProperty: "A string."
};
const handlerObject = {};
const theProxyObject = new Proxy( targetObject, handlerObject );

console.log( theProxyObject.theProperty );

// Result: A string.
</code></pre>
<p>Accessing a property of the proxy object is effectively accessing that property on the target object, <em>by way of</em> the proxy object. You can't define an own property on the proxy object as usual, either — instead, that property will be defined on the target:</p>
<pre><code>const targetObject = {
  theProperty: "A string."
};
const handlerObject = {};
const theProxyObject = new Proxy( targetObject, handlerObject );

theProxyObject.theOtherProperty = "Another string";

console.log( theProxyObject );

/* Result (expanded):
Proxy { &lt;target&gt;: {…}, &lt;handler&gt;: {} }
  &lt;target&gt;: Object { theProperty: "A string.", theOtherProperty: "Another string" }
  &lt;handler&gt;: Object {  }
*/
</code></pre>
<p>Used the way you've seen it so far here, well, we've basically just created an unusual reference value with extra steps, but that's only because we're not asking the handler object to <em>do</em> anything by way of our proxy object — here, the handler object is basically acting as a translator from one language to that same language. The use case becomes a little more clear when we start creating <strong>handler functions</strong> on its handler object, sometimes called <strong>traps</strong>:</p>
<pre><code>const targetObject = {
  theProperty: "A string."
};
const handlerObject = {
  get() {
    return "Something else entirely.";
  }
};
const theProxyObject = new Proxy( targetObject, handlerObject );

console.log( theProxyObject.theProperty );
// Result: "Something else entirely." )
</code></pre>
<p>There’s a <a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Proxy/Proxy#handler_functions">corresponding trap for every operation that can be performed on an object</a>, all named in relatively predictable ways. That <code>get()</code> method defined on the handler object is a trap for the <code>[[Get]]</code> internal method, which is fired whenever you attempt to access the value of an object property. By making it return an explicit value this way, well, we messed up a perfectly good <code>[[Get]]</code> operation is what we did — we've intercepted that operation and changed what should result from it. Now no matter what we do with our <code>targetObject</code>, attempting to access the value of any property will result in exactly what we said it should:</p>
<pre><code>const targetObject = {
  theProperty: "A string."
};
const handlerObject = {
  get() {
    return "You've just activated my trap function!";
  }
};
const theProxyObject = new Proxy( targetObject, handlerObject );

console.log( theProxyObject );

/* Result (expanded):
Proxy { &lt;target&gt;: {…}, &lt;handler&gt;: {…} }
  &lt;target&gt;: Object { theProperty: "A string." }
  &lt;handler&gt;: Object { get: get() }
*/

targetObject.newProperty = true;

console.log( theProxyObject );

/* Result (expanded):
Proxy { &lt;target&gt;: {…}, &lt;handler&gt;: {…} }
  &lt;target&gt;: Object { theProperty: "A string.", newProperty: true }
  &lt;handler&gt;: Object { get: get() }
*/

console.log( theProxyObject.newProperty );
// Result: You've just activated my trap function!

console.log( theProxyObject[ "newProperty" ] );
// Result: You've just activated my trap function!
</code></pre>
<p>And like any function, we can have that trap function perform whatever tasks we want:</p>
<pre><code>const targetObject = {
  theProperty: true
};
const handlerObject = {
  get() {
    console.log( "Psych!" );
    return false;
  }
};
const theProxyObject = new Proxy( targetObject, handlerObject );

console.log( theProxyObject.theProperty );

/* Result:
Psych!
false
*/
</code></pre>
<p>Naturally, that includes manipulating the results of those operations with a little more finesse than a <code>console.log</code> and a string. The <code>get()</code> method of a handler object accepts three arguments: one representing the target object, one representing the key for the property being accessed, and one representing the "receiver," which is a little higher-concept: the receiver argument represents the value of <code>this</code> within the context of the getter method — a reference to <a href="https://piccalil.li/blog/javascript-what-is-this/">the object bound to the</a> <code>get</code> <a href="https://piccalil.li/blog/javascript-what-is-this/">method</a> <a href="https://piccalil.li/blog/javascript-when-is-this/">at the time when that method is invoked</a> — that might sound fraught, as <code>this</code> is wont to be, but in most cases that will be the target object.</p>
<p>Given these arguments, we're able to use our <code>get</code> method to access and manipulate the values of our target object's properties:</p>
<pre><code>const targetObject = {
  theProperty: 10
};
const handlerObject = {
  get( target, propertyKey, receiver) {
    return target[ propertyKey ] * 2;
  }
};
const theDoubleObject = new Proxy( targetObject, handlerObject );

console.log( theDoubleObject.theProperty );
// Result: 20
</code></pre>
<div><h2>FYI</h2>  
Okay, back to that `[[isRevoked]]` now that you know what would _be_ revoked. There's a second way of creating a proxy object: the `Proxy.revocable()` factory function.
<pre><code>const targetObject = {};
const handlerObject = {};
const revocableProxy = Proxy.revocable( targetObject, handlerObject );
</code></pre>
<p>The object that results from calling <code>Proxy.revocable</code> (with the same target and handler arguments as <code>new Proxy</code>) will contain two properties. The first property, <code>proxy</code>, pretty predictably contains a proxy object, and the value of this property is identical to the proxy object that would result from using <code>Proxy</code> as a constructor with those same arguments.</p>
<p>The second property is a <code>revoke</code> method that can be used to detach that proxy object from its target object:</p>
<pre><code>const revocableProxy = Proxy.revocable({}, {});

console.log( revocableProxy );

/* Result (expanded):
Object { proxy: Proxy, revoke: () }
  proxy: Proxy { &lt;target&gt;: {}, &lt;handler&gt;: {} }
    &lt;target&gt;: Object {  }
    &lt;handler&gt;: Object {  }
  revoke: function ()
*/

/* Result (Chrome, expanded):
Object { proxy: Proxy, revoke: () }
  proxy: Proxy(Object)
    [[Handler]]: Object
    [[Target]]: Object
    [[IsRevoked]]: false
  revoke: ƒ ()
*/
</code></pre>
<p>Calling <code>revoke()</code> un-proxies your object — once invoked, the proxy object that was returned by <code>Proxy.revocable()</code> will no longer hold a reference to your target or handler object:</p>
<pre><code>const revocableProxy = Proxy.revocable({}, {});

revocableProxy.revoke();

console.log( revocableProxy );

/* Result (expanded):
Object { proxy: Proxy, revoke: () }
  proxy: Proxy { &lt;target&gt;: {}, &lt;handler&gt;: {} }
    &lt;target&gt;: null
    &lt;handler&gt;: null
  revoke: function ()
*/

/* Result (Chrome, expanded):
Object { proxy: Proxy, revoke: () }
  proxy: Proxy(Object)
    [[Handler]]: null
    [[Target]]: null
    [[IsRevoked]]: true
  revoke: ƒ ()
*/
</code></pre>
<p>Just keep in mind that "revoked" means <em>revoked</em> — once <code>revoke</code> is called, there are no take-backs. If no other references to the proxy object exist, it becomes eligible for garbage collection — likewise your target and handler objects, if not referenced elsewhere.</p>
<p>Chrome's JavaScript engine exposes that [[isRevoked]] internal slot in the developer console for at-a-glance debugging purposes, I assume — there's nothing in the language (currently) that gives us direct access to the value of that internal slot. Revoking a proxy works just as well in either browser, no worries there.</p>
</div>
<p>Every one of an object's internal methods has a corresponding trap, which means that you're able to alter the basal behavior of <em>any object, at every possible level, language-wide</em>:</p>
<pre><code>const targetObject = {
  theProperty: "Still here."
};

const handlerObject = {
  deleteProperty( target, key) {
    console.log( "No." );
    return false;
  },
  has( target, key ) {
    console.log( "None of your business." );
    return false;
  },
  getPrototypeOf( target ) {
    console.log( "Who knows?" );
    return null;
  }
};

const theImmovableObject = new Proxy( targetObject, handlerObject );

delete theImmovableObject.theProperty;
/* Result:
No.
false
*/

console.log( theImmovableObject.theProperty );
// Result: Still here.

console.log( "theProperty" in theImmovableObject );
/* Result: 
None of your business.
false
*/

console.log( Object.getPrototypeOf( theImmovableObject ) );
/* Result: 
Who knows?
null
*/
</code></pre>
<p>So, y'know, just make sure you do it right.</p>
<p>No pressure or anything.</p>
<p>Oh, hey, speaking-of:</p>
<p></p>
<h2>Reflect</h2>
<p>You might have noticed a few things about the code examples you've seen so far here. One, we're partying like it's 2009 — accessing properties using <em>olde timey</em> bracket notation.</p>
<p>Two, it's a little unsettling to <code>[[Get]]</code> a property value within the context of changing the way <code>[[Get]]</code>-ing a property value works, using the very syntaxes we’re changing. It works fine, but the vibes, in strict technical terms, are <em>off</em>.</p>
<p>Third, and by <em>far</em> most importantly: breaking with assumptions as major as "when I create a property on an object, it will happen the way I expect" will <em>absolutely</em> lead to issues in our code somewhere down the line, if not right away. Being able to alter the essential nature of objects themselves draws a razor-thin line between "exciting" and "terrifying," especially when it comes to future maintainability, and the idea of working on a codebase littered with objects that <em>may or may not work like objects</em> is the stuff of nightmares.</p>
<p>For that reason, the <a href="https://tc39.es/ecma262/#sec-proxy-object-internal-methods-and-internal-slots-set-p-v-receiver">ES-262 specification helpfully outlines the following <strong>invariants</strong></a> for, say, <code>[[Set]]</code> — that is, the rules that a <code>[[Set]]</code> operation must follow:</p>
<blockquote>
<ul>
<li>The result of [[Set]] is a Boolean value.</li>
<li>Cannot change the value of a property to be different from the value of the corresponding target object property if the corresponding target object property is a non-writable, non-configurable own data property.</li>
<li>Cannot set the value of a property if the corresponding target object property is a non-configurable own accessor property that has <code>undefined</code> as its [[Set]] attribute.</li>
</ul>
</blockquote>
<p>Once you start tinkering with how a <code>[[Set]]</code> operation works, well, following those rules is now on you. If I were to write something along the lines of the following:</p>
<pre><code>const handlerObject = {
  set( target, propertyKey, value, receiver) {
    return target[ propertyKey ] = value * 2;
  }
};

const setDoubler = new Proxy( {}, handlerObject );

setDoubler.theProperty = 2;

console.log( setDoubler.theProperty );
// Result: 4
</code></pre>
<p>My <code>[[Set]]</code> operation didn't return a boolean value the way JavaScript expects, per the sacred invariant rules of <code>[[Set]]</code>. I mean, this snippet will still <em>work,</em> in that we’re outside strict mode, the returned value is coerced to a Boolean, and this one happens to be truthy. It won’t work in every context:</p>
<pre><code>"use strict";
const handlerObject = {
  set( target, propertyKey, value, receiver) {
    return target[ propertyKey ] = value * 2;
  }
};

const setDoubler = new Proxy( {}, handlerObject );

setDoubler.theProperty = 0;

console.log( setDoubler.theProperty );

// Result: Uncaught TypeError: proxy set handler returned false for property '"theProperty"'
</code></pre>
<p>We could rewrite this to explicitly <code>return</code> that expected boolean value, naturally, but even dealing with this simple handler method we find ourselves in "I need to be careful to always do <em>this</em> in <em>this</em> way" territory lest we introduce fundamentally bugged objects to our codebase. Nobody needs that.</p>
<p>That brings us to the <code>Reflect</code> object: a collection of static methods, each with the same name and parameters as our proxy handler methods. <code>Reflect</code> gives us some guardrails to address all of the above concerns (bad vibes and all) by providing us with a set of methods for interacting with objects that all ensure we never deviate too far from how objects are <em>meant</em> to work.</p>
<p><code>Reflect</code> is a <strong>namespace object</strong> — an ordinary object made up of static properties and methods, like the <code>Math</code> or <a href="https://piccalil.li/blog/date-is-out-and-temporal-is-in/"><code>Temporal</code></a> objects:</p>
<pre><code>console.log( Reflect );

/* Result (expanded):
  apply: function apply()
  construct: function construct()
  defineProperty: function defineProperty()
  deleteProperty: function deleteProperty()
  get: function get()
  getOwnPropertyDescriptor: function getOwnPropertyDescriptor()
  getPrototypeOf: function getPrototypeOf()
  has: function has()
  isExtensible: function isExtensible()
  ownKeys: function ownKeys()
  preventExtensions: function preventExtensions()
  set: function set()
  setPrototypeOf: function setPrototypeOf()
  Symbol(Symbol.toStringTag): "Reflect"
*/
</code></pre>
<p>Every one of those methods maps to the names of your proxy object handler methods and expects the same parameters, in the same way — their syntax matches the context we're working in much more than bracket notation. That alone works <em>wonders</em> for the vibes, if you ask me:</p>
<pre><code>const handlerObject = {
  set( target, propertyKey, value) {
    return Reflect.set( target, propertyKey, value * 2 );
  }
};

const setDoubler = new Proxy( {}, handlerObject );

setDoubler.theProperty = 2;

console.log( setDoubler.theProperty );
// Result: 4
</code></pre>
<p>No fuss, no muss, and — most importantly — no poring over the ES-262 specification to ensure that we're not accidentally deviating from <em>The Rules of Objects as They Are Played</em>, because <code>Reflect.set()</code> performs the <code>[[Set]]</code> operation we want <em>and</em> returns the expected boolean value, per the specification. With <code>Proxy</code> we can change how objects work, and with <code>Reflect</code> we can make sure our changed objects still work the way objects are <em>meant</em> to.</p>
<p></p>
<h2>Putting it together</h2>
<p>When we put this all together, we can use proxy objects and <code>Reflect</code> to perform tasks like validating data:</p>
<pre><code>const validationHandler = {
  set( target, propertyKey, value, receiver ) {
    if( typeof value === "string" ) {
      return Reflect.set( target, propertyKey, value );
    } else {
      console.error( "This object only accepts strings." );
      return false;
    }
  }
};
const validatedObject = new Proxy({}, validationHandler );

validatedObject.newProperty = true;
// Result: This object only accepts strings.

console.log( validatedObject );
/* Result:
Proxy { &lt;target&gt;: {}, &lt;handler&gt;: {…} }
  &lt;target&gt;: Object {  }
  &lt;handler&gt;: Object { set: set(target, propertyKey, value, receiver) }
*/
</code></pre>
<p>...Or <a href="https://codepen.io/Wilto/pen/dPNNKdJ">setting and maintaining the internal state of an object</a> — for example, the number of times an object's given property has been accessed:</p>
<pre><code>const handlerObject = {
  accessCounter( target, accessed) {
    Reflect.set( target, "timesAccessed", accessed ? accessed + 1 : 1 );
  },
  set( target, key, value) {
    this.accessCounter( target, Reflect.get( target, "timesAccessed" ) );

    return Reflect.set( target, key, value );
  },
  get( target, key) {
    this.accessCounter( target, Reflect.get( target, "timesAccessed" ) );

    return Reflect.get( target, key );
  }
};

const stateObject = new Proxy({}, handlerObject );

console.log( stateObject );

/* Result (expanded):
Proxy { &lt;target&gt;: {…}, &lt;handler&gt;: {…} }
  &lt;target&gt;: Object {  }
  &lt;handler&gt;: Object { accessCounter: accessCounter(accessed), set: set(target, propertyKey, value, receiver), get: get(target, propertyKey, receiver) }
*/

stateObject.newProperty = true;
// Result: true

console.log( stateObject );
/* Result (expanded):
Proxy { &lt;target&gt;: {…}, &lt;handler&gt;: {…} }
  &lt;target&gt;: Object { timesAccessed: 1, newProperty: true }
  &lt;handler&gt;: Object { accessCounter: accessCounter(accessed), set: set(target, propertyKey, value, receiver), get: get(target, propertyKey, receiver) }
*/

console.log( stateObject.newProperty );
// Result: true

console.log( stateObject );
/* Result (expanded):
Proxy { &lt;target&gt;: {…}, &lt;handler&gt;: {…} }
  &lt;target&gt;: Object { timesAccessed: 2, newProperty: true }
  &lt;handler&gt;: Object { accessCounter: accessCounter(accessed), set: set(target, propertyKey, value, receiver), get: get(target, propertyKey, receiver) }
*/
</code></pre>
<p>Those are kind of novelties when we're acting on a single object, sure — but with the <a href="https://piccalil.li/javascript-for-everyone">approaches and syntaxes you've already learned</a> and a little imagination, it isn't hard to see where proxy objects could be used to create an entire state <em>system</em> with just a few more lines of code, and without the need for bulky frameworks or third-party tools:</p>
<pre><code>function reactiveState( target) {
  const subscribed = new Map();

  return new Proxy({
    ...target,
    subscribe( key, callbackFunc) {
        // If it isn't there already, add it to `subscribed`:
        if( !subscribed.has( key ) ) {
          subscribed.set( key, [] );
        }
        // Associate the callback function with the subscribed object:
        subscribed.get( key ).push( callbackFunc );
      }
    }, {
      set( target, key, value, receiver) {
        const result = Reflect.set( target, key, value );

        // If this is the subscribed-to property...
        if( subscribed.has( key ) ) {
          // ...invoke the callback function with the explicit `this` value of the original object:
          subscribed.get( key ).forEach( callbackFunc =&gt; callbackFunc.call( receiver, key ) );
        }

        return result;
      }
    });
}

// Declare a callback function to be called when the state of an object changes:
const callbackLogger = function( key) {
  const enCardinal = new Intl.PluralRules( "en-US" );
  const counter = this[ key ];
  const pluralize = count =&gt; enCardinal.select( counter ) === "one" ? `` : `s`;

  console.info(`${this.component }.${ key } has been changed ${ counter } time${ pluralize( counter ) }.`);
};

const widget = reactiveState({ component: "widget", count: 0 });
const gizmo = reactiveState({ component: "gizmo", otherCounter: 0 });

// When the `count` property of our widget object changes, call the callbackLogger:
widget.subscribe( 'count', callbackLogger );

// When the `otherCounter` property of our gizmo object changes, call the callbackLogger:
gizmo.subscribe( 'otherCounter', callbackLogger );

widget.count++;
// Result: widget.count has been changed 1 time.

widget.count++;
// Result: widget.count has been changed 2 times.

// We're not subscribed to a `count` property for the gizmo object, so nothing happens here:
gizmo.count++;

// But we _are_ subscribed to an `otherCounter` property for the gizmo object:
gizmo.otherCounter++;
// Result: gizmo.otherCounter has been changed 1 time.

widget.count++;
// Result: widget.count has been changed 3 times.
</code></pre>
<p>Now, listen. I don’t want to trot out any <em>more</em> cliches here, but I won't deny that there's an impulse to wrap this lesson up with a call for temperance — to leave you with a warning about how proxy objects are "as powerful as they are dangerous," and "great power-slash-responsibility" and <em>et cetera</em>, then maybe a little bit about how there is, at least, some cold comfort to be found in <code>Reflect</code>. That's not coming from <em>nowhere</em>, that impulse — altering how the principle building blocks of a language work, at their most fundamental levels, can break stuff <em>pretty bad</em>. No two ways about that.</p>
<p>You know me, though: if I wanted to spend my cortisol on "worrying about getting things wrong," I would've gone to medical school or learned PHP. If you ask me, <code>Proxy</code> and <code>Reflect</code> are brand new, shining examples of the enduring spirit of JavaScript: use the language to change the language, and find new ways to solve problems never even imagined by the hundreds of people who've gotten a hand on <a href="https://262.ecma-international.org/16.0/index.html">the ball</a> since 1995.</p>

<p>Well, "brand new" as of 2015.</p>

<p>The <code>Reflect</code> object gives you some vital future-headache-prevention guardrails, and you should use them, of course — that's what they're there for.</p>
<p>I won't tell you to "be careful," though. Get in there and break stuff; there's no better way to learn. Besides, nothin' reloading the page can't fix, right?</p></div>
        
        ]]></description>
        
      </item>
    
      <item>
        <title>Publishing on the Atmosphere with Standard.site</title>
        <link>https://piccalil.li/blog/publishing-on-the-atmosphere-with-standardsite/?ref=articles-rss-feed</link>
        <dc:creator><![CDATA[Declan Chidlow]]></dc:creator>
        <pubDate>Thu, 25 Jun 2026 11:55:00 GMT</pubDate>
        <guid isPermaLink="true">https://piccalil.li/blog/publishing-on-the-atmosphere-with-standardsite/?ref=articles-rss-feed</guid>
        <description><![CDATA[<p><a href="https://standard.site/">Standard.site</a> provides a set of lexicons for publishing long-form content on the internet using the same protocol used under the hood by Bluesky.</p>
<p>If you are wondering what 'lexicons' and 'the Atmosphere' are, don't fret. This article will explain what they mean, why you should care about Standard.site, and walk you through exactly how you can implement Standard.site using some simple JavaScript or a plugin for your favourite content management system.</p>
<h2>What Standard.site is and why you should care</h2>
<p>If Bluesky is the network's answer to short-form microblogging, think of Standard.site as its equivalent for blogs, newsletters, and long-form journalism.</p>
<p>At its core, Standard.site is an open schema that dictates how articles and essays should be formatted as data. When you publish a blog post normally, it lives on your website and relies on scrapers or RSS feeds to be shared. By adopting the Standard.site lexicon, your long-form content becomes a natively understood piece of data on the decentralised web.</p>
<p>One of the most overt benefits we get from defining Standard.site lexicons for our publication are enhanced rich embeds on Bluesky, like this:</p>
<p><img src="https://piccalil.b-cdn.net/images/blog/standard-site-bluesky-post.png" alt="A mock bluesky post showing a linked blog post, featuring a richer UI experience with direct call to action to view publisher" /></p>
<p>We also get many other benefits. For example, I was surprised to see that the professional identity network Sifa ID <a href="https://sifa.id/p/vale.rocks#publications">displayed my posts upon my profile</a>. It was also pleasant to see my posts naturally populate on readers like <a href="https://pckt.blog/">pckt</a>, <a href="https://docs.surf/">Docs.surf</a>, <a href="https://potatonet.app/">potatonet</a>, and <a href="https://leaflet.pub/">Leaflet</a> without any additional work on my part.</p>
<p>The strength of AT Protocol is that data is interoperable and can be shared, which plays into the strength of Standard.site, which is that a single set of well structured-schemas. The result is that various indexers and tools can all work with the available data in their own ways, knowing how it'll be structured. Your content can be moved between hosts without losing your data or the audience you've built, and there is no single controlling authority. To get further into this, however, we must first establish an understanding of the AT Protocol.</p>
<p></p>
<h2>Understanding the AT Protocol</h2>
<p>To implement Standard.site, you will want to understand the Authenticated Transfer Protocol (known colloquially as the 'AT Protocol' or 'atproto') at least at a surface level. The AT Protocol is a decentralised system designed to give users ownership of their data.</p>
<p>To explain it at its simplest, you have a Personal Data Server (PDS) which hosts user accounts. Currently, the largest PDS is provided by Bluesky, but anyone can run their own. A PDS holds lots of user accounts, and each user account can hold records, which are data.</p>
<p>Each user account can be identified by a globally unique DID (Decentralised Identifier) that acts as their permanent ID. A DID looks like this: <code>did:plc:7qg6mz2xtzozxkgbcvf4pdnu</code>. Each account also has a handle, which comes in the form of a DNS record. We can see this in that people on Bluesky's PDS who haven't configured a custom domain for their account have a handle like this: <code>bsky.bsky.social</code>. If you navigate to that in a browser, it'll take you to the Bluesky page: <a href="https://bsky.bsky.social/"></a><a href="https://bsky.bsky.social">https://bsky.bsky.social</a>.</p>
<p>Each account features a data repository that holds collections of JSON records. These JSON records must follow specific structures called lexicons. Lexicons are just schemas, like JSON-Schema or OpenAPI, which define how the JSON must be structured and formatted. Records are put into 'collections', which we can think of as folders. A collection is identified by a Namespace Identifier (NSID) which makes reference to a domain to identify schemas.</p>
<p>Let’s run through an example with Bluesky so we can really get a handle on things. When a user signs up to Bluesky, a <code>self</code> record is created in the <code>app.bsky.actor.profile</code> collection of that user's data repository with information about the account, like its name and profile description. Piccalilli's looks something like this:</p>
<pre><code>{
  "uri": "at://did:plc:lyk2pixxcmyeu4jrapaq26fy/app.bsky.actor.profile/self",
  "cid": "bafyreihzo3igobmunvk6tmsaqgyatyw5gona4kiayvm2f62lymc5dzqjvu",
  "value": {
    "$type": "app.bsky.actor.profile",
    "createdAt": "2024-08-01T12:44:51.324Z",
    "description": "Level up your front-end skills. Stay for the approachable, friendly content and go away with transferable skills you can use day to day.",
    "displayName": "Piccalilli"
  }
}
</code></pre>
<p>Then, every time a post is made, or the Piccalilli account likes something, or blocks someone, or does anything else on Bluesky, a new record is created to represent that action. For instance, when Piccalilli reposts something, a new record is created under the <code>app.bsky.feed.repost</code> collection.</p>
<p><strong>Almost everything is a record, inside a collection, under a user account (identified by a DID), on a PDS.</strong></p>
<p>Notably, everything on ATProto is public. There is no concept of private records, which means we can go out and inspect or reference all the data on the protocol. There are a number of tools for inspecting AT Protocol data, but <a href="https://atproto.at/">Taproot</a> and <a href="https://pdsls.dev/">PDSLs</a> are my favourites. Search for your account handle, and you'll be greeted by your underlying records. Have a poke around to help wrap your head around the structure and how everything fits together.</p>
<p></p>
<h2>The two core records</h2>
<p>Now we've (hopefully) got at least (somewhat) of a (fledgeling) understanding of AT Protocol, we can start hooking up Standard.site. To get started with Standard.site, we need to create two specific types of records in your AT Protocol repository:</p>
<ol>
<li>A Publication Record, which defines information about our publication itself.</li>
<li>Document Records, which contain information about individual articles themselves.</li>
</ol>
<p>It is these records that Bluesky and the rest of the Atmosphere will reference. You can <a href="https://atproto.com/guides/writing-data#writing-data">create them any one of a number of ways</a>. AT Protocol is very open, and you can create records via a variety of methods, but for the purposes of this article, I'll be showing a JavaScript approach using the official <a href="https://npmx.dev/package/@atproto/api"><code>@atproto/api</code></a> npm package.</p>
<p>You will need to authenticate to create these records. The easiest way to do so is by creating an app password under Privacy and Security in Bluesky's settings. An app password gives access to your account and looks like this: <code>fg2g-xob3-xl78-5ezy</code>.</p>
<div><h2>FYI</h2>
<p>An app password gives <em>full</em> access to your account and bypasses multi-factor authentication. Be <em>extremely</em> careful with it. It is a secret, and you should take extreme care of it. This illustrative code shows including the app password inline, but you should consider putting it in an environment variable.</p>
<p>If you think your app password has been made public, you should revoke it, which can be done through the same interface you created it.</p>
</div>
<h3>Publication record</h3>
<p>The first step in support is having a publication record adhering to the <a href="https://standard.site/docs/lexicons/publication/"><code>site.standard.publication</code> lexicon</a>. You only need to create this record once per-publication.</p>
<p>Using the AT Protocol SDK, you can authenticate and create this underlying JSON record for your site with the script below, replacing the template values here with your publication's details.</p>
<p>For the purpose of illustration, this script only sets required properties.</p>
<pre><code>import { AtpAgent } from "@atproto/api";

// Initialise the agent (use your specific PDS if not on Bluesky)
const agent = new AtpAgent({ service: "&lt;https://bsky.social&gt;" });

async function createPublicationRecord() {
  // 1. Authenticate (Always use an App Password, never your main password)
  await agent.login({
    identifier: "your-handle.bsky.social",
    password: "your-app-password",
  });

  const did = agent.session.did;

  // 2. Define the Publication Record
  const publicationRecord = {
    $type: "site.standard.publication",
    url: "&lt;https://example.com&gt;",
    name: "My Awesome Blog",
  };

  // 3. Write the record to your repository
  try {
    const response = await agent.com.atproto.repo.createRecord({
      repo: did,
      collection: "site.standard.publication",
      record: publicationRecord,
    });

    console.log("Publication record created!");
    console.log("Your AT-URI is:", response.data.uri);
  } catch (error) {
    console.error("Failed to create publication:", error);
  }
}

createPublicationRecord();
</code></pre>
<p>If this script was successful, it should output a message reading 'Publication record created!', followed by an AT-URI. Save this, because we'll need it later. In the future if we need to amend this record, we can <a href="https://atproto.com/guides/writing-data#updating-records">revise the record directly</a>.</p>
<h3>Theming</h3>
<p>Though the above script is great, we can take it a bit further and add some more pizazz by <a href="https://standard.site/docs/lexicons/theme/">defining a theme</a>. You can create a theme to lend some more style to how your content displays in readers and how Bluesky embeds it. This is done by adding theming fields to your publication record. You need to specify a <code>background</code>, <code>foreground</code>, <code>accent</code>, and <code>accentForeground</code>. If you're setting any of these values, you must set <em>all</em> of them.</p>
<p>Bluesky uses <code>accent</code> and <code>accentForeground</code> like so:</p>
<p><img src="https://piccalil.b-cdn.net/images/blog/standard-site-theme-diagram.png" alt="A diagram pointing out the accent and accentForeground in the context of the call to action button" /></p>
<p>You should check that your foreground and background and accent and accent foreground all have appropriate contrast. Bluesky previously took these values directly, but now they do <a href="https://bsky.app/profile/esb.lol/post/3mnilfmgqns2d">some contrast adjustment of their own</a>.</p>
<p>You, unfortunately, cannot change the text which appears on the Bluesky embed button, which will always be 'View Publication' if you publish yourself. Some external Standard.site enabled services have their own special buttons with custom text and icons, but these are hard coded in the Bluesky client.</p>
<h3>Verifying your publication</h3>
<p>Next, we have the optional step of creating a file at <code>/.well-known/site.standard.publication</code> containing the AT-URL outputted by our record creation script. This verifies that your domain controls your publication record.</p>
<p>Most services, Bluesky included, don't require this verification. Indeed, some hosts might not let you write to the <code>/.well-known</code> path. However, if you <em>can</em> create a file named <code>site.standard.publication</code> within <code>/.well-known</code> and put your AT-URL within it, your publication will be more widely supported. This verification only needs to be done once.</p>
<p>You can check this has been created correctly by going to your URL on your site. For example, for my personal website, I can visit <a href="https://vale.rocks/.well-known/site.standard.publication">https://vale.rocks/.well-known/site.standard.publication</a> in my browser and see my AT-URI:</p>
<pre><code>at://did:plc:7qg6mz2xtzozxkgbcvf4pdnu/site.standard.publication/3mn2c332ulp2u
</code></pre>
<h3>Document records</h3>
<p>Now that your publication record exists, you need to create per-document records following the <a href="https://standard.site/docs/lexicons/document/"><code>site.standard.document</code> lexicon</a>. Every document needs its own record.</p>
<p>Standard.site supports having your document's content in the record, which is the approach that Offprint, pckt, Leaflet, and some other publishing platforms use. Whether you do this is up to you.</p>
<p>If you <em>do</em> include the content, then it can be displayed natively in Standard.site reader applications, and Bluesky embeds will provide a reading time estimate. For the purpose of this script, I'll again only be including required properties.</p>
<pre><code>import { AtpAgent } from "@atproto/api";
const agent = new AtpAgent({ service: "&lt;https://bsky.social&gt;" });

async function publishDocumentRecrd() {
  await agent.login({
    identifier: "your-handle.bsky.social",
    password: "your-app-password",
  });

  const did = agent.session.did;

  // 1. Define the Document Record
  const documentRecord = {
    $type: "site.standard.document",
    site: `at://your-did/site.standard.publication/your-pub-rkey`, // Full AT-URI of your publication
    title: "My New Post",
    publishedAt: "2026-06-11T00:00:00.000Z",
  };

  // 2. Write the record to your repository
  try {
    const response = await agent.com.atproto.repo.createRecord({
      repo: did,
      collection: "site.standard.document",
      record: documentRecord,
    });

    console.log("Success! Document record published to the Atmosphere:");
    console.log(response.data.uri);
  } catch (error) {
    console.error("Failed to publish document:", error);
  }
}

publishDocumentRecord();
</code></pre>
<p>If this script was successful, it should output a message reading 'Success! Document record published to the Atmosphere:', followed by an AT-URI. Save this, because we need it to verify the document.</p>
<h3>Verifying your document</h3>
<p>To complete the two-way verification, the HTML <code>&lt;head&gt;</code> of your live article must contain a link tag pointing back to the document record you just created:</p>
<pre><code>&lt;link rel="site.standard.document" href="at://did:plc:your-did/site.standard.document/the-record-rkey" /&gt;
</code></pre>
<p>Once these ends are tied together, your webpage and document record point to each other, and clients across the decentralised web can seamlessly reference the record.</p>
<h3>Adding images</h3>
<p>If you look through the Standard.site docs at all, you might notice references to icons and cover images. To make use of these, <a href="https://atproto.com/guides/images-and-video">we must upload a <em>blob</em></a>, which is what unstructured data (like images) within a repository are called.</p>
<p>We need to upload the blob first, so that we can refer to it with a reference in our record. Here is an example of uploading an image as a blob and then referencing it to use it as a cover image.</p>
<pre><code>import fs from "fs";
import { AtpAgent } from "@atproto/api";
const agent = new AtpAgent({ service: "&lt;https://bsky.social&gt;" });

async function uploadImageAndPublishDocument() {
  await agent.login({
    identifier: "your-handle.bsky.social",
    password: "your-app-password",
  });

  const did = agent.session.did;

  // 1. Read the local image file into a buffer
  const imageBuffer = fs.readFileSync("./path/to/your/cover.jpg");

  // 2. Upload the blob to your repository
  const { data: blobResponse } = await agent.com.atproto.repo.uploadBlob(
    imageBuffer,
    { encoding: "image/jpeg" }
  );

  console.log("Blob successfully uploaded!");

  // 3. Define the Document Record, attaching the returned blob reference
  const documentRecord = {
    $type: "site.standard.document",
    site: `at://your-did/site.standard.publication/your-pub-rkey`,
    title: "My New Post with a Cover Image",
    publishedAt: "2026-06-18T12:00:00.000Z",
    cover: blobResponse.blob, // This links the blob to your document
  };

  // 4. Write the document record to your repository
  try {
    const response = await agent.com.atproto.repo.createRecord({
      repo: did,
      collection: "site.standard.document",
      record: documentRecord,
    });

    console.log("Document record with cover image published:");
    console.log(response.data.uri);
  } catch (error) {
    console.error("Failed to publish document:", error);
  }
}

uploadImageAndPublishDocument();
</code></pre>
<p></p>
<h2>Setting up Standard.site on CMS platforms</h2>
<p>If writing JavaScript to push records manually sounds tedious, or you're already using a major content management system, you might prefer to have the process handled for you. How you integrate Standard.site depends very much on how your own site is built, and some platforms have ready-made integrations via plugins that handle all of the above behind the scenes:</p>
<ul>
<li><strong>WordPress:</strong> Has <a href="https://wordpress.org/plugins/atmosphere/">the ATmosphere plugin</a> and <a href="https://wordpress.wireservice.net/">the Wireservice plugin</a>.</li>
<li><strong>CraftCMS:</strong> Has <a href="https://plugins.craftcms.com/standard-site">a plugin simply called Standard.site</a>.</li>
<li><strong>Obsidian:</strong> Has a <a href="https://github.com/SootyOwl/obsidian-standard-site">community plugin</a>.</li>
<li><strong>Static sites</strong>: Generators like 11ty, Hugo, Astro, and Jekyll can use a dedicated CLI tool called <a href="https://sequoia.pub/">Sequoia</a>.</li>
</ul>
<h2>Checking it works</h2>
<p>Obviously, creating all these records and configuring all this Standard.site business is a bit useless if it doesn't actually work. The easiest way to test is by just plopping a link to a post on Bluesky and hoping it embeds, but if it doesn't, it can feel a tad opaque when it comes to figuring out what went wrong.</p>
<p>To rectify this, you can <a href="https://site-validator.fly.dev/">make use of Standard.site Validator</a> by <a href="https://octet-stream.net/">Thomas Karpiniec</a>. Consider returning to <a href="https://atproto.at/">Taproot</a> or <a href="https://pdsls.dev/">PDSLs</a> to study and review your records. Both will let you know if anything fails to validate and where things went awry.</p>
<h2>Further reading</h2>
<p>For some further reading, Piccalilli's own <a href="https://wil.to/">Mat Marquis</a>, creator of the <a href="https://piccalil.li/javascript-for-everyone">JavaScript for Everyone</a> course, has written his own posts documenting his <a href="https://wil.to/posts/standard-site/">understanding of</a> and <a href="https://wil.to/posts/implementing-standard-site/">implementation of</a> Standard.site on his own blog.</p>
<p>If you want to have multiple Standard.site publications under a single domain, then Jason Lengstorf has <a href="https://codetv.dev/blog/multiple-standard-site-publications-on-one-website">an in-depth guide on the Code.TV blog</a>.</p>
        
        ]]></description>
        
      </item>
    
      <item>
        <title>A Front-end developer’s guide to the hybrid mobile app development landscape</title>
        <link>https://piccalil.li/blog/a-front-end-developers-guide-to-the-hybrid-mobile-app-development-landscape/?ref=articles-rss-feed</link>
        <dc:creator><![CDATA[Rachael Yomtoob]]></dc:creator>
        <pubDate>Thu, 04 Jun 2026 11:55:00 GMT</pubDate>
        <guid isPermaLink="true">https://piccalil.li/blog/a-front-end-developers-guide-to-the-hybrid-mobile-app-development-landscape/?ref=articles-rss-feed</guid>
        <description><![CDATA[<p>Just as with every aspect of my life, I find it hard to identify my software development skills. At my heart, I am a developer, though I spent way too much time as a high school senior fretting about whether or not I’d become an engineer. On paper, my job title has been product owner for almost the same amount of time as engineer/developer, but I was still writing code and reviewing PRs. Then comes the question of what kind of developer am I? Web developer? Mobile developer? Front-end? Full-stack? So many titles, but not a single one that fully encompasses my experience. I can tell you one thing that I’m not is a back-end developer 😆</p>
<p>All that said, for the purposes of this article, I’m a front-end web developer who has spent their 7 year career building a product for testing accessibility of mobile apps. I’d like to give you — a front-end web developer, who may not have mobile experience — a quick run-through to the mobile apps built with web technologies landscape.</p>
<h2>What is a hybrid mobile app?</h2>
<p>As a front-end web developer, we’re used to our HTML content being rendered in the browser whether it’s being generated by a framework like React or Vue, or being served from an HTML file.</p>
<p>The mobile app world is a quite different story where the rendered content doesn’t follow agreed upon and maintained guidance, like web standards. For example, Apple and Google completely govern their own native tech stacks for building iOS and Android apps respectively. Those tech stacks create elements on the screen called <code>Views</code>. Then there’s the 3rd party cross-platform frameworks such as React Native, .NET MAUI (previously Xamarin), Flutter. These all allow you to build both an Android and iOS app, and sometimes a web and/or desktop app, from a single code base, but they each do that process a bit differently to each other.</p>
<p>React Native and .NET MAUI generate <code>Views</code> that mimic or are very similar to native mobile <code>Views</code>. They are designed to use technologies familiar to React and .NET developers: TypeScript/JavaScript with CSS and C# with XAML. Flutter is in it’s own league because it uses Dart to build <code>Widgets</code> that are rendered by it’s own graphics engine, bypassing the concept of native <code>Views</code> entirely.</p>
<p>While all these frameworks are often referred to in discussions of hybrid mobile apps, I prefer to distinguish them as cross-platform or code-once frameworks and reserve hybrid to specifically describe the subset of apps which present a blend of web and mobile content.</p>
<p>We’ll define a hybrid mobile app as follows: “A mobile app which primarily features HTML content rendered in <code>WebViews</code> alongside some native mobile content or functionality”</p>
<p>A <code>WebView</code> is a native mobile <code>View</code> which acts as an embedded web browser component to render HTML content inside of it instead of native controls like sliders or buttons. The mobile browser apps such as Safari and Chrome utilize a <code>WebView</code> as the main component with the addition of peripherals like the URL bar and back button. That’s a route to web developers to feel <em>empowered</em> rather than hindered.</p>
<p>The cross-platform framework names I haven’t mentioned yet are by this definition, hybrid app development frameworks: Ionic, Capacitor, Cordova/PhoneGap. It’s a bit complicated to untangle what each framework is and does, so we’ll dive into that in the next section.</p>
<p></p>
<h2>Hybrid App Development Frameworks</h2>
<p>Let’s start by talking about Apache Cordova/Adobe PhoneGap, since it’s the original hybrid framework, created back in 2009. Here’s the overview statement on <a href="https://cordova.apache.org/docs/en/latest/guide/overview/index.html">the Cordova documentation site</a>:</p>
<blockquote>
<p>Apache Cordova is an open-source mobile development framework. It allows you to use standard web technologies - HTML5, CSS3, and JavaScript for cross-platform development. Applications execute within wrappers targeted to each platform, and rely on standards-compliant API bindings to access each device's capabilities such as sensors, data, network status, etc.</p>
</blockquote>
<p>There’s some history with the project being acquired by Adobe, including in an offering called PhoneGap, but that was discontinued in 2020. Cordova is still alive and actively maintained, but you may see references to PhoneGap while looking for hybrid app development topics.</p>
<p>Cordova isn’t a UI framework, but rather a runtime environment for your web app on a mobile device. This means you could take an existing web app that you built with your favorite front-end tools and package it up to render inside a <code>WebView</code> in both an iOS and Android app. Now there’s quite a lot of reasons why you wouldn’t want to to that, but let’s cover the other runtime frameworks first.</p>
<p>Capacitor is a newer option for a hybrid framework to ship your web content wrapped in a mobile app, developed in 2018 as a modern alternative to Cordova/PhoneGap. The intro on <a href="https://capacitorjs.com/docs">the Capacitor docs site</a> reads:</p>
<blockquote>
<p>Capacitor is a cross-platform native runtime that makes it easy to build performant mobile applications that run natively on iOS, Android, and more using modern web tooling. Representing the next evolution of Hybrid apps, Capacitor creates <strong>Web Native apps</strong>, providing a modern native container approach for teams who want to build web-first without sacrificing full access to native SDKs when they need it.</p>
</blockquote>
<p>What separates Capacitor from Cordova is that it’s built and maintained by the Ionic framework team. While the former are both native runtime frameworks, Ionic is actually a front-end UI framework for hybrid apps. Ionic can be used with both Cordova and Capacitor, but Capacitor is designed specifically to work in conjunction with Ionic. Here’s the introduction on <a href="https://ionicframework.com/docs">the Ionic docs site</a>:</p>
<blockquote>
<p>Ionic is an open source UI toolkit for building performant, high-quality mobile apps using web technologies — HTML, CSS, and JavaScript — with integrations for popular frameworks like <a href="https://ionicframework.com/docs/angular/overview">Angular</a>, <a href="https://ionicframework.com/docs/react/overview">React</a>, and <a href="https://ionicframework.com/docs/vue/overview">Vue</a>.</p>
</blockquote>
<p>Now we’re finally in the front-end developer’s domain! Before we dig into it too much, let’s go over some important design concepts for hybrid apps.</p>
<p></p>
<h2>Designing a Hybrid App</h2>
<p>Like I said earlier, you can use either Capacitor or Cordova, to wrap your existing web front-end into a <code>WebView</code> inside a mobile app, but there are some considerations to make since content built for the web is structured quite differently than content built for mobile.</p>
<p>For example, a common pattern in web apps is a single page with lot of content that can be scrolled to or jumped to with links/landmarks. Whereas, in a mobile app, content is often better presented in small chunks with navigation through a flow of multiple screens. Mobile app user experience is built around the fact that the content is displayed in a small viewport, but the web was originally built for desktop and adapted to suite mobile devices as their browser became more and more capable.</p>
<p>I’ve spoke with folks in the industry that claim most users can’t tell whether an app is presenting native content or a <code>WebView</code>, but I find that hard to believe. Maybe I’m biased because I know too much about both, but it says a lot if a user downloads your app instead of visiting your website since there’s a much higher barrier to entry. They likely took the extra effort to open the App Store or Play Store and remember or find their account password to install your app because they want a native mobile experience. One thing I can confidently say is that a screen reader user will definitely know the difference since it will literally announce <code>WebView</code> when they hit one, and the landmarks are different, like the fact that Android only has 1 level of heading compared to the 6 levels in HTML <code>&lt;h1&gt;-&lt;h6&gt;</code>.</p>
<p>So all that said, how do you build a stellar hybrid app user experience? By using the concepts of responsive and mobile-first web design of course!</p>
<p>Figma has <a href="https://www.figma.com/resource-library/mobile-first-design/#core-principles-of-the-mobile-first-design-process">a wonderful article</a> which covers the topic focusing on websites, but can be applied to hybrid apps as well:</p>
<blockquote>
<p>“Mobile-first website design flips the traditional Web development approach. Instead of starting with a desktop layout and scaling down, you begin with a mobile website design. This approach prioritizes smaller screens, slower Internet connections, and the needs of on-the-go users.”</p>
</blockquote>
<p>Another layer to mobile-first design is that Android and iOS each have a look and feel uniquely branded to Google and Apple. We’ve established that the web and mobile ecosystems are very different, but iOS and Android are almost just as different too. So you’re faced with some choices: design 2 experiences, pick iOS or Android and ship it to the other platform anyways, or roll your own that ideally caters to both. Trust me, I’ve seen it all, and my least favorite is an Android styled app shipped to iOS</p>
<p>While companies/brands love the idea of a consistent experience across platforms, users prefer your app to look behave like the rest on their device do. A lot of factors go into choosing a design pattern for an app such as target audience, development resource capacity, etc, so let’s set that aside and finally dig into building the bits</p>
<h2>Building a Hybrid app Front-end</h2>
<p>I want to bring us back to Ionic because <a href="https://ionicframework.com/docs/components">its UI component library</a> allowing front-end developers to build seamless experiences for both iOS and Android from a single codebase using technologies you know and love. It’s full of mobile-first components which render on iOS matching Apple’s aesthetics and on Android matching Material Design, Google’s open-source design system. These components are available in Angular, React, Vue, and plain ole JavaScript with beautiful interactive demos on the documentation site.</p>
<p>Let’s take a closer look at a Toggle component which is commonly used in the settings of a mobile app. In React, you import the <code>IonToggle</code> from Ionic’s library and use it just like you would any component for a web-first app.</p>
<pre><code>import React from 'react';
import { IonToggle } from '@ionic/react';

function Example() {
  return (
    &lt;&gt;
      &lt;IonToggle&gt;Default Toggle&lt;/IonToggle&gt;
      &lt;IonToggle checked={true}&gt;Checked Toggle&lt;/IonToggle&gt;
      &lt;IonToggle disabled={true}&gt;Disabled Toggle&lt;/IonToggle&gt;
      &lt;IonToggle checked={true} disabled={true}&gt;
        Disabled Checked Toggle
      &lt;/IonToggle&gt;
    &lt;/&gt;
  );
}
export default Example;
</code></pre>
<p>On iOS, you’ll get 4 toggles in Apple’s well known style.</p>
<p><img src="https://piccalil.b-cdn.net/images/blog/4-apple-toggle-elements.png" alt="4 iOS toggle elements in different states: default, checked, disabled and disabled checked" /></p>
<p>For Android, the recognisable material design elements are used.</p>
<p><img src="https://piccalil.b-cdn.net/images/blog/4-android-toggle-elements.png" alt="4 Android toggle elements in different states: default, checked, disabled and disabled checked" /></p>
<p>All of the Ionic components can be styled with CSS custom properties and standard CSS, so you can achieve that consistent experience across platforms by modifying the iOS and Android styles slightly or make them match exactly.</p>
<p></p>
<h2>Accessibility</h2>
<p>As digital accessibility advocate, I can’t write an article without touching on the topic. I think of accessibility in 2 realms: compliance and user experience.</p>
<p>The realm of compliance focuses on building digital content that adheres to the <a href="https://www.w3.org/TR/WCAG22/">Web Content Accessibility Guidelines (WCAG)</a>. These are the guidelines which legislature around the world reference, and large enterprises strive to meet. There isn’t a similar de facto standard for mobile app accessibility despite the immense difference in how content is rendered, but instead, there’s only adaptations and interpretations of WCAG for mobile.</p>
<p>On the other hand, Apple and Google put emphasis on accessibility through the user experience of the iOS and Android ecosystems. There’s <a href="https://developer.apple.com/design/human-interface-guidelines/accessibility">Apple’s Human Interface Guidelines with a foundations section dedicated to accessibility</a>, and <a href="https://developer.android.com/guide/topics/ui/accessibility">the Android Developer guide on accessibility.</a></p>
<p>There’s no straightforward way to make a perfectly accessible app today, but that’s ok! Just as with most things in life, it’s about finding the right balance. On the compliance side, that <code>WebView</code> inside a mobile app is rendering HTML, so WCAG criteria can be directly applied, so there’s certainly advantages for hybrid apps in that regard. However, accessibility compliance doesn’t always equate to a high quality user experience, which is where the advice from Apple and Google come into play.</p>
<h2>Wrapping up</h2>
<p>While the world of mobile apps can be intimidating to a front-end web developer, it’s certainly worth exploring, even to be aware of the landscape for if you find yourself in a project in the future.</p>
<p>With that in mind, I hope this article is a useful introduction and shows that you don’t have to step completely out of your comfort zone thanks to the concept of hybrid mobile apps, providing a very <em>familiar</em> experience. It makes them a useful jumping off point!</p>
        
        ]]></description>
        
      </item>
    
    </channel>
  </rss>
