<?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: Paul Young</title>
    <description>The latest articles on DEV Community by Paul Young (@paul-young-dev).</description>
    <link>https://dev.to/paul-young-dev</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%2F4156206%2F3dd27006-1fe5-4dfd-9554-81f683952044.jpg</url>
      <title>DEV Community: Paul Young</title>
      <link>https://dev.to/paul-young-dev</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/paul-young-dev"/>
    <language>en</language>
    <item>
      <title>Building LitGrid: One Web Component DataGrid for React, Angular, Vue, and Blazor</title>
      <dc:creator>Paul Young</dc:creator>
      <pubDate>Fri, 02 Oct 2026 08:48:48 +0000</pubDate>
      <link>https://dev.to/tipolox/building-litgrid-one-web-component-datagrid-for-react-angular-vue-and-blazor-e4l</link>
      <guid>https://dev.to/tipolox/building-litgrid-one-web-component-datagrid-for-react-angular-vue-and-blazor-e4l</guid>
      <description>&lt;h2&gt;
  
  
  TL;DR
&lt;/h2&gt;

&lt;p&gt;This is my first article about LitGrid, so I wanted to start with the architectural decision that shaped the project from the beginning.&lt;/p&gt;

&lt;p&gt;I started LitGrid with one rule for myself:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Build the grid once as a real Web Component, keep its internals behind Shadow DOM, and adapt frameworks around that component instead of rebuilding the grid for each one.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Today, React, Angular, Vue, Blazor, and direct Web Component usage all sit on top of the same LitGrid implementation.&lt;/p&gt;

&lt;p&gt;Behind the component, grid state and virtualization live in separate layers.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Live demo:&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
&lt;a href="https://tipolox.com/litgrid/demo" rel="noopener noreferrer"&gt;https://tipolox.com/litgrid/demo&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Source:&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
&lt;a href="https://github.com/tipolox/litgrid" rel="noopener noreferrer"&gt;https://github.com/tipolox/litgrid&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The part I found interesting was not simply building another table component.&lt;/p&gt;

&lt;p&gt;I was already working with Lit and Shadow DOM, and I kept coming back to the same question: could I keep that component boundary and still make the grid practical in several frameworks? &lt;/p&gt;


&lt;h2&gt;
  
  
  The constraint that shaped everything
&lt;/h2&gt;

&lt;p&gt;When I started LitGrid, the idea was not to build a React grid, then an Angular grid, then repeat the work again for Vue and Blazor.&lt;/p&gt;

&lt;p&gt;I wanted one implementation.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;React ────────┐
Angular ──────┤
Vue ──────────┼──► LitGrid Web Component
Blazor ───────┤
Web Component ┘
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The idea was simple: the React package should not implement its own sorting, selection, virtualization, keyboard navigation, column resizing, or filtering. The same goes for Angular, Vue, and Blazor.&lt;/p&gt;

&lt;p&gt;Their job is to translate framework conventions into the public API of the same browser component. If I fix grid behavior in one place, I want all integration to benefit from it.&lt;/p&gt;

&lt;p&gt;This has led to this architecture:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Framework adapters
        │
        ▼
   Web Component
        │
     LitElement
    Shadow DOM
        │
   ┌────┴─────┐
   │          │
Grid Core   Renderer
state/data  virtualization
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;React, Angular, Vue, and Blazor are not interchangeable, and I don't try to hide those differences.&lt;/p&gt;

&lt;p&gt;The part I want to keep shared is the &lt;strong&gt;grid implementation itself&lt;/strong&gt;. Each adapter deals with framework ergonomics; the grid behavior stays in one place.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why Lit and Shadow DOM?
&lt;/h2&gt;

&lt;p&gt;Lit was part of the original direction, not something I added later to make the project framework-neutral.&lt;/p&gt;

&lt;p&gt;The main &lt;code&gt;DataGrid&lt;/code&gt; class extends &lt;code&gt;LitElement&lt;/code&gt;, and LitGrid keeps Lit's normal Shadow DOM render root instead of switching to light DOM.&lt;/p&gt;

