<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: korial29</title>
    <description>The latest articles on DEV Community by korial29 (@korial29).</description>
    <link>https://dev.to/korial29</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4053326%2F848f4c10-9fc3-436d-bfcc-fba7e9ecc573.jpg</url>
      <title>DEV Community: korial29</title>
      <link>https://dev.to/korial29</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/korial29"/>
    <language>en</language>
    <item>
      <title>A PDF Viewer Web Component That Doesn't Make You Choose Between Themeable and Lightweight</title>
      <dc:creator>korial29</dc:creator>
      <pubDate>Wed, 29 Jul 2026 13:48:44 +0000</pubDate>
      <link>https://dev.to/korial29/a-pdf-viewer-web-component-that-doesnt-make-you-choose-between-themeable-and-lightweight-27e9</link>
      <guid>https://dev.to/korial29/a-pdf-viewer-web-component-that-doesnt-make-you-choose-between-themeable-and-lightweight-27e9</guid>
      <description>&lt;p&gt;Drop a PDF into a web page and you usually end up picking one of two uncomfortable options: embed PDF.js's own viewer in an &lt;code&gt;&amp;lt;iframe&amp;gt;&lt;/code&gt; (works, but you're stuck behind an iframe boundary — no theming, no styling it to match your app), or pull in &lt;code&gt;pdfjs-dist&lt;/code&gt; directly and build your own toolbar, zoom controls, search, and page navigation from PDF.js's fairly low-level rendering API.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.npmjs.com/package/lit-pdf-viewer" rel="noopener noreferrer"&gt;&lt;code&gt;lit-pdf-viewer&lt;/code&gt;&lt;/a&gt; is a native web component that skips both compromises: a real custom element (not an iframe), with a full toolbar already built, that you can theme like any other element on your page.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fakujk8p6lz68qb4jivxu.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fakujk8p6lz68qb4jivxu.png" alt=" " width="799" height="493"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Why a custom element, specifically
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;&amp;lt;lit-pdf-viewer&amp;gt;&lt;/code&gt; is a &lt;a href="https://developer.mozilla.org/en-US/docs/Web/API/Web_components" rel="noopener noreferrer"&gt;Web Component&lt;/a&gt; — no React/Vue/Angular adapter needed. Because custom elements are a browser primitive rather than a framework integration, it behaves identically everywhere:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;script &lt;/span&gt;&lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"module"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;lit-pdf-viewer&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/script&amp;gt;&lt;/span&gt;

&lt;span class="nt"&gt;&amp;lt;lit-pdf-viewer&lt;/span&gt; &lt;span class="na"&gt;src=&lt;/span&gt;&lt;span class="s"&gt;"path/to/document.pdf"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/lit-pdf-viewer&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Or inside a Lit component, same element, same API:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;lit-pdf-viewer&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;html&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;LitElement&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;lit&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;customElement&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;lit/decorators.js&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="p"&gt;@&lt;/span&gt;&lt;span class="nd"&gt;customElement&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;my-app&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;MyApp&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="nc"&gt;LitElement&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;render&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;html&lt;/span&gt;&lt;span class="s2"&gt;`&amp;lt;lit-pdf-viewer src="path/to/document.pdf"&amp;gt;&amp;lt;/lit-pdf-viewer&amp;gt;`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It fills the height of its host, so give it one:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="nt"&gt;lit-pdf-viewer&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;display&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;block&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;height&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;100vh&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  The bundle cost, honestly
&lt;/h2&gt;

&lt;p&gt;Adding the component costs about &lt;strong&gt;17 KB gzipped&lt;/strong&gt; — that's the whole thing, Lit runtime included, before you've loaded a single PDF. &lt;a href="https://mozilla.github.io/pdf.js/" rel="noopener noreferrer"&gt;PDF.js&lt;/a&gt; itself (~196 KB gzipped) is only pulled in via a dynamic &lt;code&gt;import()&lt;/code&gt; once a &lt;code&gt;&amp;lt;lit-pdf-viewer&amp;gt;&lt;/code&gt; actually connects to the page or a print is triggered — not just for having the package listed in your dependencies.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's in the toolbar
&lt;/h2&gt;

