<?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: Akshat Saxena</title>
    <description>The latest articles on DEV Community by Akshat Saxena (@akshat009).</description>
    <link>https://dev.to/akshat009</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%2F4098314%2F96700a0e-acb8-4e81-aef1-214c4dd38c19.png</url>
      <title>DEV Community: Akshat Saxena</title>
      <link>https://dev.to/akshat009</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/akshat009"/>
    <language>en</language>
    <item>
      <title>A Gutenberg block that uses the Interactivity API — with no render callback and no framework on the front end</title>
      <dc:creator>Akshat Saxena</dc:creator>
      <pubDate>Fri, 28 Aug 2026 04:55:16 +0000</pubDate>
      <link>https://dev.to/akshat009/a-gutenberg-block-that-uses-the-interactivity-api-with-no-render-callback-and-no-framework-on-the-mhb</link>
      <guid>https://dev.to/akshat009/a-gutenberg-block-that-uses-the-interactivity-api-with-no-render-callback-and-no-framework-on-the-mhb</guid>
      <description>&lt;p&gt;A YouTube embed looks innocent enough. &lt;/p&gt;

&lt;p&gt;You drop an &lt;code&gt;iframe&lt;/code&gt; into a page, give it a &lt;code&gt;src&lt;/code&gt;, and you're done, right? &lt;/p&gt;

&lt;p&gt;Except you're not. &lt;/p&gt;

&lt;p&gt;Google's Lighthouse documentation puts a full embedded YouTube player at around &lt;strong&gt;540 KB&lt;/strong&gt;. On a live page, it's closer to &lt;strong&gt;1.3 MB&lt;/strong&gt;. And the cost doesn't get amortized when you add another embed — two players can mean roughly twice the payload because the resources aren't shared.&lt;/p&gt;

&lt;p&gt;HTTP Archive data referenced by web.dev puts another horrifying metric on it: the median YouTube embed occupies the browser's main thread for &lt;strong&gt;more than 1.7 seconds&lt;/strong&gt;. &lt;/p&gt;

&lt;p&gt;And here's the part that bothered me most: &lt;strong&gt;All of that happens even if nobody presses Play.&lt;/strong&gt; 🤯&lt;/p&gt;

&lt;h2&gt;
  
  
  The Solution: The Facade Pattern
&lt;/h2&gt;

&lt;p&gt;The fix isn't new. It's the &lt;strong&gt;facade pattern&lt;/strong&gt;:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Show a poster.&lt;/li&gt;
&lt;li&gt;Show a play button.&lt;/li&gt;
&lt;li&gt;Don't load the actual player.&lt;/li&gt;
&lt;li&gt;Only load it when the visitor clicks.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Tools like &lt;a href="https://github.com/paulirish/lite-youtube-embed" rel="noopener noreferrer"&gt;&lt;code&gt;lite-youtube-embed&lt;/code&gt;&lt;/a&gt; have been doing this for years. &lt;/p&gt;

&lt;p&gt;I wanted to bring this exact same concept into a WordPress Gutenberg block—but not just for YouTube, and &lt;em&gt;definitely&lt;/em&gt; not by shipping an entire JavaScript framework to the browser just to replace a JPEG with an iframe.&lt;/p&gt;

&lt;p&gt;That experiment became the &lt;a href="https://wordpress.org/plugins/story-video-block/" rel="noopener noreferrer"&gt;Story Video Block&lt;/a&gt; (now live on the WP repo, source on &lt;a href="https://github.com/akshat009/story-video-block" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;). &lt;/p&gt;

&lt;p&gt;Here are the most interesting architectural decisions and technical gotchas I ran into while building it. 👇&lt;/p&gt;




&lt;h2&gt;
  
  
  The Interactivity Dilemma
&lt;/h2&gt;

&lt;p&gt;The actual interaction needed for a facade is tiny. &lt;em&gt;Click. Swap in the embed. Play the video.&lt;/em&gt; That's basically it. &lt;/p&gt;