&lt;p&gt;Conceptually:&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;yc-grid&amp;gt;&lt;/span&gt;
  #shadow-root
    header
    viewport
    rows
    cells
    menus
    pagination
&lt;span class="nt"&gt;&amp;lt;/yc-grid&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That sounds like a small implementation choice, but it defines a lot of the architecture.&lt;/p&gt;

&lt;p&gt;Applications do not need to depend on internal selectors like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;.viewport
.header-cell
.cell
.header-menu
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Those details belong to LitGrid.&lt;/p&gt;

&lt;p&gt;Consumers work with the host element through public properties, methods, and events.&lt;/p&gt;

&lt;p&gt;Styling follows the same philosophy.&lt;/p&gt;

&lt;p&gt;Internal styles live inside the component, while semantic CSS custom properties provide the external theming surface.&lt;/p&gt;

&lt;p&gt;For example:&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;yc-grid&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="py"&gt;--litgrid-color-accent&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;#2563eb&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="py"&gt;--litgrid-color-surface&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;#ffffff&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="py"&gt;--litgrid-color-text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;#111827&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;LitGrid currently uses CSS custom properties as its main public styling mechanism.&lt;/p&gt;

&lt;p&gt;It does not currently expose a broad &lt;code&gt;::part()&lt;/code&gt; styling API.&lt;/p&gt;

&lt;p&gt;That is intentional for now. Once an internal element becomes part of a public styling contract, changing the markup later gets harder. I would rather expose a smaller, deliberate surface first.&lt;/p&gt;

&lt;p&gt;Shadow DOM gave me the encapsulation I wanted. It also made a few problems impossible to ignore.&lt;/p&gt;




&lt;h2&gt;
  
  
  What Shadow DOM made me handle
&lt;/h2&gt;

&lt;p&gt;Encapsulation is useful, but a DataGrid is not a static card component.&lt;/p&gt;

&lt;p&gt;It has focus, keyboard navigation, menus, selection, ARIA state, scrolling, drag interactions, and a lot of DOM coordination.&lt;/p&gt;

&lt;h3&gt;
  
  
  Focus management
&lt;/h3&gt;

&lt;p&gt;LitGrid keeps its active-cell model internally and uses the grid viewport as the keyboard focus entry point.&lt;/p&gt;

&lt;p&gt;Arrow keys move the active cell.&lt;/p&gt;

&lt;p&gt;Home and End move across row boundaries.&lt;/p&gt;

&lt;p&gt;Page Up and Page Down move by viewport-sized ranges.&lt;/p&gt;

&lt;p&gt;Tab and Shift+Tab can move between grid cells while still allowing normal browser focus traversal when the grid boundary is reached.&lt;/p&gt;

&lt;p&gt;Header menus also need explicit focus behavior.&lt;/p&gt;

&lt;p&gt;When a menu opens, focus moves into it.&lt;/p&gt;

&lt;p&gt;When Escape closes the menu, focus returns to the trigger.&lt;/p&gt;

&lt;p&gt;This was one of the first places where encapsulation stopped being "free." A grid has to feel predictable from the keyboard, so I ended up treating focus movement as explicit component behavior and covering it with tests rather than assuming the browser would always do the right thing across the shadow boundary.&lt;/p&gt;

&lt;h3&gt;
  
  
  ARIA relationships
&lt;/h3&gt;

&lt;p&gt;The viewport uses ARIA grid semantics, including logical row and column counts and an active descendant.&lt;/p&gt;

&lt;p&gt;One important detail is that the referenced elements are inside the &lt;strong&gt;same Shadow Root&lt;/strong&gt; as the viewport.&lt;/p&gt;

&lt;p&gt;For example, the active-cell relationship stays internal to the component rather than trying to reference an element across the Shadow DOM boundary.&lt;/p&gt;

&lt;p&gt;That keeps the accessibility model aligned with the component structure.&lt;/p&gt;

&lt;p&gt;I also try to be careful with the accessibility claim. LitGrid implements ARIA grid semantics, keyboard interaction, and optional screen-reader announcements, and those structures have automated coverage.&lt;/p&gt;