&lt;p&gt;Not a bare canvas — the toolbar, search, and page controls ship built in:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Zoom: page-fit, page-width, actual size, or a custom numeric level&lt;/li&gt;
&lt;li&gt;Page navigation: previous/next, first/last, direct page input&lt;/li&gt;
&lt;li&gt;Text search, including multi-phrase search and a whole-word mode&lt;/li&gt;
&lt;li&gt;Rotate clockwise / counter-clockwise&lt;/li&gt;
&lt;li&gt;Text selection and hand-tool modes, full-screen, download, print (with an optional separate higher-quality print source)&lt;/li&gt;
&lt;li&gt;Keyboard accessibility
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;terms&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;invoice total&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;due date&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;

&lt;span class="nx"&gt;html&lt;/span&gt;&lt;span class="s2"&gt;`
  &amp;lt;lit-pdf-viewer src="invoice.pdf" .searchQueries=&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;terms&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;gt;&amp;lt;/lit-pdf-viewer&amp;gt;
`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="c"&gt;&amp;lt;!-- Serve a screen-optimized PDF but print a higher-quality one --&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;lit-pdf-viewer&lt;/span&gt; &lt;span class="na"&gt;src=&lt;/span&gt;&lt;span class="s"&gt;"document-screen.pdf"&lt;/span&gt; &lt;span class="na"&gt;printSrc=&lt;/span&gt;&lt;span class="s"&gt;"document-print.pdf"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/lit-pdf-viewer&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Theming through the shadow boundary
&lt;/h2&gt;

&lt;p&gt;Every visual surface — toolbar background, buttons, icons, the search bar, tooltips, the error banner — is driven by CSS custom properties with sensible defaults. Set them on the element itself, or any ancestor (&lt;code&gt;:root&lt;/code&gt; included), and they inherit down through every nested shadow root automatically — no &lt;code&gt;::part()&lt;/code&gt; gymnastics required for the common case:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="nt"&gt;lit-pdf-viewer&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="py"&gt;--litpdf-toolbar-background&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;#1e1e1e&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="py"&gt;--litpdf-icon-color&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;#eee&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="py"&gt;--litpdf-accent-color&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;#ff6b35&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="py"&gt;--litpdf-surface-background&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;#2a2a2a&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="py"&gt;--litpdf-text-color&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;#eee&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  i18n out of the box
&lt;/h2&gt;

&lt;p&gt;Toolbar, search bar, error panel, and loading indicator are translated in English, French, Spanish, and German, auto-detected from &lt;code&gt;navigator.language&lt;/code&gt; with English as the fallback:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;lit-pdf-viewer&lt;/span&gt; &lt;span class="na"&gt;src=&lt;/span&gt;&lt;span class="s"&gt;"doc.pdf"&lt;/span&gt; &lt;span class="na"&gt;locale=&lt;/span&gt;&lt;span class="s"&gt;"es"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/lit-pdf-viewer&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Override individual strings (or add a language of your own) without losing the rest of the resolved locale:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nx"&gt;html&lt;/span&gt;&lt;span class="s2"&gt;`
  &amp;lt;lit-pdf-viewer
    src="doc.pdf"
    locale="en"
    .translations=&lt;/span&gt;&lt;span class="p"&gt;${{&lt;/span&gt;
      &lt;span class="nl"&gt;toolbar&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;print&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Print document&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="nx"&gt;search&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;placeholder&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Find in this document...&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;,
    }}
  &amp;gt;&amp;lt;/lit-pdf-viewer&amp;gt;