&lt;p&gt;But traditional block solutions can be surprisingly expensive for such a small interaction:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;You could enqueue a classic &lt;code&gt;view.js&lt;/code&gt; and manually handle event delegation and DOM manipulation.&lt;/li&gt;
&lt;li&gt;You could ship React to the front end (some block plugins actually do this). You're essentially sending a UI framework to someone's phone so a poster image can turn into an iframe.&lt;/li&gt;
&lt;li&gt;You could use jQuery (please don't).&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Enter WordPress's Interactivity API ⚡
&lt;/h3&gt;

&lt;p&gt;Since WordPress 6.5, we have the &lt;strong&gt;Interactivity API&lt;/strong&gt;. It gives blocks a declarative way to add client-side behavior. More importantly, the runtime is shared by all blocks on the page. &lt;/p&gt;

&lt;p&gt;The opt-in is incredibly simple in your &lt;code&gt;block.json&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="nl"&gt;"editorScript"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"file:./index.js"&lt;/span&gt;&lt;span class="err"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="nl"&gt;"style"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"file:./style-index.css"&lt;/span&gt;&lt;span class="err"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="nl"&gt;"viewScriptModule"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"file:./view.js"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Notice one detail: &lt;strong&gt;&lt;code&gt;viewScriptModule&lt;/code&gt;, not &lt;code&gt;viewScript&lt;/code&gt;.&lt;/strong&gt; &lt;br&gt;
That registers the file through the Script Modules API and allows you to natively import the shared WP runtime:&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="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;store&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;@wordpress/interactivity&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Revelation: You Don't Need a Dynamic Block
&lt;/h2&gt;

&lt;p&gt;This was my biggest misconception. Because almost every Interactivity API tutorial uses a dynamic block (&lt;code&gt;render.php&lt;/code&gt;), I assumed they were mandatory. They aren't.&lt;/p&gt;

&lt;p&gt;The directives are just HTML attributes. A static block's &lt;code&gt;save()&lt;/code&gt; function can output them, and the frontend runtime will hydrate whatever it finds. &lt;/p&gt;

&lt;p&gt;Here's how my static &lt;code&gt;save()&lt;/code&gt; looks:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;blockProps&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;useBlockProps&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;save&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;clsx&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="s2"&gt;`has-media-&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt; &lt;span class="nx"&gt;videoPosition&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;span class="nx"&gt;videoPosition&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;[&lt;/span&gt; &lt;span class="s2"&gt;`card-style-&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt; &lt;span class="nx"&gt;cardStyle&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;span class="nx"&gt;cardStyle&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;style&lt;/span&gt;&lt;span class="p"&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;--story-video-block-bg&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;backgroundColor&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="kc"&gt;undefined&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="o"&gt;!&lt;/span&gt; &lt;span class="nx"&gt;isFile&lt;/span&gt;
        &lt;span class="p"&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;data-wp-interactive&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;create-block/story-video-block&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;data-wp-context&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                    &lt;span class="na"&gt;isPlaying&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;embedUrl&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                    &lt;span class="na"&gt;videoSrc&lt;/span&gt;&lt;span class="p"&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="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="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 &lt;code&gt;JSON.stringify()&lt;/code&gt; is the whole server-state story for this block! The markup lives in &lt;code&gt;post_content&lt;/code&gt;. There's no PHP render callback running on every request, yet each block instance gets its own scoped, reactive state. &lt;/p&gt;

&lt;h2&gt;
  
  
  The Performance Trick: The &lt;code&gt;iframe&lt;/code&gt; with no &lt;code&gt;src&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;You cannot render an iframe and just CSS hide it. If it has a &lt;code&gt;src&lt;/code&gt;, the browser &lt;strong&gt;will&lt;/strong&gt; load the player. Hidden doesn't mean unloaded. &lt;/p&gt;

&lt;p&gt;So, the iframe is deliberately rendered without a &lt;code&gt;src&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt;
    &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"story-video-block__video"&lt;/span&gt;
    &lt;span class="na"&gt;hidden&lt;/span&gt;
    &lt;span class="na"&gt;data-wp-bind--hidden&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"state.isNotPlaying"&lt;/span&gt;
&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;iframe&lt;/span&gt;
        &lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;heading&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nf"&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;Video&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;story-video-block&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="si"&gt;}&lt;/span&gt;
        &lt;span class="na"&gt;data-wp-bind--src&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"context.videoSrc"&lt;/span&gt;
        &lt;span class="na"&gt;allow&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"autoplay; fullscreen; picture-in-picture"&lt;/span&gt;
        &lt;span class="na"&gt;allowFullScreen&lt;/span&gt;
    &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Initially, &lt;code&gt;context.videoSrc === ''&lt;/code&gt;. Because WP's &lt;code&gt;data-wp-bind--*&lt;/code&gt; removes an attribute when its value is falsy, there is no &lt;code&gt;src&lt;/code&gt; attribute at all. No request, no player.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Bonus UX win:&lt;/strong&gt; Because the source is assigned inside the click handler, the browser treats playback as user-initiated. That means &lt;code&gt;autoplay=1&lt;/code&gt; actually works. One click, video starts. No double-clicking required.&lt;/p&gt;

&lt;h2&gt;
  
  
  Progressive Enhancement: The facade is a link 🔗
&lt;/h2&gt;

&lt;p&gt;I didn't make the facade a &lt;code&gt;&amp;lt;div&amp;gt;&lt;/code&gt;. JavaScript shouldn't be the only way to reach the video.&lt;/p&gt;

&lt;p&gt;The facade is a real &lt;code&gt;&amp;lt;a&amp;gt;&lt;/code&gt; tag pointing to the original video:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;a&lt;/span&gt;
    &lt;span class="na"&gt;href&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;videoUrl&lt;/span&gt; &lt;span class="si"&gt;}&lt;/span&gt;
    &lt;span class="na"&gt;target&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"_blank"&lt;/span&gt;
    &lt;span class="na"&gt;rel&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"noopener noreferrer"&lt;/span&gt;
    &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"story-video-block__facade"&lt;/span&gt;
    &lt;span class="na"&gt;data-wp-on--click&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"actions.play"&lt;/span&gt;
    &lt;span class="na"&gt;data-wp-bind--hidden&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"state.isPlaying"&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;Our Interactivity store then enhances it:&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="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;store&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;getContext&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;@wordpress/interactivity&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nf"&gt;store&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;create-block/story-video-block&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;state&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kd"&gt;get&lt;/span&gt; &lt;span class="nf"&gt;isPlaying&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="nf"&gt;getContext&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nx"&gt;isPlaying&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="kd"&gt;get&lt;/span&gt; &lt;span class="nf"&gt;isNotPlaying&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="o"&gt;!&lt;/span&gt; &lt;span class="nf"&gt;getContext&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nx"&gt;isPlaying&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;actions&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nf"&gt;play&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="p"&gt;{&lt;/span&gt;
            &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;context&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;getContext&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
            &lt;span class="k"&gt;if &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;isPlaying&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="c1"&gt;// If no direct-embed URL (like TikTok), let the original &amp;lt;a&amp;gt; link work!&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;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;embedUrl&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="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;preventDefault&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;videoSrc&lt;/span&gt; &lt;span class="o"&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;embedUrl&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;isPlaying&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="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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This gives us graceful degradation:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;JS + Supported provider (YouTube/Vimeo):&lt;/strong&gt; Plays inline seamlessly.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;JS + No direct embed (TikTok):&lt;/strong&gt; Acts as a normal link.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No JS enabled:&lt;/strong&gt; Acts as a normal link.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  🐛 A CSS Gotcha That Cost Me an Evening
&lt;/h2&gt;

&lt;p&gt;The Interactivity API was working. The &lt;code&gt;hidden&lt;/code&gt; attribute was toggling perfectly in the DOM. But the elements were still visible on the screen. &lt;em&gt;Why?&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Browsers have a default user-agent style:&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="o"&gt;[&lt;/span&gt;&lt;span class="nt"&gt;hidden&lt;/span&gt;&lt;span class="o"&gt;]&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;none&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;But if your block's custom CSS applies an explicit &lt;code&gt;display&lt;/code&gt; (like &lt;code&gt;display: flex;&lt;/code&gt;), it overrides the low-specificity &lt;code&gt;[hidden]&lt;/code&gt; rule. &lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The fix:&lt;/strong&gt;&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="o"&gt;[&lt;/span&gt;&lt;span class="nt"&gt;hidden&lt;/span&gt;&lt;span class="o"&gt;]&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;none&lt;/span&gt; &lt;span class="cp"&gt;!important&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;em&gt;Pro-tip: If you're using &lt;code&gt;data-wp-bind--hidden&lt;/code&gt; and nothing is happening visually, check your CSS specificity before ripping apart your JavaScript!&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Why skip WordPress oEmbed on the frontend?
&lt;/h2&gt;

&lt;p&gt;WordPress has robust oEmbed support. Why did I write manual regex provider detection for the frontend? &lt;/p&gt;

&lt;p&gt;Because the &lt;code&gt;/oembed/1.0/proxy&lt;/code&gt; endpoint requires &lt;code&gt;edit_posts&lt;/code&gt; permissions. An anonymous site visitor gets a &lt;code&gt;401 Unauthorized&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Instead, the block parses the URL itself (supporting YouTube, Vimeo, Dailymotion, Twitch, mp4s, etc.) and constructs the embed URL string locally. Zero network requests just to figure out a URL we already know how to build. &lt;/p&gt;

&lt;h2&gt;
  
  
  Final Thoughts
&lt;/h2&gt;

&lt;p&gt;Building this reminded me of a golden rule in web development: &lt;strong&gt;Don't make every visitor pay for a feature that only some visitors will use.&lt;/strong&gt; &lt;/p&gt;

&lt;p&gt;A video doesn't need to be a 1MB+ third-party application just because someone &lt;em&gt;might&lt;/em&gt; press Play. A static Gutenberg block doesn't need to become dynamic just to hold state. And a tiny bit of interactivity doesn't require shipping a massive JS runtime.&lt;/p&gt;

&lt;p&gt;If you're building blocks for WordPress, give the Interactivity API a shot for your next frontend component! &lt;/p&gt;




&lt;p&gt;&lt;em&gt;The &lt;a href="https://wordpress.org/plugins/story-video-block/" rel="noopener noreferrer"&gt;Story Video Block&lt;/a&gt; is currently v0.1.0, requires WP 6.8+, and is GPL. If you want to check out the full code or contribute, &lt;a href="https://github.com/akshat009/story-video-block" rel="noopener noreferrer"&gt;PRs are welcome on GitHub!&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>wordpress</category>
      <category>gutenberg</category>
      <category>javascript</category>
      <category>webperf</category>
    </item>
  </channel>
</rss>