&lt;p&gt;What I am &lt;strong&gt;not&lt;/strong&gt; claiming is exhaustive validation across every browser and screen-reader combination. That kind of confidence needs broader real-world testing.&lt;/p&gt;




&lt;h2&gt;
  
  
  A public API instead of DOM coupling
&lt;/h2&gt;

&lt;p&gt;The Web Component boundary only helps if the adapters respect it.&lt;/p&gt;

&lt;p&gt;For example, I don't want the React package reaching through the shadow root to find private elements:&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;grid&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;shadowRoot&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;.viewport&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="nf"&gt;something&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That would work until the internal markup changed. Then a harmless refactor inside LitGrid could break a framework adapter.&lt;/p&gt;

&lt;p&gt;Instead, the host exposes operations directly:&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;grid&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;rows&lt;/span&gt;
&lt;span class="nx"&gt;grid&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;columns&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;columns&lt;/span&gt;
&lt;span class="nx"&gt;grid&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;config&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;config&lt;/span&gt;

&lt;span class="nx"&gt;grid&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setColumnVisible&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;email&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nx"&gt;grid&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setQuickSearch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;active&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nx"&gt;grid&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setPage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;State changes can also be observed through DOM events.&lt;/p&gt;

&lt;p&gt;For example:&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;new&lt;/span&gt; &lt;span class="nc"&gt;CustomEvent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;column-state-change&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="na"&gt;bubbles&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="na"&gt;composed&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="na"&gt;detail&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;reason&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;state&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;Those events are dispatched from the custom-element host and translated by the framework adapters into the conventions expected by each framework.&lt;/p&gt;




&lt;h2&gt;
  
  
  What a thin React adapter actually looks like
&lt;/h2&gt;

&lt;p&gt;This is also why I describe the adapters as thin. In the React integration, "thin" is fairly literal.&lt;/p&gt;

&lt;p&gt;The React integration ultimately renders the Web Component:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;yc&lt;/span&gt;&lt;span class="err"&gt;-&lt;/span&gt;&lt;span class="na"&gt;grid&lt;/span&gt; &lt;span class="na"&gt;ref&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;gridRef&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;yc&lt;/span&gt;&lt;span class="err"&gt;-&lt;/span&gt;&lt;span class="na"&gt;grid&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Complex values are assigned as JavaScript properties rather than serialized as HTML attributes:&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;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;syncGridInputs&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;grid&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;inputs&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;grid&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;inputs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;
  &lt;span class="nx"&gt;grid&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;columns&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;inputs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;columns&lt;/span&gt;
  &lt;span class="nx"&gt;grid&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;config&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;inputs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;config&lt;/span&gt;
  &lt;span class="nx"&gt;grid&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;theme&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;inputs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;theme&lt;/span&gt;

  &lt;span class="nx"&gt;grid&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ariaLabel&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;inputs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ariaLabel&lt;/span&gt;
  &lt;span class="nx"&gt;grid&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ariaDescription&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;inputs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ariaDescription&lt;/span&gt;

  &lt;span class="nx"&gt;grid&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;viewportHeight&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;inputs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;height&lt;/span&gt;
  &lt;span class="nx"&gt;grid&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;virtualRowHeight&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;inputs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;rowHeight&lt;/span&gt;

  &lt;span class="nx"&gt;grid&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;overscanCount&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;inputs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;overscan&lt;/span&gt;
  &lt;span class="nx"&gt;grid&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;columnOverscanCount&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;inputs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;columnOverscan&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That property assignment matters for Web Components.&lt;/p&gt;

&lt;p&gt;Things like arrays, column definitions, callbacks, and configuration objects are not naturally represented as string attributes.&lt;/p&gt;