`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Where this sits relative to the alternatives
&lt;/h2&gt;

&lt;p&gt;A few other PDF-in-the-browser web components already exist, and they're worth knowing about: &lt;a href="https://www.npmjs.com/package/pdfjs-viewer-element" rel="noopener noreferrer"&gt;&lt;code&gt;pdfjs-viewer-element&lt;/code&gt;&lt;/a&gt; wraps PDF.js's own default viewer inside an isolated &lt;code&gt;&amp;lt;iframe&amp;gt;&lt;/code&gt; — a solid drop-in choice if you don't need it to look like the rest of your app. &lt;a href="https://github.com/justinfagnani/pdf-viewer-element" rel="noopener noreferrer"&gt;&lt;code&gt;pdf-viewer-element&lt;/code&gt;&lt;/a&gt; (from Lit's own creator) stays intentionally close to the metal — a rendering primitive rather than a full toolbar. &lt;code&gt;lit-pdf-viewer&lt;/code&gt; targets the space between the two: a real, styleable custom element with the toolbar, search, and i18n already built, for the common case of "I want a PDF viewer in my page that looks like it belongs there."&lt;/p&gt;

&lt;h2&gt;
  
  
  Install
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install &lt;/span&gt;lit-pdf-viewer
npx lit-pdf-viewer-install &amp;lt;your-public-dir&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The second command copies the PDF.js worker, CMaps, and icon font into your public directory — required static assets the component serves alongside your app.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;npm: &lt;a href="https://www.npmjs.com/package/lit-pdf-viewer" rel="noopener noreferrer"&gt;lit-pdf-viewer&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;source: &lt;a href="https://github.com/korial29/lit-pdf-viewer" rel="noopener noreferrer"&gt;github.com/korial29/lit-pdf-viewer&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;live demo: &lt;a href="https://korial29.github.io/lit-pdf-viewer/" rel="noopener noreferrer"&gt;korial29.github.io/lit-pdf-viewer&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Apache-2.0 licensed. Issues and PRs welcome.&lt;/p&gt;

</description>
      <category>webcomponents</category>
      <category>javascript</category>
      <category>pdf</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Building Form-Associated Custom Elements with ElementInternals</title>
      <dc:creator>korial29</dc:creator>
      <pubDate>Wed, 29 Jul 2026 13:33:33 +0000</pubDate>
      <link>https://dev.to/korial29/building-form-associated-custom-elements-with-elementinternals-24f0</link>
      <guid>https://dev.to/korial29/building-form-associated-custom-elements-with-elementinternals-24f0</guid>
      <description>&lt;p&gt;Web components have been able to fully participate in native HTML forms since 2023 — submit their value with &lt;code&gt;FormData&lt;/code&gt;, trigger &lt;code&gt;:invalid&lt;/code&gt;, reset alongside the &lt;code&gt;&amp;lt;form&amp;gt;&lt;/code&gt;, the works. The API that makes it possible, &lt;a href="https://developer.mozilla.org/en-US/docs/Web/API/ElementInternals" rel="noopener noreferrer"&gt;&lt;code&gt;ElementInternals&lt;/code&gt;&lt;/a&gt;, is now supported in Chromium, Firefox, and Safari 16+.&lt;/p&gt;

&lt;p&gt;The catch: nobody tells you it's about 40 lines of repetitive plumbing per component. This post walks through what that plumbing looks like, then shows a small reusable mixin (&lt;a href="https://www.npmjs.com/package/form-control-mixin" rel="noopener noreferrer"&gt;&lt;code&gt;form-control-mixin&lt;/code&gt;&lt;/a&gt;) that gets rid of it.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ff64aogi2xti2xzd14cxx.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ff64aogi2xti2xzd14cxx.png" alt=" " width="800" height="849"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What "form-associated" actually buys you
&lt;/h2&gt;

&lt;p&gt;A form-associated custom element can:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;submit a value as part of the form's &lt;code&gt;FormData&lt;/code&gt;, under its &lt;code&gt;name&lt;/code&gt; attribute&lt;/li&gt;
&lt;li&gt;expose validity through the same &lt;code&gt;ValidityState&lt;/code&gt; interface native inputs use (&lt;code&gt;valueMissing&lt;/code&gt;, &lt;code&gt;tooShort&lt;/code&gt;, &lt;code&gt;patternMismatch&lt;/code&gt;, …)&lt;/li&gt;
&lt;li&gt;react to the form being reset, its ancestor &lt;code&gt;&amp;lt;fieldset&amp;gt;&lt;/code&gt; being disabled, or the browser restoring state (bfcache, autofill)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;All of that lives behind &lt;code&gt;HTMLElement.attachInternals()&lt;/code&gt;. Here's the minimum viable version, no library, no framework:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;RequiredInput&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="nc"&gt;HTMLElement&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;static&lt;/span&gt; &lt;span class="nx"&gt;formAssociated&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="err"&gt;#&lt;/span&gt;&lt;span class="nx"&gt;internals&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;attachInternals&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

  &lt;span class="nf"&gt;connectedCallback&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;innerHTML&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;&amp;lt;input&amp;gt;&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;querySelector&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;input&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;addEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;input&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="err"&gt;#&lt;/span&gt;&lt;span class="nx"&gt;internals&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setFormValue&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;target&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="err"&gt;#&lt;/span&gt;&lt;span class="nx"&gt;internals&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setValidity&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;target&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;valueMissing&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Please fill out this field.&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="nf"&gt;formResetCallback&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;querySelector&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;input&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;customElements&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;define&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;required-input&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;RequiredInput&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's &lt;em&gt;one&lt;/em&gt; validator, on &lt;em&gt;one&lt;/em&gt; field, with no reset-syncing of validity, no &lt;code&gt;disabled&lt;/code&gt; handling, no styling hook for "the user actually touched this field." Add a second validator, or a second component, and you're rewriting the same &lt;code&gt;setFormValue&lt;/code&gt;/&lt;code&gt;setValidity&lt;/code&gt; dance every time.&lt;/p&gt;

&lt;h2&gt;
  
  
  The reusable version
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;form-control-mixin&lt;/code&gt; wraps that plumbing in a mixin you apply to any class extending &lt;code&gt;HTMLElement&lt;/code&gt; — a plain custom element, a &lt;code&gt;LitElement&lt;/code&gt;, a Stencil or FAST base class:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install &lt;/span&gt;form-control-mixin
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;FormControlMixin&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;requiredValidator&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;minLengthValidator&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;form-control-mixin&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;MyInput&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="nc"&gt;FormControlMixin&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;HTMLElement&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;validators&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;requiredValidator&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;minLengthValidator&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;)],&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="nf"&gt;connectedCallback&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;requestValidation&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="nf"&gt;formResetCallback&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;requestValidation&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;customElements&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;define&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;my-input&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;MyInput&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;form&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;my-input&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"nickname"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/my-input&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;button&amp;gt;&lt;/span&gt;Submit&lt;span class="nt"&gt;&amp;lt;/button&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/form&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;new FormData(form).get('nickname')&lt;/code&gt;, &lt;code&gt;:invalid&lt;/code&gt;, &lt;code&gt;reportValidity()&lt;/code&gt;, the browser's native validation bubble — all of it works, because the mixin is just calling the same &lt;code&gt;ElementInternals&lt;/code&gt; methods you'd call by hand, in the right order, every time &lt;code&gt;requestValidation()&lt;/code&gt; runs.&lt;/p&gt;

&lt;p&gt;Validators are plain objects — &lt;code&gt;{ key, message, isValid(host, value) }&lt;/code&gt; — checked in order, first failure wins. Nine ship built in: &lt;code&gt;requiredValidator&lt;/code&gt;, &lt;code&gt;minLengthValidator&lt;/code&gt;, &lt;code&gt;maxLengthValidator&lt;/code&gt;, &lt;code&gt;patternValidator&lt;/code&gt;, &lt;code&gt;emailValidator&lt;/code&gt;, &lt;code&gt;urlValidator&lt;/code&gt;, &lt;code&gt;minValidator&lt;/code&gt;, &lt;code&gt;maxValidator&lt;/code&gt;, &lt;code&gt;stepValidator&lt;/code&gt;, mirroring the native &lt;code&gt;required&lt;/code&gt;/&lt;code&gt;minlength&lt;/code&gt;/&lt;code&gt;maxlength&lt;/code&gt;/&lt;code&gt;pattern&lt;/code&gt;/&lt;code&gt;type="email"&lt;/code&gt;/&lt;code&gt;type="url"&lt;/code&gt;/&lt;code&gt;min&lt;/code&gt;/&lt;code&gt;max&lt;/code&gt;/&lt;code&gt;step&lt;/code&gt; semantics respectively. Writing your own — an async username-availability check, a cross-field comparison — is the same three-property shape.&lt;/p&gt;

&lt;h2&gt;
  
  
  Styling interaction state without attribute soup
&lt;/h2&gt;

&lt;p&gt;This is the part that usually gets bolted on later as a pile of &lt;code&gt;reflect: true&lt;/code&gt; properties and manual &lt;code&gt;classList.toggle()&lt;/code&gt; calls. &lt;code&gt;ElementInternals&lt;/code&gt; has a &lt;code&gt;states&lt;/code&gt; property — a &lt;a href="https://developer.mozilla.org/en-US/docs/Web/API/CustomStateSet" rel="noopener noreferrer"&gt;&lt;code&gt;CustomStateSet&lt;/code&gt;&lt;/a&gt; — built exactly for this, and the mixin keeps it in sync automatically:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="nt"&gt;my-input&lt;/span&gt;&lt;span class="nd"&gt;:state&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nt"&gt;invalid&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;outline&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;2px&lt;/span&gt; &lt;span class="nb"&gt;solid&lt;/span&gt; &lt;span class="no"&gt;crimson&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c"&gt;/* only complain once the user has actually left the field */&lt;/span&gt;
&lt;span class="nt"&gt;my-input&lt;/span&gt;&lt;span class="nd"&gt;:state&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nt"&gt;touched&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;&lt;span class="nd"&gt;:state&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nt"&gt;invalid&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;background&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;#fdeced&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nt"&gt;my-input&lt;/span&gt;&lt;span class="nd"&gt;:state&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nt"&gt;dirty&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;border-color&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;#6355ff&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;valid&lt;/code&gt;/&lt;code&gt;invalid&lt;/code&gt; mirror &lt;code&gt;validity.valid&lt;/code&gt;. &lt;code&gt;touched&lt;/code&gt; flips on the first &lt;code&gt;focusout&lt;/code&gt;. &lt;code&gt;dirty&lt;/code&gt; flips once the value diverges from whatever it was on the first &lt;code&gt;requestValidation()&lt;/code&gt; call. No attributes, no re-renders triggered by styling concerns — just CSS reacting to state the component already has to track anyway.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why cross-browser testing isn't optional here
&lt;/h2&gt;

&lt;p&gt;If you reach for &lt;code&gt;jsdom&lt;/code&gt; to test any of this — don't. &lt;code&gt;jsdom&lt;/code&gt; doesn't correctly implement &lt;code&gt;ElementInternals&lt;/code&gt;: form association, &lt;code&gt;setFormValue&lt;/code&gt;, and the validity plumbing either no-op or throw depending on the version. Tests will pass while the actual behavior is broken in a real browser, which is worse than having no tests at all.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;form-control-mixin&lt;/code&gt;'s own suite runs against real Chromium, Firefox, and WebKit via &lt;a href="https://modern-web.dev/docs/test-runner/overview/" rel="noopener noreferrer"&gt;&lt;code&gt;@web/test-runner&lt;/code&gt;&lt;/a&gt; + Playwright — the same engines your users actually run.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why this exists
&lt;/h2&gt;

&lt;p&gt;An attempt at solving this generically already exists — &lt;a href="https://www.npmjs.com/package/@open-wc/form-control" rel="noopener noreferrer"&gt;&lt;code&gt;@open-wc/form-control&lt;/code&gt;&lt;/a&gt; — but it hasn't had a release in about three years and never settled on a finished API. Bigger component libraries (Shoelace, Ionic, FAST) solved the same problem, but only inside their own codebase, not as something you can install if you're building your own design system from scratch.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;form-control-mixin&lt;/code&gt; is ~3 KB gzipped, has zero runtime dependencies, ships its own TypeScript types, and doesn't assume Lit or any other renderer.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;npm: &lt;a href="https://www.npmjs.com/package/form-control-mixin" rel="noopener noreferrer"&gt;form-control-mixin&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;source: &lt;a href="https://github.com/korial29/form-control-mixin" rel="noopener noreferrer"&gt;github.com/korial29/form-control-mixin&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;live demo: &lt;a href="https://korial29.github.io/form-control-mixin/demo/" rel="noopener noreferrer"&gt;korial29.github.io/form-control-mixin/demo&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;MIT licensed. Issues, PRs, and "here's a validator I wish existed" requests are all welcome.&lt;/p&gt;

</description>
      <category>webcomponents</category>
      <category>javascript</category>
      <category>typescript</category>
      <category>webdev</category>
    </item>
  </channel>
</rss>