&lt;p&gt;The adapter also listens to DOM events:&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="nf"&gt;useEffect&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;element&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;gridRef&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;current&lt;/span&gt;

  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;element&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;onColumnStateChange&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;handler&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;event&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;onColumnStateChange&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;detail&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="nx"&gt;element&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;column-state-change&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;handler&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

  &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;element&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;removeEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;column-state-change&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;handler&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="nx"&gt;onColumnStateChange&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And then React developers use a normal React-facing API:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;DataGrid&lt;/span&gt;
  &lt;span class="na"&gt;data&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;rows&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
  &lt;span class="na"&gt;columns&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;columns&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
  &lt;span class="na"&gt;height&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="mi"&gt;400&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
  &lt;span class="na"&gt;onColumnStateChange&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;handleColumnStateChange&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is most of the job: translate React-shaped inputs and callbacks into the Web Component contract. Sorting, selection, virtualization, and the rest still belong to LitGrid itself.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Web Component isn't the whole architecture
&lt;/h2&gt;

&lt;p&gt;As the grid grew, another problem became obvious: putting every piece of logic inside one large &lt;code&gt;LitElement&lt;/code&gt; would eventually make the component difficult to reason about.&lt;/p&gt;

&lt;p&gt;So I split responsibilities instead of treating the Web Component as the entire architecture.&lt;/p&gt;

&lt;h3&gt;
  
  
  Grid Core
&lt;/h3&gt;

&lt;p&gt;The core handles state and data operations such as:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;sorting&lt;/li&gt;
&lt;li&gt;filtering&lt;/li&gt;
&lt;li&gt;Quick Search&lt;/li&gt;
&lt;li&gt;pagination&lt;/li&gt;
&lt;li&gt;selection&lt;/li&gt;
&lt;li&gt;configuration&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It does not need the DOM to perform those operations.&lt;/p&gt;

&lt;p&gt;That makes the logic easier to test independently from the component UI and keeps browser-rendering concerns out of the data engine.&lt;/p&gt;

&lt;h3&gt;
  
  
  Renderer utilities
&lt;/h3&gt;

&lt;p&gt;The renderer layer contains virtualization and scroll-mapping calculations.&lt;/p&gt;

&lt;p&gt;That math should not care whether the caller happens to be React, Angular, Vue, or Blazor. Keeping it separate also makes it easier to test without dragging the whole component lifecycle into the test.&lt;/p&gt;

&lt;h3&gt;
  
  
  Web Component
&lt;/h3&gt;

&lt;p&gt;The Web Component combines those layers with:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;DOM rendering&lt;/li&gt;
&lt;li&gt;pointer interaction&lt;/li&gt;
&lt;li&gt;focus&lt;/li&gt;
&lt;li&gt;keyboard navigation&lt;/li&gt;
&lt;li&gt;menus&lt;/li&gt;
&lt;li&gt;sizing&lt;/li&gt;
&lt;li&gt;scrolling&lt;/li&gt;
&lt;li&gt;ARIA behavior&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That separation gives the project a useful rule of thumb:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Data/state problem
      ↓
    Core

Virtual-coordinate problem
      ↓
  Renderer

Browser interaction/rendering
      ↓
  Web Component

Framework convention
      ↓
   Adapter
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The boundaries are not perfect, and I do not expect them to be. But when I add something new, this gives me a practical first question: is this grid state, virtualization math, browser behavior, or framework glue?&lt;/p&gt;




&lt;h2&gt;
  
  
  Virtualization is more than "render fewer rows"
&lt;/h2&gt;

&lt;p&gt;The obvious reason to virtualize a grid is DOM size.&lt;/p&gt;

&lt;p&gt;If a logical dataset contains:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;100,000 rows × 50 columns
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;the grid should not create five million DOM cells.&lt;/p&gt;

&lt;p&gt;LitGrid instead renders the rows and columns that intersect the active viewport, plus overscan.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Logical dataset
│
│      not rendered
│
├───────────────────────
│       overscan
│   ┌───────────────┐
│   │   viewport    │
│   │               │
│   │ rendered rows │
│   │ and columns   │
│   │               │
│   └───────────────┘
│       overscan
├───────────────────────
│
│      not rendered
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Rendering fewer rows was the obvious virtualization problem. The less obvious problem showed up when the logical scroll space became very large.&lt;/p&gt;

&lt;p&gt;A browser cannot represent an arbitrarily tall element forever. Eventually, a huge spacer can be clamped internally, which means the browser's physical scroll range no longer matches the grid's logical row space.&lt;/p&gt;

&lt;p&gt;LitGrid therefore keeps a distinction between:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;logical / virtual scroll space
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;and:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;physical browser scroll space
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The current implementation caps its intended physical scroll height:&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;private&lt;/span&gt; &lt;span class="nx"&gt;maxScrollableHeight&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;67108864&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For datasets whose virtual height exceeds that display space, LitGrid maps the browser-visible scroll position back into the larger virtual coordinate system.&lt;/p&gt;

&lt;p&gt;Conceptually:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;browser scrollTop
       │
       │ map ratio
       ▼
virtual scroll offset
       │
       ▼
virtualizer
       │
       ▼
visible row range
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There is another complication.&lt;/p&gt;

&lt;p&gt;A browser may clamp the physical spacer below even the requested capped size.&lt;/p&gt;

&lt;p&gt;After render, LitGrid measures the actual spacer height.&lt;/p&gt;

&lt;p&gt;If the browser produced something smaller than expected, subsequent scroll mapping uses the measured size rather than assuming the requested height was honored.&lt;/p&gt;

&lt;p&gt;I did not start the project thinking about browser element-height limits. This is exactly the kind of problem that only appeared after the basic virtualization model was already working, and it forced me to separate logical scroll coordinates from the physical scroll space the browser can actually represent.&lt;/p&gt;




&lt;h2&gt;
  
  
  Row and column virtualization
&lt;/h2&gt;

&lt;p&gt;LitGrid virtualizes both axes.&lt;/p&gt;

&lt;p&gt;The row virtualizer determines:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;start row
end row
offset top
visible size
bottom padding
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;while the column virtualizer performs a similar job horizontally.&lt;/p&gt;

&lt;p&gt;Both fixed and variable row sizes are supported.&lt;/p&gt;

&lt;p&gt;This lets the rendered DOM stay tied to what the user can currently see rather than to the entire dataset.&lt;/p&gt;

&lt;p&gt;I deliberately avoid calling LitGrid "the fastest grid."&lt;/p&gt;

&lt;p&gt;Performance depends on:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;dataset structure&lt;/li&gt;
&lt;li&gt;cell renderers&lt;/li&gt;
&lt;li&gt;browser&lt;/li&gt;
&lt;li&gt;hardware&lt;/li&gt;
&lt;li&gt;overscan configuration&lt;/li&gt;
&lt;li&gt;application behavior&lt;/li&gt;
&lt;li&gt;framework integration&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The concrete claim is simpler:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;LitGrid uses row and column virtualization so the rendered DOM follows the active viewport rather than the complete logical dataset.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  The renderer tradeoff
&lt;/h2&gt;

&lt;p&gt;This is where the architecture pushes back the hardest against normal framework expectations.&lt;/p&gt;

&lt;p&gt;Cell rendering belongs to LitGrid's Lit rendering system.&lt;/p&gt;

&lt;p&gt;A renderer can return a simple value:&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="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;key&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;amount&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;render&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;=&amp;gt;&lt;/span&gt;
    &lt;span class="s2"&gt;`$&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nc"&gt;Number&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="nf"&gt;toLocaleString&lt;/span&gt;&lt;span class="p"&gt;()}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;or a Lit template:&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="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="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="nl"&gt;key&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;status&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;render&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;=&amp;gt;&lt;/span&gt;
    &lt;span class="nx"&gt;html&lt;/span&gt;&lt;span class="s2"&gt;`&amp;lt;strong&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nc"&gt;String&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="s2"&gt;&amp;lt;/strong&amp;gt;`&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A Lit template can also contain another Web Component:&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="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;key&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;status&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;render&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;=&amp;gt;&lt;/span&gt;
    &lt;span class="nx"&gt;html&lt;/span&gt;&lt;span class="s2"&gt;`
      &amp;lt;status-badge .status=&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="s2"&gt;&amp;gt;&amp;lt;/status-badge&amp;gt;
    `&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;What it cannot currently do is take over cell rendering with a framework-native renderer.&lt;/p&gt;

&lt;p&gt;For example, this is not a supported React cell renderer today:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="nx"&gt;render&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;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;StatusBadge&lt;/span&gt; &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Likewise, LitGrid does not currently mount Vue VNodes or Angular components as native cell renderers.&lt;/p&gt;

&lt;p&gt;That is a real limitation. A React developer naturally wants to reuse an existing &lt;code&gt;&amp;lt;StatusBadge /&amp;gt;&lt;/code&gt;, action menu, avatar, or button inside a cell. Today, LitGrid does not let React take over that cell directly.&lt;/p&gt;

&lt;p&gt;For some applications that will be fine; for others it may be a dealbreaker. I would rather say that plainly than call the integration "framework-ready" and leave the important caveat in the small print.&lt;/p&gt;

&lt;p&gt;The reason for the limitation is also the same reason LitGrid can maintain one internal rendering implementation:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;The framework integrates with the grid, but Lit remains responsible for the grid's internal rendering.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;I do think there is a way to improve this without throwing away the architecture.&lt;/p&gt;

&lt;p&gt;One option I am exploring is a renderer-host API where LitGrid owns the cell and its lifecycle, but a framework adapter is allowed to mount something into a host that LitGrid provides.&lt;/p&gt;

&lt;p&gt;Something like:&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="kr"&gt;interface&lt;/span&gt; &lt;span class="nx"&gt;ExternalCellRenderer&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;mount&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;host&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="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;CellContext&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt;
  &lt;span class="nf"&gt;update&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;CellContext&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt;
  &lt;span class="nf"&gt;destroy&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That could potentially allow React, Vue, or Angular components to live inside virtualized cells without moving framework logic into the grid core.&lt;/p&gt;

&lt;p&gt;The important design requirement would be that existing string and Lit renderers stay on their current fast path.&lt;/p&gt;

&lt;p&gt;Framework-native rendering should be opt-in so only applications using it pay the extra component lifecycle cost.&lt;/p&gt;

&lt;p&gt;I have put this on the roadmap as a design problem, not as a promised feature. Before making it public API, I want to prove that mount/update/destroy works cleanly with virtualization and that users who stay on the existing Lit path do not pay for the extra framework lifecycle.&lt;/p&gt;




&lt;h2&gt;
  
  
  Browser-side rendering and SSR
&lt;/h2&gt;

&lt;p&gt;SSR is another place where the current architecture has a visible edge.&lt;/p&gt;

&lt;p&gt;The Web Component package currently registers the custom element in the browser:&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;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&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;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;yc-grid&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;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;yc-grid&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;DataGrid&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;That means LitGrid should currently be treated as a &lt;strong&gt;browser-side interactive component&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;For SSR frameworks such as Next.js, the integration should be loaded in a client/browser context rather than assuming the grid itself can execute in a normal server-only JavaScript environment.&lt;/p&gt;

&lt;p&gt;The same general distinction applies elsewhere:&lt;/p&gt;

&lt;p&gt;the application shell can be server-rendered, while the interactive DataGrid is initialized on the client.&lt;/p&gt;

&lt;p&gt;I would rather document that boundary clearly now and harden it over time than imply that choosing Web Components automatically solves SSR.&lt;/p&gt;




&lt;h2&gt;
  
  
  Blazor is a slightly different integration
&lt;/h2&gt;

&lt;p&gt;Blazor forced a slightly different adapter shape because the integration crosses both a framework boundary and a language/runtime boundary.&lt;/p&gt;

&lt;p&gt;The public NuGet package is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;dotnet add package Tipolox.LitGrid.Blazor
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It provides a strongly typed:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;DataGrid&amp;lt;TItem&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;and bundles the required JavaScript as Razor Class Library static assets.&lt;/p&gt;

&lt;p&gt;It supports .NET 8+ Blazor WebAssembly and Blazor Server applications.&lt;/p&gt;

&lt;p&gt;DOM interaction still happens in JavaScript because the actual grid lives in the browser.&lt;/p&gt;

&lt;p&gt;The Razor component communicates with that browser component through asynchronous JS interop.&lt;/p&gt;

&lt;p&gt;That means Blazor shares the same underlying Web Component, but the adapter mechanics are necessarily different from React or Vue.&lt;/p&gt;

&lt;p&gt;I also do not want to blur browser-side grid performance with Blazor Server round trips.&lt;/p&gt;

&lt;p&gt;In Server mode, round trips that cross the Blazor circuit naturally depend on network and server latency.&lt;/p&gt;

&lt;p&gt;The grid's browser-side interaction remains local, but application callbacks that cross that boundary have different performance characteristics from WebAssembly.&lt;/p&gt;




&lt;h2&gt;
  
  
  What LitGrid supports today
&lt;/h2&gt;

&lt;p&gt;The current open-source release includes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Virtualization
  Row virtualization
  Column virtualization
  Variable row sizing

Data
  Sorting
  Filtering
  Quick Search
  Pagination

Interaction
  Row selection
  Cell selection
  Multi-selection
  Keyboard navigation
  Clipboard operations

Columns
  Resize
  Reorder
  Visibility
  Best fit

State
  Column state
  Persistence
  State-change events

UI
  Light and dark themes
  Lit-based custom renderers

Accessibility
  ARIA grid semantics
  Keyboard focus behavior
  Optional screen-reader announcements
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Column state can also be manipulated programmatically:&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;grid&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setColumnState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;savedState&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;LitGrid intentionally emits state-change events when the effective state changes through that imperative API.&lt;/p&gt;

&lt;p&gt;The event contains a reason such as:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;resize
reorder
visibility
restore
reset
set
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;so applications can distinguish the source of a change when necessary.&lt;/p&gt;




&lt;h2&gt;
  
  
  Using LitGrid directly
&lt;/h2&gt;

&lt;p&gt;The Web Component package can be used without a framework wrapper.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pnpm add @tipolox/litgrid-web
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Import the package:&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;@tipolox/litgrid-web&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then:&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;yc-grid&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"users-grid"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/yc-grid&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;and assign structured values as properties:&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;grid&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
  &lt;span class="nb"&gt;document&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;#users-grid&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="nx"&gt;grid&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt; &lt;span class="o"&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;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Ada&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="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Grace&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;grid&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;columns&lt;/span&gt; &lt;span class="o"&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;key&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;id&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;header&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;ID&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;width&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;80&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;name&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;header&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Name&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;width&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;220&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;]&lt;/span&gt;

&lt;span class="nx"&gt;grid&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;viewportHeight&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;400&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The framework adapters are conveniences around this browser component.&lt;/p&gt;

&lt;p&gt;They are not prerequisites for the grid itself.&lt;/p&gt;




&lt;h2&gt;
  
  
  Framework packages
&lt;/h2&gt;

&lt;p&gt;The current integrations are distributed as:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pnpm add @tipolox/litgrid-react
pnpm add @tipolox/litgrid-angular
pnpm add @tipolox/litgrid-vue
pnpm add @tipolox/litgrid-web
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;and for Blazor:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;dotnet add package Tipolox.LitGrid.Blazor
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The naming is admittedly a little mixed today:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;npm organization   @tipolox/*
browser element    &amp;lt;yc-grid&amp;gt;
Blazor package     Tipolox.LitGrid.Blazor
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Those names come from different stages of the project, but they all refer to the same LitGrid implementation.&lt;/p&gt;




&lt;h2&gt;
  
  
  When LitGrid may not be the right fit
&lt;/h2&gt;

&lt;p&gt;Since this is the first article I am writing about LitGrid, I also want to be clear about where I would &lt;strong&gt;not&lt;/strong&gt; recommend it yet.&lt;/p&gt;

&lt;p&gt;LitGrid may not be the best fit today if your application requires:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Native framework components inside every custom cell.&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
The current renderer belongs to Lit. React JSX, Angular components, and Vue VNodes are not currently native cell-renderer outputs.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Deep styling of arbitrary internal elements.&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
The current public styling contract is primarily CSS custom properties rather than a large &lt;code&gt;::part()&lt;/code&gt; surface.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Server-only rendering of the grid itself.&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
LitGrid is currently designed as an interactive browser component.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A mature grid ecosystem with years of production history.&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
LitGrid is still a young open-source project. I am actively looking for the kinds of edge cases that only appear when a component meets real applications.&lt;/p&gt;

&lt;p&gt;Some of those constraints are exactly the things I want to improve. But for now, they are part of the product, and I think they belong in the same article as the strengths.&lt;/p&gt;


&lt;h2&gt;
  
  
  Why open source?
&lt;/h2&gt;

&lt;p&gt;LitGrid is MIT licensed.&lt;/p&gt;

&lt;p&gt;Open sourcing LitGrid was important to me because a DataGrid ends up touching far more of the browser platform than the word "grid" suggests:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;scrolling
layout
focus
keyboard navigation
ARIA
pointer interaction
virtualization
measurement
rendering
state
framework lifecycles
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Those are exactly the kinds of areas where edge cases appear.&lt;/p&gt;

&lt;p&gt;When somebody hits an edge case, I want the discussion to be about the code and the design decision that produced it. Keeping the implementation visible makes that possible.&lt;/p&gt;

&lt;p&gt;Open source also makes the "one grid implementation" idea easier to evaluate.&lt;/p&gt;

&lt;p&gt;The architecture is there to inspect: the React, Angular, Vue, Blazor, Web Component, core, and virtualization packages are all visible in the repository.&lt;/p&gt;




&lt;h2&gt;
  
  
  The question LitGrid is trying to answer
&lt;/h2&gt;

&lt;p&gt;After working on it for a while, I think the most useful way to describe LitGrid is still as a question:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Can another DataGrid be built?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Obviously it can.&lt;/p&gt;

&lt;p&gt;The question I am more interested in is:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Can a complex interactive UI component be implemented once around Web Components, Lit, and Shadow DOM while still providing practical integrations for React, Angular, Vue, and Blazor?&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;LitGrid is my attempt to answer that question in actual code. I like the direction so far, but the project has also made the costs of the architecture very concrete.&lt;/p&gt;

&lt;p&gt;Shadow DOM gives LitGrid a strong implementation boundary, but focus and accessibility behavior need deliberate handling.&lt;/p&gt;

&lt;p&gt;A single rendering engine prevents framework implementations from drifting apart, but framework-native cell components become harder.&lt;/p&gt;

&lt;p&gt;Virtualization keeps DOM size under control, but very large coordinate spaces require scroll mapping because browser element height is finite.&lt;/p&gt;

&lt;p&gt;Those tradeoffs are the part I want to keep exploring. If you have built a serious Web Component, a cross-framework library, or a virtualized UI and ran into a completely different set of problems, that is the feedback I would most like to hear.&lt;/p&gt;




&lt;h2&gt;
  
  
  Try it or inspect the code
&lt;/h2&gt;

&lt;p&gt;LitGrid is open source under the MIT License.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Live demo&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
&lt;a href="https://tipolox.com/litgrid/demo" rel="noopener noreferrer"&gt;tipolox.com/litgrid/demo&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;GitHub&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
&lt;a href="https://github.com/tipolox/litgrid" rel="noopener noreferrer"&gt;github.com/tipolox/litgrid&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Open-source DataGrid overview and framework integrations&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
&lt;a href="https://tipolox.com/litgrid/open-source-datagrid" rel="noopener noreferrer"&gt;tipolox.com/litgrid/open-source-datagrid&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If you work with Web Components, virtualized UI, or cross-framework libraries, I would genuinely like to hear what broke first in your design, what you ended up compromising on, and what you would do differently now.&lt;/p&gt;

&lt;p&gt;And if you try LitGrid in a real application, please open an issue when something feels awkward or wrong. That kind of feedback is more useful to me right now than a polished feature checklist.&lt;/p&gt;

</description>
      <category>webcomponents</category>
      <category>opensource</category>
      <category>javascript</category>
      <category>frontend</category>
    </item>
  </channel>
</rss>
