<?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: Mohammed Rashad</title>
    <description>The latest articles on DEV Community by Mohammed Rashad (@rashadmehtab).</description>
    <link>https://dev.to/rashadmehtab</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%2F3082328%2F01cade8a-dc90-48fb-bc3d-1a52b8187f3e.jpg</url>
      <title>DEV Community: Mohammed Rashad</title>
      <link>https://dev.to/rashadmehtab</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/rashadmehtab"/>
    <language>en</language>
    <item>
      <title>Adding a Markdown Editor to Laravel Forms (No Sync Code Required)</title>
      <dc:creator>Mohammed Rashad</dc:creator>
      <pubDate>Mon, 12 Oct 2026 13:00:00 +0000</pubDate>
      <link>https://dev.to/rashadmehtab/adding-a-markdown-editor-to-laravel-forms-no-sync-code-required-391c</link>
      <guid>https://dev.to/rashadmehtab/adding-a-markdown-editor-to-laravel-forms-no-sync-code-required-391c</guid>
      <description>&lt;p&gt;Add a markdown editor to a Blade form the usual way and it looks fine, until the form silently refuses to submit:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;lt;textarea id="content" name="content" required&amp;gt;{{ old('content') }}&amp;lt;/textarea&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// The editor everyone reaches for first&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;easyMDE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;EasyMDE&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;element&lt;/span&gt;&lt;span class="p"&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;getElementById&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;content&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Click Save and nothing happens. No validation error, no request, no clue on the page. The console says:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;An invalid form control with name='content' is not focusable.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;EasyMDE hides your &lt;code&gt;&amp;lt;textarea&amp;gt;&lt;/code&gt; with &lt;code&gt;display: none&lt;/code&gt; and edits a copy. The hidden field is still &lt;code&gt;required&lt;/code&gt;, so the browser refuses to submit a form it can't focus the invalid field in, and because the submit event never fires, the editor never copies its content back.&lt;/p&gt;

&lt;p&gt;To be fair to EasyMDE: without &lt;code&gt;required&lt;/code&gt; it does sync on submit by itself, so a plain form works. The trouble is that the value only exists in the textarea &lt;em&gt;during&lt;/em&gt; submit:&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="c1"&gt;// Any code that reads the field before submit gets an empty string&lt;/span&gt;
&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;FormData&lt;/span&gt;&lt;span class="p"&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;form&lt;/span&gt;&lt;span class="dl"&gt;'&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;content&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;   &lt;span class="c1"&gt;// ""&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That breaks anything that serialises the form itself: Livewire, Alpine, htmx, autosave, an "unsaved changes" guard, a character counter.&lt;/p&gt;

&lt;h2&gt;
  
  
  A better approach
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://frutjam.com/plugins/markdown-editor" rel="noopener noreferrer"&gt;markdown-text-editor&lt;/a&gt; styles the textarea you already have and leaves it as the field you type into. It's never hidden and never copied, so its value is correct at every moment, not just during submit.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;required&lt;/code&gt; works because the field is visible. &lt;code&gt;FormData&lt;/code&gt; works because the value is live. And &lt;code&gt;$request-&amp;gt;input()&lt;/code&gt;, form request validation, &lt;code&gt;old()&lt;/code&gt; repopulation, CSRF and mass assignment all behave exactly as they do with a plain textarea.&lt;/p&gt;

&lt;h2&gt;
  
  
  Setup
&lt;/h2&gt;

&lt;p&gt;Install via npm (if you use Vite):&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;markdown-text-editor
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Or use the CDN directly in your Blade template, no build step needed:&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;src=&lt;/span&gt;&lt;span class="s"&gt;"https://cdn.jsdelivr.net/npm/markdown-text-editor"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/script&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Blade template
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;lt;form method="POST" action="{{ route('posts.store') }}"&amp;gt;
    @csrf

    &amp;lt;div&amp;gt;
        &amp;lt;label for="content"&amp;gt;Content&amp;lt;/label&amp;gt;
        &amp;lt;textarea id="content" name="content" class="markdown-editor"&amp;gt;{{ old('content') }}&amp;lt;/textarea&amp;gt;
        @error('content') &amp;lt;span&amp;gt;{{ $message }}&amp;lt;/span&amp;gt; @enderror
    &amp;lt;/div&amp;gt;

    &amp;lt;button type="submit"&amp;gt;Save&amp;lt;/button&amp;gt;
&amp;lt;/form&amp;gt;

&amp;lt;script src="https://cdn.jsdelivr.net/npm/markdown-text-editor"&amp;gt;&amp;lt;/script&amp;gt;
&amp;lt;script&amp;gt;
    new MarkdownEditor('.markdown-editor');
&amp;lt;/script&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's it. Submit the form: &lt;code&gt;$request-&amp;gt;input('content')&lt;/code&gt; contains the markdown exactly as typed. No hooks, no sync, no custom extraction.&lt;/p&gt;

&lt;h2&gt;
  
  
  Controller
&lt;/h2&gt;

&lt;p&gt;Your controller doesn't change at all:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="c1"&gt;// PostController.php&lt;/span&gt;
&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;store&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;Request&lt;/span&gt; &lt;span class="nv"&gt;$request&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nv"&gt;$validated&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$request&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;validate&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;
        &lt;span class="s1"&gt;'title'&lt;/span&gt;   &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'required|string|max:255'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s1"&gt;'content'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'required|string'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;]);&lt;/span&gt;

    &lt;span class="nc"&gt;Post&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$validated&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// content is already there, untouched&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;redirect&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;route&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'posts.index'&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;
  
  
  Editing existing content
&lt;/h2&gt;

&lt;p&gt;When editing a post, pre-fill the textarea with the saved value, the editor inherits it automatically:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;lt;form method="POST" action="{{ route('posts.update', $post) }}"&amp;gt;
    @csrf
    @method('PUT')

    &amp;lt;textarea id="content" name="content" class="markdown-editor"&amp;gt;{{ old('content', $post-&amp;gt;content) }}&amp;lt;/textarea&amp;gt;

    &amp;lt;button type="submit"&amp;gt;Update&amp;lt;/button&amp;gt;
&amp;lt;/form&amp;gt;

&amp;lt;script src="https://cdn.jsdelivr.net/npm/markdown-text-editor"&amp;gt;&amp;lt;/script&amp;gt;
&amp;lt;script&amp;gt;
    new MarkdownEditor('.markdown-editor');
&amp;lt;/script&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Validation failures keep the markdown
&lt;/h2&gt;

&lt;p&gt;This is where replacing the textarea hurts most, and it is the reason &lt;code&gt;old()&lt;/code&gt; is in every example above.&lt;/p&gt;

&lt;p&gt;When validation fails, Laravel redirects back with the input flashed to the session. &lt;code&gt;old('content')&lt;/code&gt; prints it straight into the textarea, and the editor picks it up from there:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;store&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;Request&lt;/span&gt; &lt;span class="nv"&gt;$request&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nv"&gt;$validated&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$request&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;validate&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;
        &lt;span class="s1"&gt;'title'&lt;/span&gt;   &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'required|string|max:255'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s1"&gt;'content'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'required|string|min:50'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;]);&lt;/span&gt;

    &lt;span class="c1"&gt;// On failure Laravel redirects back automatically — old('content')&lt;/span&gt;
    &lt;span class="c1"&gt;// refills the textarea with everything the user wrote.&lt;/span&gt;
    &lt;span class="nc"&gt;Post&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$validated&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;redirect&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;route&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'posts.index'&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;Someone writes 800 words, forgets the title, and gets the title error back with every word still in place. An editor that replaced the textarea would need its own code to restore that, and if it restores late, it overwrites what the user has already started retyping.&lt;/p&gt;

&lt;h2&gt;
  
  
  Vite or the CDN
&lt;/h2&gt;

&lt;p&gt;Both work. Use Vite when the rest of your front end is bundled:&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="c1"&gt;// resources/js/app.js&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;MarkdownEditor&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;markdown-text-editor&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&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;querySelectorAll&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;.markdown-editor&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;forEach&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;el&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;MarkdownEditor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;el&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 plaintext"&gt;&lt;code&gt;@vite(['resources/js/app.js'])
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Use the CDN for an admin panel or a single page, where a build step buys you nothing.&lt;/p&gt;

&lt;h2&gt;
  
  
  Optional: auto-save draft
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;textarea&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;.markdown-editor&lt;/span&gt;&lt;span class="dl"&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;saved&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;localStorage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getItem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;post-draft&lt;/span&gt;&lt;span class="dl"&gt;'&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;saved&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;textarea&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="nx"&gt;textarea&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="nx"&gt;saved&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;MarkdownEditor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;.markdown-editor&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;onChange&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="nx"&gt;localStorage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setItem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;post-draft&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="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;h2&gt;
  
  
  Features you get out of the box
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;WYSIWYG hybrid mode&lt;/strong&gt;: renders bold, italic and headings live while keeping raw Markdown underneath&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Live preview&lt;/strong&gt;: side-by-side preview panel&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Find &amp;amp; Replace&lt;/strong&gt;: &lt;code&gt;Ctrl+F&lt;/code&gt; / &lt;code&gt;Ctrl+H&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Keyboard shortcuts&lt;/strong&gt;: &lt;code&gt;Ctrl+B&lt;/code&gt;, &lt;code&gt;Ctrl+I&lt;/code&gt;, &lt;code&gt;Ctrl+K&lt;/code&gt;, headings via &lt;code&gt;Ctrl+1&lt;/code&gt;–&lt;code&gt;Ctrl+3&lt;/code&gt;, lists via &lt;code&gt;Ctrl+L&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;RTL support&lt;/strong&gt;, Arabic, Urdu and Farsi work out of the box&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Dark mode&lt;/strong&gt;, add &lt;code&gt;data-theme="dark"&lt;/code&gt; to any ancestor element&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;XSS safe&lt;/strong&gt;: preview sanitized with DOMPurify&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;CSP compatible&lt;/strong&gt;, no inline event handlers&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;53 KB gzipped&lt;/strong&gt; (252 KB minified, CSS included): EasyMDE is 107 KB gzipped across its JS and CSS&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Rendering markdown in Blade
&lt;/h2&gt;

&lt;p&gt;To display saved markdown in your views, use the &lt;code&gt;league/commonmark&lt;/code&gt; package:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;composer require league/commonmark
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="c1"&gt;// App\Models\Post.php&lt;/span&gt;
&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;League\CommonMark\CommonMarkConverter&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;Post&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="nc"&gt;Model&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;getRenderedContentAttribute&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nv"&gt;$converter&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;CommonMarkConverter&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;
            &lt;span class="s1"&gt;'html_input'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'strip'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="s1"&gt;'allow_unsafe_links'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="kc"&gt;false&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="nv"&gt;$converter&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;convert&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;getContent&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;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;{{-- show.blade.php --}}
&amp;lt;article&amp;gt;
    {!! $post-&amp;gt;rendered_content !!}
&amp;lt;/article&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Or keep it out of the model and convert where you render:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;League\CommonMark\CommonMarkConverter&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nv"&gt;$converter&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;CommonMarkConverter&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="s1"&gt;'html_input'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'strip'&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
&lt;span class="nv"&gt;$html&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$converter&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;convert&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$post&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;getContent&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 plaintext"&gt;&lt;code&gt;&amp;lt;article&amp;gt;{!! $html !!}&amp;lt;/article&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Note the &lt;code&gt;html_input =&amp;gt; 'strip'&lt;/code&gt; setting. Markdown allows raw HTML, so anything a user writes would otherwise be rendered as-is, and &lt;code&gt;{!! !!}&lt;/code&gt; prints it unescaped. Store the raw markdown, strip the HTML at render time, and you can change how it renders later without touching the data.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why the textarea matters
&lt;/h2&gt;

&lt;p&gt;An editor that hides the textarea and edits a copy has to guess when to put the content back. CodeMirror guesses "on submit", which is right most of the time, until the browser refuses to submit or Livewire reads the form before that moment. Every one of those failures is silent, because from the outside the field looks fine.&lt;/p&gt;

&lt;p&gt;An editor that leaves the textarea in place has nothing to guess about. The browser owns the value, Laravel reads what the browser sent, and nothing in between has an opinion.&lt;/p&gt;

&lt;p&gt;That's the whole design, and it's why the Laravel integration in this post is two lines long.&lt;/p&gt;




&lt;p&gt;Full docs, theming guide, and image upload configuration: &lt;a href="https://frutjam.com/plugins/markdown-editor" rel="noopener noreferrer"&gt;markdown-text-editor on Frutjam&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Source on &lt;a href="https://github.com/nezanuha/markdown-text-editor" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;, package on &lt;a href="https://www.npmjs.com/package/markdown-text-editor" rel="noopener noreferrer"&gt;npm&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>laravel</category>
      <category>php</category>
      <category>webdev</category>
      <category>javascript</category>
    </item>
    <item>
      <title>Zero JavaScript UI: Build Faster, More Accessible Web Apps With CSS-Only Components</title>
      <dc:creator>Mohammed Rashad</dc:creator>
      <pubDate>Wed, 07 Oct 2026 13:00:00 +0000</pubDate>
      <link>https://dev.to/rashadmehtab/zero-javascript-ui-build-faster-more-accessible-web-apps-with-css-only-components-2bol</link>
      <guid>https://dev.to/rashadmehtab/zero-javascript-ui-build-faster-more-accessible-web-apps-with-css-only-components-2bol</guid>
      <description>&lt;p&gt;The idea that you need JavaScript to build a polished, interactive web application has been quietly falling apart for years. The native web platform: HTML and CSS: now handles more UI patterns than most developers realize. Accordions, drawers, tabs, animated transitions, and accessible form controls, all achievable without a single line of JavaScript.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Frutjam&lt;/strong&gt; is a Tailwind CSS component library built on this principle. The vast majority of components require zero JavaScript. Here is what that looks like in practice, and why it matters for your performance, accessibility, and development velocity.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Browser Has Caught Up
&lt;/h2&gt;

&lt;p&gt;A few years ago, the case for JavaScript-driven UI was legitimate. The browser had no native way to create accessible modals with focus trapping, toggle accordions with smooth animations, or build floating popovers with correct positioning.&lt;/p&gt;

&lt;p&gt;Today, every one of those has a native solution:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Old JavaScript pattern&lt;/th&gt;
&lt;th&gt;Native CSS/HTML replacement&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Modal library (Bootstrap, MUI)&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;&amp;lt;dialog&amp;gt;&lt;/code&gt; element + &lt;code&gt;showModal()&lt;/code&gt; (one line of native JS)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Accordion component&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;&amp;lt;details&amp;gt;&lt;/code&gt; / &lt;code&gt;&amp;lt;summary&amp;gt;&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Toggle / swap&lt;/td&gt;
&lt;td&gt;Checkbox + CSS &lt;code&gt;:checked&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Floating popover&lt;/td&gt;
&lt;td&gt;Native Popover API&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Drawer / side panel&lt;/td&gt;
&lt;td&gt;Checkbox + CSS &lt;code&gt;:has()&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Animated transitions&lt;/td&gt;
&lt;td&gt;CSS &lt;code&gt;@starting-style&lt;/code&gt; + &lt;code&gt;transition&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Tab switching&lt;/td&gt;
&lt;td&gt;Radio inputs + CSS &lt;code&gt;:checked&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Tooltip&lt;/td&gt;
&lt;td&gt;CSS &lt;code&gt;:hover&lt;/code&gt; + &lt;code&gt;popover&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Frutjam wraps all of these patterns into clean, semantic component classes built on Tailwind CSS. The result is a fully interactive UI with zero JavaScript.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why Zero JavaScript UI Matters
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Core Web Vitals and PageSpeed
&lt;/h3&gt;

&lt;p&gt;JavaScript is the single largest contributor to poor Core Web Vitals scores. Parsing and executing JS blocks the main thread, causing:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;High &lt;strong&gt;Total Blocking Time (TBT)&lt;/strong&gt;, the metric that most directly predicts user-perceived slowness&lt;/li&gt;
&lt;li&gt;Delayed &lt;strong&gt;Time to Interactive (TTI)&lt;/strong&gt;, when the page becomes usable&lt;/li&gt;
&lt;li&gt;Layout shifts from late-rendering JS components: &lt;strong&gt;Cumulative Layout Shift (CLS)&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Removing UI JavaScript eliminates these problems at the root. There's nothing to parse, nothing to initialize, nothing to cause a layout shift. Your server renders HTML; the browser displays it immediately. That's a PageSpeed 100 by default, not by optimization.&lt;/p&gt;

&lt;p&gt;Google uses Core Web Vitals as a ranking signal. Pages with lower TBT and CLS rank better, all else being equal. Zero JavaScript components give you a structural advantage here, not a performance trick you apply after the fact.&lt;/p&gt;

&lt;h3&gt;
  
  
  Accessibility Without Configuration
&lt;/h3&gt;

&lt;p&gt;Native HTML elements carry built-in accessibility semantics. A &lt;code&gt;&amp;lt;dialog&amp;gt;&lt;/code&gt; traps focus automatically. A &lt;code&gt;&amp;lt;details&amp;gt;&lt;/code&gt; announces its open/closed state to screen readers without any &lt;code&gt;aria-*&lt;/code&gt; attributes. A radio-based tab interface is keyboard-navigable out of the box.&lt;/p&gt;

&lt;p&gt;Frutjam adds WCAG AA color contrast on top, every component's color tokens pass contrast requirements. You get accessible UI without writing &lt;code&gt;aria-expanded&lt;/code&gt;, &lt;code&gt;aria-controls&lt;/code&gt;, or &lt;code&gt;role&lt;/code&gt; attributes for the core components.&lt;/p&gt;

&lt;h3&gt;
  
  
  Works Everywhere, Re-Initializes Nowhere
&lt;/h3&gt;

&lt;p&gt;When your UI layer has no JavaScript, it works in any rendering context without additional setup:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Django templates&lt;/strong&gt;: server-rendered HTML, no client-side framework needed&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;HTMX&lt;/strong&gt;: partial HTML swaps work without re-initialization callbacks&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Laravel Blade&lt;/strong&gt;: same HTML, same components, same behavior&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;React / Next.js&lt;/strong&gt;: CSS-only components work alongside React with less client-side overhead&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Static site generators&lt;/strong&gt;: HTML output renders correctly on first paint&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This matters most for HTMX and server-rendered stacks, where JavaScript-initialized components silently break when the DOM is replaced.&lt;/p&gt;




&lt;h2&gt;
  
  
  Browser Support
&lt;/h2&gt;

&lt;p&gt;All Frutjam components rely on features that are fully supported in modern browsers:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Feature&lt;/th&gt;
&lt;th&gt;Chrome&lt;/th&gt;
&lt;th&gt;Firefox&lt;/th&gt;
&lt;th&gt;Safari&lt;/th&gt;
&lt;th&gt;Edge&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;&amp;lt;dialog&amp;gt;&lt;/code&gt; + Popover API&lt;/td&gt;
&lt;td&gt;✓ 114+&lt;/td&gt;
&lt;td&gt;✓ 122+&lt;/td&gt;
&lt;td&gt;✓ 17+&lt;/td&gt;
&lt;td&gt;✓ 114+&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;&amp;lt;details&amp;gt;&lt;/code&gt; / &lt;code&gt;&amp;lt;summary&amp;gt;&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;✓ All&lt;/td&gt;
&lt;td&gt;✓ All&lt;/td&gt;
&lt;td&gt;✓ All&lt;/td&gt;
&lt;td&gt;✓ All&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CSS &lt;code&gt;:has()&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;✓ 105+&lt;/td&gt;
&lt;td&gt;✓ 121+&lt;/td&gt;
&lt;td&gt;✓ 15.4+&lt;/td&gt;
&lt;td&gt;✓ 105+&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CSS &lt;code&gt;@starting-style&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;✓ 117+&lt;/td&gt;
&lt;td&gt;✓ 129+&lt;/td&gt;
&lt;td&gt;✓ 17.5+&lt;/td&gt;
&lt;td&gt;✓ 117+&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Coverage is effectively 95%+ of global users as of 2025. The &lt;code&gt;&amp;lt;details&amp;gt;&lt;/code&gt; element has had universal browser support since 2016.&lt;/p&gt;




&lt;h2&gt;
  
  
  Real Components, Zero JavaScript
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Modal
&lt;/h3&gt;

&lt;p&gt;The Frutjam modal uses the native &lt;code&gt;&amp;lt;dialog&amp;gt;&lt;/code&gt; element. Opening requires one line of native browser JS: &lt;code&gt;showModal()&lt;/code&gt;: via an inline &lt;code&gt;onclick&lt;/code&gt; attribute. No library, no initialization, no bundle to load:&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;button&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"btn"&lt;/span&gt; &lt;span class="na"&gt;onclick=&lt;/span&gt;&lt;span class="s"&gt;"confirmModal.showModal()"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Delete item&lt;span class="nt"&gt;&amp;lt;/button&amp;gt;&lt;/span&gt;

&lt;span class="nt"&gt;&amp;lt;dialog&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"modal modal-center modal-middle modal-slide-up"&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"confirmModal"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"modal-content"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;h2&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"heading-lg"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Are you sure?&lt;span class="nt"&gt;&amp;lt;/h2&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;p&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"para"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;This action cannot be undone.&lt;span class="nt"&gt;&amp;lt;/p&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"flex gap-2 justify-end mt-4"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;button&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"btn"&lt;/span&gt; &lt;span class="na"&gt;onclick=&lt;/span&gt;&lt;span class="s"&gt;"confirmModal.close()"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Cancel&lt;span class="nt"&gt;&amp;lt;/button&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;button&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"btn btn-error"&lt;/span&gt; &lt;span class="na"&gt;onclick=&lt;/span&gt;&lt;span class="s"&gt;"confirmModal.close()"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Delete&lt;span class="nt"&gt;&amp;lt;/button&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;button&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"modal-backdrop"&lt;/span&gt; &lt;span class="na"&gt;onclick=&lt;/span&gt;&lt;span class="s"&gt;"confirmModal.close()"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Close&lt;span class="nt"&gt;&amp;lt;/button&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/dialog&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;showModal()&lt;/code&gt; is a native &lt;code&gt;HTMLDialogElement&lt;/code&gt; method, it is part of the browser, not a library. The &lt;code&gt;&amp;lt;dialog&amp;gt;&lt;/code&gt; element handles focus trapping, ESC key dismissal, and top-layer rendering automatically. The slide-up animation (&lt;code&gt;modal-slide-up&lt;/code&gt;) is pure CSS.&lt;/p&gt;

&lt;h3&gt;
  
  
  Animated Accordion
&lt;/h3&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;details&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"accordion"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;summary&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"accordion-header"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    What is CSS-only UI?
  &lt;span class="nt"&gt;&amp;lt;/summary&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"accordion-body"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;p&amp;gt;&lt;/span&gt;UI components that require no JavaScript to function. State is managed
    by the browser via native HTML elements and CSS selectors.&lt;span class="nt"&gt;&amp;lt;/p&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/details&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;@starting-style&lt;/code&gt; CSS rule in Frutjam animates the expand/collapse. No JavaScript event listeners, no animation libraries.&lt;/p&gt;

&lt;h3&gt;
  
  
  CSS-Only Tab Panels
&lt;/h3&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;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"tabs tabs-boxed"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;input&lt;/span&gt; &lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"radio"&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"code-tabs"&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"tab"&lt;/span&gt; &lt;span class="na"&gt;aria-label=&lt;/span&gt;&lt;span class="s"&gt;"HTML"&lt;/span&gt; &lt;span class="na"&gt;checked&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"tab-content"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;HTML version here&lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;

  &lt;span class="nt"&gt;&amp;lt;input&lt;/span&gt; &lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"radio"&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"code-tabs"&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"tab"&lt;/span&gt; &lt;span class="na"&gt;aria-label=&lt;/span&gt;&lt;span class="s"&gt;"React"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"tab-content"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;React version here&lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;

  &lt;span class="nt"&gt;&amp;lt;input&lt;/span&gt; &lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"radio"&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"code-tabs"&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"tab"&lt;/span&gt; &lt;span class="na"&gt;aria-label=&lt;/span&gt;&lt;span class="s"&gt;"Django"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"tab-content"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Django template version here&lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The CSS &lt;code&gt;:checked&lt;/code&gt; selector drives which panel is visible. Keyboard navigation between tabs is browser-native via arrow keys.&lt;/p&gt;

&lt;h3&gt;
  
  
  Drawer
&lt;/h3&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;input&lt;/span&gt; &lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"checkbox"&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"drawer-toggle"&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"drawer-toggle"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"drawer-content"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;label&lt;/span&gt; &lt;span class="na"&gt;for=&lt;/span&gt;&lt;span class="s"&gt;"drawer-toggle"&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"btn"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Open Sidebar&lt;span class="nt"&gt;&amp;lt;/label&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"drawer-side"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;label&lt;/span&gt; &lt;span class="na"&gt;for=&lt;/span&gt;&lt;span class="s"&gt;"drawer-toggle"&lt;/span&gt; &lt;span class="na"&gt;aria-label=&lt;/span&gt;&lt;span class="s"&gt;"Close"&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"drawer-overlay"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/label&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;nav&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"menu bg-base-100 min-h-full w-64 p-4"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;li&amp;gt;&amp;lt;a&lt;/span&gt; &lt;span class="na"&gt;href=&lt;/span&gt;&lt;span class="s"&gt;"/"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Home&lt;span class="nt"&gt;&amp;lt;/a&amp;gt;&amp;lt;/li&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;li&amp;gt;&amp;lt;a&lt;/span&gt; &lt;span class="na"&gt;href=&lt;/span&gt;&lt;span class="s"&gt;"/components"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Components&lt;span class="nt"&gt;&amp;lt;/a&amp;gt;&amp;lt;/li&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/nav&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Open/close state lives in the checkbox. CSS &lt;code&gt;:has(:checked)&lt;/code&gt; drives the layout. No JavaScript involved.&lt;/p&gt;

&lt;h3&gt;
  
  
  Toast Notifications
&lt;/h3&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;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"toast toast-top toast-end"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"alert alert-success"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;span&amp;gt;&lt;/span&gt;Changes saved successfully.&lt;span class="nt"&gt;&amp;lt;/span&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Frutjam's toast is positioned via CSS. For timed auto-dismiss, a single CSS animation handles the fade-out, no JavaScript timer required for the basic pattern.&lt;/p&gt;




&lt;h3&gt;
  
  
  Searchable Combobox
&lt;/h3&gt;

&lt;p&gt;A combobox is one UI pattern where pure CSS hits a real limit, filtering a list as the user types requires JavaScript. But you don't have to write it yourself.&lt;/p&gt;

&lt;p&gt;Frutjam ships &lt;code&gt;createCombobox&lt;/code&gt; from &lt;code&gt;frutjam/js&lt;/code&gt;, a built-in helper that adds filtering, keyboard navigation (↑ ↓ Enter Escape), and &lt;code&gt;aria-*&lt;/code&gt; wiring in a single call. The open/close behavior is still CSS-only (&lt;code&gt;focus-within:combobox-open&lt;/code&gt;):&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;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"relative w-64 focus-within:combobox-open"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;input&lt;/span&gt; &lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"text"&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"input w-full"&lt;/span&gt; &lt;span class="na"&gt;placeholder=&lt;/span&gt;&lt;span class="s"&gt;"Search..."&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;ul&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"combobox-list"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;li&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"combobox-item"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;React&lt;span class="nt"&gt;&amp;lt;/li&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;li&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"combobox-item"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Vue&lt;span class="nt"&gt;&amp;lt;/li&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;li&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"combobox-item"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Angular&lt;span class="nt"&gt;&amp;lt;/li&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;li&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"combobox-item"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Svelte&lt;span class="nt"&gt;&amp;lt;/li&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/ul&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/div&amp;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 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;createCombobox&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;frutjam/js&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;span class="nf"&gt;createCombobox&lt;/span&gt;&lt;span class="p"&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;.my-combobox&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;p&gt;The browser has no native combobox element. When a UI pattern genuinely requires JavaScript, Frutjam provides it, so you are not writing filtering logic, keyboard handling, and ARIA attributes from scratch every time.&lt;/p&gt;

&lt;h2&gt;
  
  
  What You Do Not Give Up
&lt;/h2&gt;

&lt;p&gt;The common concern with zero JavaScript UI is capability. For standard UI components, there is no meaningful sacrifice:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Smooth animations&lt;/strong&gt;: CSS &lt;code&gt;@starting-style&lt;/code&gt; and &lt;code&gt;transition&lt;/code&gt; handle enter/exit animations natively&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Focus management&lt;/strong&gt;: &lt;code&gt;&amp;lt;dialog&amp;gt;&lt;/code&gt; traps focus natively; focus returns to the trigger on close&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Keyboard navigation&lt;/strong&gt;: native inputs are keyboard-navigable without writing event listeners; combobox keyboard nav is handled by the built-in &lt;code&gt;createCombobox()&lt;/code&gt; helper&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Dark mode&lt;/strong&gt;: &lt;code&gt;data-theme&lt;/code&gt; attribute switching with zero JavaScript&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Where JavaScript is genuinely necessary: form validation with real-time feedback, async data fetching, complex application state: tools like HTMX handle it at the request/response level, not the component level. Your UI components stay clean and framework-agnostic.
&lt;/h2&gt;

&lt;h2&gt;
  
  
  Installation
&lt;/h2&gt;

&lt;p&gt;Install Frutjam alongside Tailwind CSS:&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; &lt;span class="nt"&gt;-D&lt;/span&gt; frutjam
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Add two lines to your CSS:&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="k"&gt;@import&lt;/span&gt; &lt;span class="s1"&gt;"tailwindcss"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;@plugin&lt;/span&gt; &lt;span class="s1"&gt;"frutjam"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Apply a theme to your HTML root:&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;html&lt;/span&gt; &lt;span class="na"&gt;data-theme=&lt;/span&gt;&lt;span class="s"&gt;"darkberry"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every component is immediately available. No JavaScript configuration, no plugin initialization, no build step beyond Tailwind itself.&lt;/p&gt;




&lt;h2&gt;
  
  
  Frequently Asked Questions
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Do Frutjam components work with React or Next.js?&lt;/strong&gt;&lt;br&gt;
Yes. React renders HTML; Frutjam styles HTML. You apply Frutjam class names to JSX elements the same way you apply any CSS class. For modals, call &lt;code&gt;document.getElementById("myModal").showModal()&lt;/code&gt; from React state, the &lt;code&gt;&amp;lt;dialog&amp;gt;&lt;/code&gt; handles everything else. Accordions, drawers, and tabs need zero React state at all.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What about form validation without JavaScript?&lt;/strong&gt;&lt;br&gt;
CSS-only validation works for required fields, pattern matching, and email format checking using the &lt;code&gt;:invalid&lt;/code&gt; and &lt;code&gt;:valid&lt;/code&gt; pseudo-classes. For complex validation logic or async server-side checks, a minimal amount of JavaScript (or HTMX) is appropriate. The point isn't to eliminate JavaScript entirely, it's to stop using JavaScript for UI state that native HTML handles better.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Is &lt;code&gt;&amp;lt;dialog&amp;gt;&lt;/code&gt; supported on mobile?&lt;/strong&gt;&lt;br&gt;
Yes. The &lt;code&gt;&amp;lt;dialog&amp;gt;&lt;/code&gt; element has been supported in Safari on iOS since version 15.4 (released March 2022), Chrome Android since version 37, and Firefox Android since version 98. Global mobile coverage is above 95%.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How do I animate component open/close without JavaScript?&lt;/strong&gt;&lt;br&gt;
Frutjam uses CSS &lt;code&gt;@starting-style&lt;/code&gt; combined with &lt;code&gt;transition&lt;/code&gt; to animate components in and out. This is a native CSS feature supported in all modern browsers since 2023–2024. The animation is defined in the library, you get smooth transitions on &lt;code&gt;&amp;lt;dialog&amp;gt;&lt;/code&gt;, &lt;code&gt;&amp;lt;details&amp;gt;&lt;/code&gt;, and popover elements without any JavaScript or animation library.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Can I use Frutjam alongside a JavaScript framework like Alpine.js?&lt;/strong&gt;&lt;br&gt;
Yes. Frutjam doesn't own any DOM state, so it coexists cleanly with Alpine.js, Stimulus, or any small JS library. Accordions and drawers are entirely CSS, Alpine never touches them. For modals, you can use either Frutjam's native &lt;code&gt;showModal()&lt;/code&gt; or wire it to an Alpine variable, both work.&lt;/p&gt;




&lt;h2&gt;
  
  
  What Zero JavaScript UI Looks Like in Production
&lt;/h2&gt;

&lt;p&gt;A production page using Frutjam typically has three assets:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Asset&lt;/th&gt;
&lt;th&gt;Size (min+gzip)&lt;/th&gt;
&lt;th&gt;Notes&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Tailwind CSS (JIT)&lt;/td&gt;
&lt;td&gt;~5–8 KB&lt;/td&gt;
&lt;td&gt;Base utilities, only used classes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Frutjam CSS (JIT)&lt;/td&gt;
&lt;td&gt;~12–18 KB&lt;/td&gt;
&lt;td&gt;Component styles, only used classes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;HTMX (optional)&lt;/td&gt;
&lt;td&gt;~14 KB&lt;/td&gt;
&lt;td&gt;Only if you need dynamic content&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Total page weight for a UI-heavy page: &lt;strong&gt;~20–40 KB&lt;/strong&gt; with zero JavaScript UI components. Compare this to a typical React + UI library setup at 150–400 KB before application code. The difference is measurable in Lighthouse scores, Core Web Vitals, and user-perceived load time: especially on mobile networks.&lt;/p&gt;

</description>
      <category>css</category>
      <category>html</category>
      <category>webdev</category>
      <category>frontend</category>
    </item>
    <item>
      <title>Dropdowns That Position Themselves: CSS Anchor Positioning Is Baseline Now</title>
      <dc:creator>Mohammed Rashad</dc:creator>
      <pubDate>Mon, 05 Oct 2026 16:41:06 +0000</pubDate>
      <link>https://dev.to/rashadmehtab/dropdowns-that-position-themselves-css-anchor-positioning-is-baseline-now-450d</link>
      <guid>https://dev.to/rashadmehtab/dropdowns-that-position-themselves-css-anchor-positioning-is-baseline-now-450d</guid>
      <description>&lt;p&gt;Every dropdown you have ever built had the same problem underneath it: the panel has to appear next to the button, stay there while the page scrolls, and flip to the other side when it runs out of room.&lt;/p&gt;

&lt;p&gt;For a decade the only answer was JavaScript. Popper.js, and then Floating UI, existed almost entirely to measure two rectangles, do the arithmetic, and rewrite &lt;code&gt;top&lt;/code&gt; and &lt;code&gt;left&lt;/code&gt; on every scroll and resize.&lt;/p&gt;

&lt;p&gt;That's now a CSS feature, and as of this month it works in every major browser.&lt;/p&gt;

&lt;h2&gt;
  
  
  What changed
&lt;/h2&gt;

&lt;p&gt;CSS anchor positioning reached &lt;a href="https://developer.mozilla.org/en-US/docs/Web/CSS/Reference/Properties/position-anchor" rel="noopener noreferrer"&gt;Baseline&lt;/a&gt; in September 2026. Chromium shipped it first and the others have now followed; MDN has the version table. It's marked &lt;em&gt;newly available&lt;/em&gt;, which means the current release of every major browser supports it, while older installs do not.&lt;/p&gt;

&lt;p&gt;The idea is simple. One element gives itself a name; another says it wants to be positioned relative to that name.&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="nc"&gt;.trigger&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="py"&gt;anchor-name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;--my-menu&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nc"&gt;.panel&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;position&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;absolute&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="py"&gt;position-anchor&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;--my-menu&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="py"&gt;position-area&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;block-end&lt;/span&gt; &lt;span class="n"&gt;span-inline-end&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;   &lt;span class="c"&gt;/* below, aligned to the end */&lt;/span&gt;
  &lt;span class="py"&gt;position-try-fallbacks&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;flip-block&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;          &lt;span class="c"&gt;/* flip above if it won't fit */&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The browser keeps them tethered from then on. Scroll the page, resize the window, move the trigger, the panel follows, because the relationship is declared rather than calculated.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;position-try-fallbacks&lt;/code&gt; is the part that used to be most of the library code. Give it a list, and when the preferred placement would put the panel off screen, the browser tries the next one: flip vertically, flip horizontally, or both.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it replaces
&lt;/h2&gt;

&lt;p&gt;A typical Floating UI setup for a single dropdown:&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;computePosition&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;flip&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;shift&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;offset&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;autoUpdate&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;@floating-ui/dom&lt;/span&gt;&lt;span class="dl"&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;cleanup&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;autoUpdate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;button&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;panel&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;computePosition&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;button&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;panel&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;placement&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;bottom-end&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;middleware&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;offset&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="nf"&gt;flip&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="nf"&gt;shift&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;padding&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;8&lt;/span&gt; &lt;span class="p"&gt;})],&lt;/span&gt;
  &lt;span class="p"&gt;}).&lt;/span&gt;&lt;span class="nf"&gt;then&lt;/span&gt;&lt;span class="p"&gt;(({&lt;/span&gt; &lt;span class="nx"&gt;x&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;y&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="nb"&gt;Object&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;assign&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;panel&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;style&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;left&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;x&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;px`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;top&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;y&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;px`&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="c1"&gt;// and remember to call cleanup() when the panel closes&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's a dependency, a listener on every scrollable ancestor, a layout read and a style write on every frame, and a teardown function you have to remember. The CSS above does the same job with no bytes shipped and nothing to clean up.&lt;/p&gt;

&lt;h2&gt;
  
  
  How Frutjam's popover uses it
&lt;/h2&gt;

&lt;p&gt;We built the popover on anchor positioning before it was Baseline, so the implementation is already in the library. It's three declarations.&lt;/p&gt;

&lt;p&gt;The trigger names itself:&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="k"&gt;@utility&lt;/span&gt; &lt;span class="n"&gt;popover-toggle&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="py"&gt;anchor-name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;var&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;--popover--anchor-name&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The panel anchors to that name and picks an area:&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="k"&gt;@utility&lt;/span&gt; &lt;span class="n"&gt;popover-content&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="py"&gt;position-anchor&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;var&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;--popover--anchor-name&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="py"&gt;position-area&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;var&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;--popover--position-area&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;block-end&lt;/span&gt; &lt;span class="n"&gt;span-inline-end&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="py"&gt;position-try-fallbacks&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;var&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;--popover--position-try&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;And each placement class sets the variables:&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="k"&gt;@utility&lt;/span&gt; &lt;span class="n"&gt;popover-top-start&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="py"&gt;--popover--position-area&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;block-start&lt;/span&gt; &lt;span class="n"&gt;span-inline-start&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;Twelve placement classes, one line of CSS each. In markup you pick one:&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;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"popover popover-top-start"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;button&lt;/span&gt; &lt;span class="na"&gt;popovertarget=&lt;/span&gt;&lt;span class="s"&gt;"menu"&lt;/span&gt; &lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"button"&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"popover-toggle btn"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Options&lt;span class="nt"&gt;&amp;lt;/button&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"popover-content"&lt;/span&gt; &lt;span class="na"&gt;popover&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"menu"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;ul&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"menu"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;li&amp;gt;&amp;lt;a&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"menu-item"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Edit&lt;span class="nt"&gt;&amp;lt;/a&amp;gt;&amp;lt;/li&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;li&amp;gt;&amp;lt;a&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"menu-item"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Delete&lt;span class="nt"&gt;&amp;lt;/a&amp;gt;&amp;lt;/li&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/ul&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's the whole dropdown. The &lt;code&gt;popover&lt;/code&gt; attribute gives you click-outside to close, Escape to close, and rendering above everything without a &lt;code&gt;z-index&lt;/code&gt;. Anchor positioning decides where it goes. &lt;code&gt;@starting-style&lt;/code&gt; animates it in. There's no JavaScript on the page.&lt;/p&gt;

&lt;h2&gt;
  
  
  Logical properties, so RTL comes free
&lt;/h2&gt;

&lt;p&gt;Note what the placement values say: &lt;code&gt;block-start&lt;/code&gt;, &lt;code&gt;span-inline-end&lt;/code&gt;. Not top and right.&lt;/p&gt;

&lt;p&gt;Those are logical directions, resolved against the writing direction of the document. In an Arabic or Hebrew layout, a popover aligned to &lt;code&gt;inline-start&lt;/code&gt; moves to the right-hand side by itself, because "start" means the side the text starts from.&lt;/p&gt;

&lt;p&gt;A dropdown positioned in pixels needs a second set of rules under &lt;code&gt;[dir="rtl"]&lt;/code&gt;, and someone has to remember to write them. This is the kind of thing that made RTL support a project of its own, and the reason so many libraries treat it as an afterthought.&lt;/p&gt;

&lt;h2&gt;
  
  
  What to do about older browsers
&lt;/h2&gt;

&lt;p&gt;&lt;em&gt;Newly available&lt;/em&gt; means the current version of each major browser has it, not every browser in use. Plan for both.&lt;/p&gt;

&lt;p&gt;Frutjam's popover degrades quietly. Where anchor positioning is unavailable the panel still opens, still closes on Escape and on outside clicks, and still renders above the page; it simply falls back to the static position it would have had in the flow, near its trigger, instead of being tethered to it. Nothing breaks, the placement is just less precise.&lt;/p&gt;

&lt;p&gt;If you want to branch explicitly, feature-query it:&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="nc"&gt;.panel&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c"&gt;/* placement that works anywhere */&lt;/span&gt;
  &lt;span class="nl"&gt;position&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;absolute&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="py"&gt;inset-block-start&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;100%&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;@supports&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;anchor-name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;--x&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nc"&gt;.panel&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="py"&gt;position-anchor&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;--my-menu&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="py"&gt;position-area&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;block-end&lt;/span&gt; &lt;span class="n"&gt;span-inline-end&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="py"&gt;position-try-fallbacks&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;flip-block&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;Worth deciding deliberately rather than by accident: a dropdown that is slightly misplaced on an old browser is usually acceptable, while one that opens off screen is not.&lt;/p&gt;

&lt;h2&gt;
  
  
  The pattern this belongs to
&lt;/h2&gt;

&lt;p&gt;Anchor positioning is the fourth in a run of features that each deleted a category of UI JavaScript:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Feature&lt;/th&gt;
&lt;th&gt;What it replaced&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;&amp;lt;dialog&amp;gt;&lt;/code&gt; and &lt;code&gt;showModal()&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Focus trapping, the inert background, Escape handling&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;The popover API&lt;/td&gt;
&lt;td&gt;Click-outside handlers, &lt;code&gt;z-index&lt;/code&gt; stacking wars&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;@starting-style&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Animation libraries for enter and exit transitions&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Anchor positioning&lt;/td&gt;
&lt;td&gt;Popper.js, Floating UI, scroll listeners&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;None of these are exotic. They are the boring, load-bearing parts of a UI library, and the platform has absorbed them one at a time over about three years.&lt;/p&gt;

&lt;p&gt;What changes is the cost of a component library. A dropdown used to mean a dependency, a runtime and a bundle. Now it's a handful of CSS custom properties, and the browser does it faster than any library could, because it already knows where everything is.&lt;/p&gt;




&lt;p&gt;The &lt;a href="https://frutjam.com/components/popover" rel="noopener noreferrer"&gt;popover component&lt;/a&gt; ships all twelve placements with edge flipping, and every Frutjam component is CSS-only by default.&lt;/p&gt;

</description>
      <category>css</category>
      <category>webdev</category>
      <category>frontend</category>
      <category>html</category>
    </item>
    <item>
      <title>Why JS component libraries break with htmx (and what I use instead)</title>
      <dc:creator>Mohammed Rashad</dc:creator>
      <pubDate>Wed, 30 Sep 2026 13:00:00 +0000</pubDate>
      <link>https://dev.to/rashadmehtab/why-js-component-libraries-break-with-htmx-and-what-i-use-instead-4730</link>
      <guid>https://dev.to/rashadmehtab/why-js-component-libraries-break-with-htmx-and-what-i-use-instead-4730</guid>
      <description>&lt;p&gt;You swap a fragment with htmx and the dropdowns inside it stop working. The markup is right, the classes are right, but the component library's JavaScript ran once on page load and never saw the new HTML.&lt;/p&gt;

&lt;p&gt;So you wire up htmx:afterSwap to re-initialise everything, and then you do it again for every component, on every endpoint that returns a partial.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Frutjam&lt;/strong&gt; is a CSS-only Tailwind CSS component library built to complement HTMX naturally. No JS library or plugin means no re-initialization problems when HTMX swaps HTML fragments.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why JavaScript-Driven UI Libraries Conflict With HTMX
&lt;/h2&gt;

&lt;p&gt;HTMX's core idea is simple: use HTML attributes to add dynamic behavior to elements, letting the server return HTML fragments. It intentionally avoids a JavaScript component model.&lt;/p&gt;

&lt;p&gt;That design breaks the moment you add a JavaScript-driven UI library:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Initialization timing.&lt;/strong&gt; JavaScript UI components register on &lt;code&gt;DOMContentLoaded&lt;/code&gt; or similar hooks. When HTMX swaps in new HTML, those hooks do not re-run. You end up writing &lt;code&gt;htmx:afterSwap&lt;/code&gt; event listeners just to re-initialize dropdowns or modals — code that fights HTMX rather than working with it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;State conflicts.&lt;/strong&gt; JavaScript UI libraries maintain internal state. When HTMX replaces a DOM node that a JavaScript library has claimed, you get ghost listeners, duplicated event handlers, or broken components.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Bundle bloat.&lt;/strong&gt; HTMX itself is around 14 KB (min+gzip). Loading a full JavaScript UI framework on top defeats the performance argument for choosing HTMX in the first place.&lt;/p&gt;

&lt;p&gt;The solution is a UI library with no JavaScript to re-initialize — one where every component is pure HTML and CSS.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why Not Just Use Alpine.js?
&lt;/h2&gt;

&lt;p&gt;Alpine.js is often recommended alongside HTMX for UI behavior. It works well for small amounts of client-side state. But it introduces the same class of problem as a JavaScript UI library:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Alpine components need to initialize — &lt;code&gt;x-data&lt;/code&gt; blocks set up on page load&lt;/li&gt;
&lt;li&gt;HTMX-swapped HTML containing Alpine components needs &lt;code&gt;Alpine.initTree()&lt;/code&gt; called manually&lt;/li&gt;
&lt;li&gt;You are now managing two JavaScript libraries (HTMX + Alpine) plus a CSS library&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;With Frutjam, there is no Alpine.js needed for standard UI components. Accordions, drawers, and tabs are pure CSS. Modals use a single inline &lt;code&gt;onclick="id.showModal()"&lt;/code&gt; — native browser JS, not a library, so HTMX swaps never break them. HTMX handles data; Frutjam handles the UI.&lt;/p&gt;




&lt;h2&gt;
  
  
  Frutjam: Built for HTMX-Powered Apps
&lt;/h2&gt;

&lt;p&gt;Every Frutjam component relies on native browser behavior and CSS. HTMX can swap any fragment containing Frutjam components and they will work correctly — no callbacks, no re-initialization scripts, no edge cases.&lt;/p&gt;

&lt;h3&gt;
  
  
  Modals via Native &lt;code&gt;&amp;lt;dialog&amp;gt;&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;Frutjam modals use the native &lt;code&gt;&amp;lt;dialog&amp;gt;&lt;/code&gt; element with inline &lt;code&gt;onclick="id.showModal()"&lt;/code&gt;. Because the trigger is inline HTML — not a JavaScript library initialization — HTMX can swap a fragment containing this button and it works immediately with no callback:&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="c"&gt;&amp;lt;!-- Server returns this fragment; HTMX swaps it into #modal-container --&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;button&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"btn btn-error"&lt;/span&gt; &lt;span class="na"&gt;onclick=&lt;/span&gt;&lt;span class="s"&gt;"confirmModal.showModal()"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Delete&lt;span class="nt"&gt;&amp;lt;/button&amp;gt;&lt;/span&gt;

&lt;span class="nt"&gt;&amp;lt;dialog&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"modal modal-center modal-middle"&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"confirmModal"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"modal-content"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;h2&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"heading-lg"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Confirm delete&lt;span class="nt"&gt;&amp;lt;/h2&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;p&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"para"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;This action cannot be undone.&lt;span class="nt"&gt;&amp;lt;/p&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"flex gap-2 justify-end mt-4"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;button&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"btn"&lt;/span&gt; &lt;span class="na"&gt;onclick=&lt;/span&gt;&lt;span class="s"&gt;"confirmModal.close()"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Cancel&lt;span class="nt"&gt;&amp;lt;/button&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;button&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"btn btn-error"&lt;/span&gt;
        &lt;span class="na"&gt;hx-delete=&lt;/span&gt;&lt;span class="s"&gt;"/items/42"&lt;/span&gt;
        &lt;span class="na"&gt;hx-target=&lt;/span&gt;&lt;span class="s"&gt;"#item-list"&lt;/span&gt;
        &lt;span class="na"&gt;hx-swap=&lt;/span&gt;&lt;span class="s"&gt;"outerHTML"&lt;/span&gt;
        &lt;span class="na"&gt;onclick=&lt;/span&gt;&lt;span class="s"&gt;"confirmModal.close()"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Delete&lt;span class="nt"&gt;&amp;lt;/button&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;button&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"modal-backdrop"&lt;/span&gt; &lt;span class="na"&gt;onclick=&lt;/span&gt;&lt;span class="s"&gt;"confirmModal.close()"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Close&lt;span class="nt"&gt;&amp;lt;/button&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/dialog&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No &lt;code&gt;htmx:afterSwap&lt;/code&gt; listener. No JavaScript library to reinitialize. The &lt;code&gt;onclick&lt;/code&gt; attribute is re-evaluated by the browser on every render, so HTMX swaps just work.&lt;/p&gt;

&lt;h3&gt;
  
  
  Accordions With Lazy-Loaded Content
&lt;/h3&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;details&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"accordion"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;summary&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"accordion-header"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Order details&lt;span class="nt"&gt;&amp;lt;/summary&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"accordion-body"&lt;/span&gt;
    &lt;span class="na"&gt;hx-get=&lt;/span&gt;&lt;span class="s"&gt;"/orders/42/details"&lt;/span&gt;
    &lt;span class="na"&gt;hx-trigger=&lt;/span&gt;&lt;span class="s"&gt;"toggle once"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    Loading...
  &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/details&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;&amp;lt;details&amp;gt;&lt;/code&gt; element holds its open/closed state natively. HTMX lazy-loads content on the first open toggle without any component re-initialization.&lt;/p&gt;

&lt;h3&gt;
  
  
  Drawers via Checkbox
&lt;/h3&gt;

&lt;p&gt;Frutjam's drawer uses a native checkbox for open/closed state. HTMX can freely swap content inside the drawer without touching the toggle:&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;input&lt;/span&gt; &lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"checkbox"&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"menu-toggle"&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"drawer-toggle"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;

&lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"drawer-content"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;label&lt;/span&gt; &lt;span class="na"&gt;for=&lt;/span&gt;&lt;span class="s"&gt;"menu-toggle"&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"btn"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Open Menu&lt;span class="nt"&gt;&amp;lt;/label&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;

&lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"drawer-side"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;label&lt;/span&gt; &lt;span class="na"&gt;for=&lt;/span&gt;&lt;span class="s"&gt;"menu-toggle"&lt;/span&gt; &lt;span class="na"&gt;aria-label=&lt;/span&gt;&lt;span class="s"&gt;"Close menu"&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"drawer-overlay"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/label&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;nav&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"menu bg-base-100 min-h-full w-64 p-4"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;ul&lt;/span&gt; &lt;span class="na"&gt;hx-get=&lt;/span&gt;&lt;span class="s"&gt;"/nav/items"&lt;/span&gt; &lt;span class="na"&gt;hx-trigger=&lt;/span&gt;&lt;span class="s"&gt;"load"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;li&amp;gt;&lt;/span&gt;Loading...&lt;span class="nt"&gt;&amp;lt;/li&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/ul&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/nav&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;HTMX updates the nav list; the checkbox controls drawer open/close. They never interfere.&lt;/p&gt;

&lt;h3&gt;
  
  
  Server-Driven Pagination
&lt;/h3&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;div&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"results"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="c"&gt;&amp;lt;!-- Content updated by HTMX --&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;

&lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"join"&lt;/span&gt; &lt;span class="na"&gt;hx-target=&lt;/span&gt;&lt;span class="s"&gt;"#results"&lt;/span&gt; &lt;span class="na"&gt;hx-swap=&lt;/span&gt;&lt;span class="s"&gt;"innerHTML"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;button&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"join-item btn"&lt;/span&gt; &lt;span class="na"&gt;hx-get=&lt;/span&gt;&lt;span class="s"&gt;"/items?page=1"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;1&lt;span class="nt"&gt;&amp;lt;/button&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;button&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"join-item btn btn-active"&lt;/span&gt; &lt;span class="na"&gt;hx-get=&lt;/span&gt;&lt;span class="s"&gt;"/items?page=2"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;2&lt;span class="nt"&gt;&amp;lt;/button&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;button&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"join-item btn"&lt;/span&gt; &lt;span class="na"&gt;hx-get=&lt;/span&gt;&lt;span class="s"&gt;"/items?page=3"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;3&lt;span class="nt"&gt;&amp;lt;/button&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No JavaScript pagination library. No state management. The server controls the active page; Frutjam handles the visual system.&lt;/p&gt;




&lt;h3&gt;
  
  
  HTMX-Powered Search with Combobox
&lt;/h3&gt;

&lt;p&gt;Frutjam's combobox pairs naturally with HTMX for server-side filtering. The open/close behavior is CSS-only (&lt;code&gt;focus-within:combobox-open&lt;/code&gt;). For live search, &lt;code&gt;hx-get&lt;/code&gt; sends the query to the server and HTMX swaps the results — no client-side filtering needed at all:&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;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"relative w-full focus-within:combobox-open"&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"user-search"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;input&lt;/span&gt; &lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"text"&lt;/span&gt;
    &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"input w-full"&lt;/span&gt;
    &lt;span class="na"&gt;placeholder=&lt;/span&gt;&lt;span class="s"&gt;"Search users..."&lt;/span&gt;
    &lt;span class="na"&gt;hx-get=&lt;/span&gt;&lt;span class="s"&gt;"/users/search"&lt;/span&gt;
    &lt;span class="na"&gt;hx-target=&lt;/span&gt;&lt;span class="s"&gt;"#user-results"&lt;/span&gt;
    &lt;span class="na"&gt;hx-trigger=&lt;/span&gt;&lt;span class="s"&gt;"input changed delay:200ms"&lt;/span&gt;
    &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"q"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;ul&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"combobox-list"&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"user-results"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="c"&gt;&amp;lt;!-- Server returns &amp;lt;li class="combobox-item"&amp;gt; fragments here --&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/ul&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The server returns a list of &lt;code&gt;&amp;lt;li class="combobox-item"&amp;gt;&lt;/code&gt; elements. HTMX swaps them in. No client-side filtering script, no component re-initialization. If you want client-side filtering instead — for small static lists — add the built-in helper:&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;createCombobox&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;frutjam/js&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;span class="nf"&gt;createCombobox&lt;/span&gt;&lt;span class="p"&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;#user-search&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;p&gt;&lt;code&gt;createCombobox&lt;/code&gt; adds keyboard navigation (↑ ↓ Enter Escape) and &lt;code&gt;aria-*&lt;/code&gt; attributes. The browser has no native combobox element, so unlike the accordion or drawer, filtering logic has to come from somewhere — Frutjam ships it so you do not write it.&lt;/p&gt;




&lt;h2&gt;
  
  
  Django Tailwind Components
&lt;/h2&gt;

&lt;p&gt;Two CSS imports. That is the entire setup:&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="c"&gt;/* In your base CSS file */&lt;/span&gt;
&lt;span class="k"&gt;@import&lt;/span&gt; &lt;span class="s1"&gt;"tailwindcss"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;@plugin&lt;/span&gt; &lt;span class="s1"&gt;"frutjam"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Install the npm package alongside Tailwind:&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; &lt;span class="nt"&gt;-D&lt;/span&gt; frutjam
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every Django template, Jinja2 partial, and DRF HTML response can use Frutjam components immediately. HTMX attributes work on any Frutjam element without additional configuration.&lt;/p&gt;

&lt;p&gt;A typical Django + HTMX + Frutjam template looks like this:&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="c"&gt;&amp;lt;!-- base.html --&amp;gt;&lt;/span&gt;
&lt;span class="cp"&gt;&amp;lt;!DOCTYPE html&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;html&lt;/span&gt; &lt;span class="na"&gt;data-theme=&lt;/span&gt;&lt;span class="s"&gt;"darkberry"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;head&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;link&lt;/span&gt; &lt;span class="na"&gt;rel=&lt;/span&gt;&lt;span class="s"&gt;"stylesheet"&lt;/span&gt; &lt;span class="na"&gt;href=&lt;/span&gt;&lt;span class="s"&gt;"{% static 'css/app.css' %}"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;script &lt;/span&gt;&lt;span class="na"&gt;src=&lt;/span&gt;&lt;span class="s"&gt;"https://unpkg.com/htmx.org@2"&lt;/span&gt; &lt;span class="na"&gt;defer&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/script&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/head&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;body&amp;gt;&lt;/span&gt;
  {% block content %}{% endblock %}
&lt;span class="nt"&gt;&amp;lt;/body&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/html&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No JavaScript UI framework. No component initialization. HTMX handles behavior; Frutjam handles UI.&lt;/p&gt;




&lt;h2&gt;
  
  
  Laravel Tailwind Components
&lt;/h2&gt;

&lt;p&gt;The setup in Laravel is identical — Frutjam is framework-agnostic:&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="c"&gt;/* resources/css/app.css */&lt;/span&gt;
&lt;span class="k"&gt;@import&lt;/span&gt; &lt;span class="s1"&gt;"tailwindcss"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;@plugin&lt;/span&gt; &lt;span class="s1"&gt;"frutjam"&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 shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-D&lt;/span&gt; frutjam
npm run build
&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;!-- resources/views/layouts/app.blade.php --&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;html&lt;/span&gt; &lt;span class="na"&gt;data-theme=&lt;/span&gt;&lt;span class="s"&gt;"darkberry"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;head&amp;gt;&lt;/span&gt;
  @vite(['resources/css/app.css'])
  &lt;span class="nt"&gt;&amp;lt;script &lt;/span&gt;&lt;span class="na"&gt;src=&lt;/span&gt;&lt;span class="s"&gt;"https://unpkg.com/htmx.org@2"&lt;/span&gt; &lt;span class="na"&gt;defer&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/script&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/head&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;body&amp;gt;&lt;/span&gt;
  @yield('content')
&lt;span class="nt"&gt;&amp;lt;/body&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/html&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Blade partials returned by HTMX-targeted routes contain Frutjam components that work immediately on insertion — no JavaScript initialization step.&lt;/p&gt;




&lt;h2&gt;
  
  
  Performance: The Frutjam + HTMX Stack
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Asset&lt;/th&gt;
&lt;th&gt;Size (min+gzip)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;HTMX&lt;/td&gt;
&lt;td&gt;~14 KB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Frutjam CSS (JIT, used classes only)&lt;/td&gt;
&lt;td&gt;~12–18 KB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;UI component JavaScript&lt;/td&gt;
&lt;td&gt;0 KB (core) / ~2 KB optional (combobox helper)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Total&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;~26–32 KB&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Compare to a React SPA with a JavaScript UI library: typically 150–400 KB before your application code runs. The difference shows directly in Core Web Vitals — First Contentful Paint, Time to Interactive, and Total Blocking Time all improve when there is no script parsing blocking the render path.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Stack That Does Not Fight You
&lt;/h2&gt;

&lt;p&gt;Tailwind CSS handles the visual system. HTMX handles dynamic behavior without JavaScript components. Frutjam handles the component library. Nothing in the stack conflicts with anything else.&lt;/p&gt;

&lt;h2&gt;
  
  
  If you are building a Django, Laravel, FastAPI, or any server-rendered application and want UI components that work on first swap without re-initialization, &lt;a href="https://frutjam.com" rel="noopener noreferrer"&gt;explore Frutjam&lt;/a&gt; — the Tailwind CSS component library designed for this stack.
&lt;/h2&gt;

&lt;h2&gt;
  
  
  FastAPI + Jinja2 Setup
&lt;/h2&gt;

&lt;p&gt;Frutjam works equally well with FastAPI and Jinja2 templates:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# main.py
&lt;/span&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;fastapi&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;FastAPI&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Request&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;fastapi.templating&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Jinja2Templates&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;fastapi.staticfiles&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;StaticFiles&lt;/span&gt;

&lt;span class="n"&gt;app&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;FastAPI&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="n"&gt;app&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/static&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;StaticFiles&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;directory&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;static&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;static&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;templates&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Jinja2Templates&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;directory&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;templates&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="nd"&gt;@app.get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;home&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Request&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;templates&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;TemplateResponse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;index.html&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;request&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;request&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;!-- templates/base.html --&amp;gt;&lt;/span&gt;
&lt;span class="cp"&gt;&amp;lt;!DOCTYPE html&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;html&lt;/span&gt; &lt;span class="na"&gt;data-theme=&lt;/span&gt;&lt;span class="s"&gt;"darkberry"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;head&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;link&lt;/span&gt; &lt;span class="na"&gt;rel=&lt;/span&gt;&lt;span class="s"&gt;"stylesheet"&lt;/span&gt; &lt;span class="na"&gt;href=&lt;/span&gt;&lt;span class="s"&gt;"/static/css/app.css"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;script &lt;/span&gt;&lt;span class="na"&gt;src=&lt;/span&gt;&lt;span class="s"&gt;"https://unpkg.com/htmx.org@2"&lt;/span&gt; &lt;span class="na"&gt;defer&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/script&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/head&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;body&amp;gt;&lt;/span&gt;
  {% block content %}{% endblock %}
&lt;span class="nt"&gt;&amp;lt;/body&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/html&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The same Frutjam components that work in Django templates work identically here. The library is server-framework agnostic — it only cares about the HTML that reaches the browser.&lt;/p&gt;




&lt;h2&gt;
  
  
  HTMX Attribute Quick Reference With Frutjam
&lt;/h2&gt;

&lt;p&gt;These HTMX attributes work on any Frutjam element without configuration:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;HTMX attribute&lt;/th&gt;
&lt;th&gt;Use case with Frutjam&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;hx-get&lt;/code&gt; / &lt;code&gt;hx-post&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Load content into cards, lists, accordion bodies&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;hx-target&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Replace a specific element while leaving the surrounding Frutjam layout intact&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;hx-swap="outerHTML"&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Replace an entire Frutjam card or list item from the server&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;hx-trigger="toggle once"&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Lazy-load accordion content on first open&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;hx-indicator&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Show a Frutjam &lt;code&gt;loading&lt;/code&gt; spinner while a request is in flight&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;hx-boost&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Progressive enhancement on links inside Frutjam menus and navbars&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  Frequently Asked Questions
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Does Frutjam require any JavaScript setup with HTMX?&lt;/strong&gt;&lt;br&gt;
No external JavaScript setup. Install via npm, add two lines to your CSS. Accordions, drawers, and tabs need no JS at all. Modals use inline &lt;code&gt;onclick="id.showModal()"&lt;/code&gt; — plain HTML, so they work on first render and after every HTMX swap without any &lt;code&gt;htmx:afterSwap&lt;/code&gt; listener.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What if I need a component that genuinely requires JavaScript?&lt;/strong&gt;&lt;br&gt;
The combobox is the clearest example: filtering a list as the user types requires JavaScript. Frutjam ships &lt;code&gt;createCombobox&lt;/code&gt; from &lt;code&gt;frutjam/js&lt;/code&gt; — a built-in helper that handles filtering, keyboard navigation, and &lt;code&gt;aria-*&lt;/code&gt; attributes. You do not write it yourself. With HTMX, you can skip client-side filtering entirely and let the server return filtered results. Accordions, drawers, and tabs are pure CSS. Modals use one line of native browser JS (&lt;code&gt;showModal()&lt;/code&gt;) with no library or initialization lifecycle.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Can I use Frutjam with htmx.org's &lt;code&gt;hx-boost&lt;/code&gt;?&lt;/strong&gt;&lt;br&gt;
Yes. &lt;code&gt;hx-boost&lt;/code&gt; replaces page navigation with HTMX partial swaps. Frutjam components in the swapped content work correctly because there is nothing to re-initialize — the CSS is already loaded globally.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Does Frutjam work with Django REST Framework (DRF)?&lt;/strong&gt;&lt;br&gt;
Yes. DRF's &lt;code&gt;TemplateHTMLRenderer&lt;/code&gt; returns full HTML responses. Frutjam components in those templates work identically to any other Django template.&lt;/p&gt;

</description>
      <category>css</category>
      <category>frontend</category>
      <category>htmx</category>
      <category>javascript</category>
    </item>
    <item>
      <title>EasyMDE silently breaks Django forms (and what I use instead)</title>
      <dc:creator>Mohammed Rashad</dc:creator>
      <pubDate>Mon, 28 Sep 2026 13:00:00 +0000</pubDate>
      <link>https://dev.to/rashadmehtab/easymde-silently-breaks-django-forms-and-what-i-use-instead-ne8</link>
      <guid>https://dev.to/rashadmehtab/easymde-silently-breaks-django-forms-and-what-i-use-instead-ne8</guid>
      <description>&lt;p&gt;Add a markdown editor to a Django form the usual way and it looks fine — until the form silently refuses to submit:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# forms.py — Django adds required=True to every non-blank field
&lt;/span&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;PostForm&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;forms&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ModelForm&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Meta&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;model&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Post&lt;/span&gt;
        &lt;span class="n"&gt;fields&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;title&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;content&lt;/span&gt;&lt;span class="sh"&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 javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// The editor everyone reaches for first&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;easyMDE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;EasyMDE&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;element&lt;/span&gt;&lt;span class="p"&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;getElementById&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_content&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Click Save and nothing happens. No validation error, no request, no clue on the&lt;br&gt;
page. The console says:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;An invalid form control with name='content' is not focusable.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;EasyMDE hides your &lt;code&gt;&amp;lt;textarea&amp;gt;&lt;/code&gt; with &lt;code&gt;display: none&lt;/code&gt; and edits a copy. The&lt;br&gt;
hidden field is still &lt;code&gt;required&lt;/code&gt;, so the browser refuses to submit a form it&lt;br&gt;
cannot focus the invalid field in — and because the submit event never fires,&lt;br&gt;
the editor never copies its content back. The user's writing goes nowhere.&lt;/p&gt;

&lt;p&gt;To be fair to EasyMDE: on a form &lt;em&gt;without&lt;/em&gt; &lt;code&gt;required&lt;/code&gt;, it does sync on submit by&lt;br&gt;
itself, so plain forms work. The trouble is that Django marks fields required by&lt;br&gt;
default, and that the value only exists in the textarea &lt;em&gt;during&lt;/em&gt; submit:&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="c1"&gt;// Any code that reads the field before submit gets an empty string&lt;/span&gt;
&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;FormData&lt;/span&gt;&lt;span class="p"&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;form&lt;/span&gt;&lt;span class="dl"&gt;'&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;content&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;   &lt;span class="c1"&gt;// ""&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That breaks anything that serialises the form itself — htmx, Turbo, Alpine,&lt;br&gt;
autosave, an "unsaved changes" guard, a character counter.&lt;/p&gt;
&lt;h2&gt;
  
  
  A better approach
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://frutjam.com/plugins/markdown-editor" rel="noopener noreferrer"&gt;markdown-text-editor&lt;/a&gt; styles the&lt;br&gt;
textarea you already have and leaves it as the field you type into. It is never&lt;br&gt;
hidden and never copied, so its value is correct at every moment — not just&lt;br&gt;
during submit.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;required&lt;/code&gt; works because the field is visible. &lt;code&gt;FormData&lt;/code&gt; works because the&lt;br&gt;
value is live. And &lt;code&gt;request.POST&lt;/code&gt;, form validation, &lt;code&gt;ModelForm.save()&lt;/code&gt;, CSRF and&lt;br&gt;
pre-filling on edit all behave exactly as they do with a plain textarea.&lt;/p&gt;
&lt;h2&gt;
  
  
  Setup
&lt;/h2&gt;

&lt;p&gt;Install via npm (if you use a JS bundler):&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;markdown-text-editor
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Or use the CDN directly in your template — no build step needed:&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;src=&lt;/span&gt;&lt;span class="s"&gt;"https://cdn.jsdelivr.net/npm/markdown-text-editor"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/script&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Django form
&lt;/h2&gt;

&lt;p&gt;Nothing changes in your form class:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# forms.py
&lt;/span&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;django&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;forms&lt;/span&gt;

&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;PostForm&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;forms&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ModelForm&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Meta&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;model&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Post&lt;/span&gt;
        &lt;span class="n"&gt;fields&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;title&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;content&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
        &lt;span class="n"&gt;widgets&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;content&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;forms&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Textarea&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;attrs&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;class&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;markdown-editor&lt;/span&gt;&lt;span class="sh"&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;h2&gt;
  
  
  Template
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;{% extends "base.html" %}

{% block content %}
&lt;span class="nt"&gt;&amp;lt;form&lt;/span&gt; &lt;span class="na"&gt;method=&lt;/span&gt;&lt;span class="s"&gt;"post"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    {% csrf_token %}
    {{ form.as_p }}
    &lt;span class="nt"&gt;&amp;lt;button&lt;/span&gt; &lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"submit"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Save&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;span class="nt"&gt;&amp;lt;script &lt;/span&gt;&lt;span class="na"&gt;src=&lt;/span&gt;&lt;span class="s"&gt;"https://cdn.jsdelivr.net/npm/markdown-text-editor"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/script&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;script&amp;gt;&lt;/span&gt;
    &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;MarkdownEditor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;.markdown-editor&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;
{% endblock %}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's it. Submit the form — &lt;code&gt;request.POST['content']&lt;/code&gt; contains the markdown exactly as typed. No hooks, no sync, no custom extraction.&lt;/p&gt;

&lt;h2&gt;
  
  
  View
&lt;/h2&gt;

&lt;p&gt;Your view doesn't change at all:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# views.py
&lt;/span&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;create_post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;method&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;POST&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;form&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;PostForm&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;POST&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;form&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;is_valid&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
            &lt;span class="n"&gt;form&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="c1"&gt;# content is already there, untouched
&lt;/span&gt;            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;redirect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;post-list&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;else&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;form&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;PostForm&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;render&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;create_post.html&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;form&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;form&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Editing existing content
&lt;/h2&gt;

&lt;p&gt;When editing an existing post, Django pre-fills the textarea via the form — and the editor picks up whatever value is already there:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;edit_post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;pk&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;post&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;get_object_or_404&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Post&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;pk&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;pk&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;method&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;POST&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;form&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;PostForm&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;POST&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;instance&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;post&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;form&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;is_valid&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
            &lt;span class="n"&gt;form&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="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;redirect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;post-detail&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;pk&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;pk&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;else&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;form&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;PostForm&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;instance&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;post&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;# textarea pre-filled, editor inherits it
&lt;/span&gt;    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;render&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;edit_post.html&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;form&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;form&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Validation errors keep the user's text
&lt;/h2&gt;

&lt;p&gt;Server-side validation is the other half of the story. When a form fails&lt;br&gt;
validation, Django re-renders the page with the submitted data bound to the&lt;br&gt;
form. Because the markdown lives in the textarea, it comes back with it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;create_post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;form&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;PostForm&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;POST&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;method&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;POST&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;form&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;is_valid&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
        &lt;span class="n"&gt;form&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="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;redirect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;post-list&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="c1"&gt;# Invalid: the same template re-renders with the user's markdown intact
&lt;/span&gt;    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;render&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;create_post.html&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;form&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;form&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The reader types 800 words, forgets the title, and gets the title error back with every word still in place.&lt;/p&gt;

&lt;h2&gt;
  
  
  Optional: auto-save draft
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;textarea&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;.markdown-editor&lt;/span&gt;&lt;span class="dl"&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;saved&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;localStorage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getItem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;post-draft&lt;/span&gt;&lt;span class="dl"&gt;'&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;saved&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;textarea&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="nx"&gt;textarea&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="nx"&gt;saved&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;MarkdownEditor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;.markdown-editor&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;onChange&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="nx"&gt;localStorage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setItem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;post-draft&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="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;h2&gt;
  
  
  Features you get out of the box
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;WYSIWYG hybrid mode&lt;/strong&gt; — renders bold, italic and headings live while keeping raw Markdown underneath&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Live preview&lt;/strong&gt; — side-by-side preview panel&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Find &amp;amp; Replace&lt;/strong&gt; — &lt;code&gt;Ctrl+F&lt;/code&gt; / &lt;code&gt;Ctrl+H&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Keyboard shortcuts&lt;/strong&gt; — &lt;code&gt;Ctrl+B&lt;/code&gt;, &lt;code&gt;Ctrl+I&lt;/code&gt;, &lt;code&gt;Ctrl+K&lt;/code&gt;, headings via &lt;code&gt;Ctrl+1&lt;/code&gt;–&lt;code&gt;Ctrl+3&lt;/code&gt;, lists via &lt;code&gt;Ctrl+L&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;RTL support&lt;/strong&gt; — Arabic, Urdu and Farsi work out of the box&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Dark mode&lt;/strong&gt; — add &lt;code&gt;data-theme="dark"&lt;/code&gt; to any ancestor element&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;XSS safe&lt;/strong&gt; — preview sanitized with DOMPurify&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;CSP compatible&lt;/strong&gt; — no inline event handlers&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;51 KB gzipped&lt;/strong&gt; (245 KB minified, CSS included) — EasyMDE is 107 KB gzipped across its JS and CSS&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Rendering markdown in templates
&lt;/h2&gt;

&lt;p&gt;To display saved markdown in your templates, use the &lt;code&gt;markdown&lt;/code&gt; library:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pip &lt;span class="nb"&gt;install &lt;/span&gt;markdown bleach
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# models.py
&lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;markdown&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;bleach&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;django.db&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;models&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;django.utils.safestring&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;mark_safe&lt;/span&gt;

&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;models&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Model&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;content&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;models&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;TextField&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;rendered_content&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;html&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;markdown&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;markdown&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;extensions&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;fenced_code&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;tables&lt;/span&gt;&lt;span class="sh"&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;mark_safe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;bleach&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;clean&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;html&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;tags&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;bleach&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;sanitizer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ALLOWED_TAGS&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;p&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;h1&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;h2&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;h3&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;pre&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;code&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;table&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;thead&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;tbody&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;tr&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;th&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;td&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
            &lt;span class="n"&gt;strip&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&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;!-- detail template --&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;article&amp;gt;&lt;/span&gt;
    {{ post.rendered_content }}
&lt;span class="nt"&gt;&amp;lt;/article&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Sanitize on render, not on save. Storing the raw markdown means you can change how it is rendered later — switch extensions, allow a new tag, fix an escaping bug — without a data migration.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why the textarea matters
&lt;/h2&gt;

&lt;p&gt;An editor that hides the textarea and edits a copy has to guess when to put the&lt;br&gt;
content back. CodeMirror guesses "on submit", which is a good guess and right&lt;br&gt;
most of the time — until the browser refuses to submit, or something reads the&lt;br&gt;
form before that moment. Every one of those failures is silent, because from the&lt;br&gt;
outside the field looks fine.&lt;/p&gt;

&lt;p&gt;An editor that leaves the textarea in place has nothing to guess about. The&lt;br&gt;
browser owns the value, Django reads what the browser sent, and nothing in&lt;br&gt;
between has an opinion.&lt;/p&gt;

&lt;p&gt;That is the whole design, and it is why the Django integration in this post is&lt;br&gt;
three lines long.&lt;/p&gt;

&lt;h2&gt;
  
  
  Starting from scratch?
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://github.com/nezanuha/django-frutjam-starter" rel="noopener noreferrer"&gt;django-frutjam-starter&lt;/a&gt; is a&lt;br&gt;
working Django 6 project with all of this already wired up — accounts, a&lt;br&gt;
dashboard, and this exact markdown editor on a notes form. Clone it, run two&lt;br&gt;
commands, and you have somewhere to start.&lt;/p&gt;




&lt;p&gt;Full docs, theming guide, and image upload configuration: &lt;a href="https://frutjam.com/plugins/markdown-editor" rel="noopener noreferrer"&gt;markdown-text-editor on Frutjam&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Source on &lt;a href="https://github.com/nezanuha/markdown-text-editor" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;, package on &lt;a href="https://www.npmjs.com/package/markdown-text-editor" rel="noopener noreferrer"&gt;npm&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>django</category>
      <category>python</category>
      <category>webdev</category>
      <category>javascript</category>
    </item>
    <item>
      <title>DaisyUI vs Frutjam: an honest comparison (I wrote one of them)</title>
      <dc:creator>Mohammed Rashad</dc:creator>
      <pubDate>Wed, 23 Sep 2026 13:00:00 +0000</pubDate>
      <link>https://dev.to/rashadmehtab/daisyui-vs-frutjam-an-honest-comparison-i-wrote-one-of-them-6bh</link>
      <guid>https://dev.to/rashadmehtab/daisyui-vs-frutjam-an-honest-comparison-i-wrote-one-of-them-6bh</guid>
      <description>&lt;p&gt;Your components look right. Then the accessibility audit runs, and half the buttons fail contrast on the dark theme.&lt;/p&gt;

&lt;p&gt;That is where I ended up after shipping a few multilingual projects on Tailwind CSS: patching theme variables one hex at a time, writing RTL overrides by hand, and fighting modal positioning that only offered three placements. Eventually I stopped patching and built the library I wanted instead.&lt;/p&gt;

&lt;p&gt;This is the honest comparison I wish I'd had before starting: what DaisyUI does well, the four places it left me stuck, and what &lt;strong&gt;Frutjam&lt;/strong&gt; — 65+ CSS-only components, WCAG AA/AAA on every theme, PageSpeed 100 — does differently.&lt;/p&gt;




&lt;h2&gt;
  
  
  DaisyUI v5: What Changed, What Didn't
&lt;/h2&gt;

&lt;p&gt;DaisyUI v5 is a meaningful upgrade that corrects many of the criticisms aimed at earlier versions. Interactive components no longer require a separate JavaScript plugin:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Modal&lt;/strong&gt;: native &lt;code&gt;&amp;lt;dialog&amp;gt;&lt;/code&gt; + &lt;code&gt;showModal()&lt;/code&gt; or the Popover API — no JS bundle&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Drawer&lt;/strong&gt;: checkbox toggle + CSS — zero JavaScript&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Dropdown&lt;/strong&gt;: native Popover API, &lt;code&gt;&amp;lt;details&amp;gt;&lt;/code&gt;, or focus-based CSS — zero JavaScript&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Accordion&lt;/strong&gt;: radio inputs or &lt;code&gt;&amp;lt;details&amp;gt;&lt;/code&gt; / &lt;code&gt;&amp;lt;summary&amp;gt;&lt;/code&gt; — zero JavaScript&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you have read comparisons claiming DaisyUI v5 requires a JavaScript plugin for interactive components, those comparisons describe DaisyUI v4. Version 5 is largely CSS-based.&lt;/p&gt;

&lt;p&gt;What DaisyUI v5 still does not address: WCAG contrast guarantees across its 25+ themes, depth of the modal positioning system, and the absence of a combobox component.&lt;/p&gt;




&lt;h2&gt;
  
  
  Where Frutjam Is a Better Fit
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. WCAG AA/AAA Contrast on Every Theme
&lt;/h3&gt;

&lt;p&gt;Frutjam ships a Berry theming system — six named themes built on OKLCH color values, each hand-tuned to pass WCAG AA and AAA contrast requirements:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Theme&lt;/th&gt;
&lt;th&gt;Mode&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Snowberry&lt;/td&gt;
&lt;td&gt;Light (default)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Darkberry&lt;/td&gt;
&lt;td&gt;Dark&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Blueberry&lt;/td&gt;
&lt;td&gt;Light (blue accent)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Strawberry&lt;/td&gt;
&lt;td&gt;Light (red accent)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Grapeberry&lt;/td&gt;
&lt;td&gt;Light (purple accent)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Peachberry&lt;/td&gt;
&lt;td&gt;Light (warm accent)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Apply a theme with one HTML attribute:&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;html&lt;/span&gt; &lt;span class="na"&gt;data-theme=&lt;/span&gt;&lt;span class="s"&gt;"darkberry"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Themes cascade via CSS variables and can be nested — a sidebar can run a dark theme while the rest of the page stays light:&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;html&lt;/span&gt; &lt;span class="na"&gt;data-theme=&lt;/span&gt;&lt;span class="s"&gt;"snowberry"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;aside&lt;/span&gt; &lt;span class="na"&gt;data-theme=&lt;/span&gt;&lt;span class="s"&gt;"darkberry"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="c"&gt;&amp;lt;!-- dark sidebar --&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/aside&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/html&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;DaisyUI ships 25+ themes. Frutjam ships fewer, but every color pair in every theme — text on background, label on input, badge on card — passes WCAG contrast checks without configuration. No contrast surprises at audit time.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Modal System: 9 Positions, 4 Animations, LTR/RTL Aware
&lt;/h3&gt;

&lt;p&gt;Both Frutjam and DaisyUI use the native &lt;code&gt;&amp;lt;dialog&amp;gt;&lt;/code&gt; element with &lt;code&gt;showModal()&lt;/code&gt;. Where they diverge is what the library provides around it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;DaisyUI&lt;/strong&gt; offers three vertical positions (&lt;code&gt;modal-top&lt;/code&gt;, &lt;code&gt;modal-middle&lt;/code&gt;, &lt;code&gt;modal-bottom&lt;/code&gt;) and no entrance animations.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Frutjam&lt;/strong&gt; provides a full placement and animation system:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Axis&lt;/th&gt;
&lt;th&gt;Classes&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Vertical&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;modal-top&lt;/code&gt;, &lt;code&gt;modal-middle&lt;/code&gt;, &lt;code&gt;modal-bottom&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Horizontal&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;modal-start&lt;/code&gt;, &lt;code&gt;modal-center&lt;/code&gt;, &lt;code&gt;modal-end&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Animations&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;modal-slide-up&lt;/code&gt;, &lt;code&gt;modal-slide-down&lt;/code&gt;, &lt;code&gt;modal-slide-start&lt;/code&gt;, &lt;code&gt;modal-slide-end&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;LTR / RTL&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;ltr:modal-end&lt;/code&gt;, &lt;code&gt;rtl:modal-start&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Responsive&lt;/td&gt;
&lt;td&gt;&lt;code&gt;modal-top lg:modal-middle&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Nine placement combinations. Four slide animations. Fully responsive via Tailwind breakpoint prefixes. LTR/RTL aware for multilingual apps.&lt;/p&gt;

&lt;p&gt;A notification toast snapping to the top-right:&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;dialog&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"modal modal-end modal-top modal-slide-end"&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"notificationModal"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"modal-content"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;p&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"para"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Your changes were saved.&lt;span class="nt"&gt;&amp;lt;/p&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;button&lt;/span&gt; &lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"button"&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"btn btn-xs"&lt;/span&gt; &lt;span class="na"&gt;onclick=&lt;/span&gt;&lt;span class="s"&gt;"notificationModal.close()"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Dismiss&lt;span class="nt"&gt;&amp;lt;/button&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/dialog&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;button&lt;/span&gt; &lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"button"&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"btn"&lt;/span&gt; &lt;span class="na"&gt;onclick=&lt;/span&gt;&lt;span class="s"&gt;"notificationModal.showModal()"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Show&lt;span class="nt"&gt;&amp;lt;/button&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A sheet that slides up from the bottom on mobile, centered on desktop:&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;dialog&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"modal modal-center modal-bottom modal-slide-up lg:modal-center lg:modal-middle"&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"sheetModal"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"modal-content"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;p&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"para"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Responsive sheet&lt;span class="nt"&gt;&amp;lt;/p&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;button&lt;/span&gt; &lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"button"&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"modal-backdrop"&lt;/span&gt; &lt;span class="na"&gt;onclick=&lt;/span&gt;&lt;span class="s"&gt;"sheetModal.close()"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Close&lt;span class="nt"&gt;&amp;lt;/button&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/dialog&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  3. Popover: 12 Positions, Click and Hover
&lt;/h3&gt;

&lt;p&gt;Frutjam's popover uses the native Popover API — zero JavaScript for click-triggered dropdowns. Twelve position classes cover every anchor alignment:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;popover-top-start    popover-top-center    popover-top-end
popover-bottom-start popover-bottom-center popover-bottom-end
popover-start-top    popover-start-center  popover-start-bottom
popover-end-top      popover-end-center    popover-end-bottom
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Click trigger&lt;/strong&gt; (native Popover API, zero JS):&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;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"popover popover-bottom-start"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;button&lt;/span&gt; &lt;span class="na"&gt;popovertarget=&lt;/span&gt;&lt;span class="s"&gt;"userMenu"&lt;/span&gt; &lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"button"&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"popover-toggle btn"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Account&lt;span class="nt"&gt;&amp;lt;/button&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;tabindex=&lt;/span&gt;&lt;span class="s"&gt;"0"&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"popover-content"&lt;/span&gt; &lt;span class="na"&gt;popover&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"userMenu"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;ul&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"menu"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;li&amp;gt;&amp;lt;a&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"menu-item"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Profile&lt;span class="nt"&gt;&amp;lt;/a&amp;gt;&amp;lt;/li&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;li&amp;gt;&amp;lt;a&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"menu-item"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Settings&lt;span class="nt"&gt;&amp;lt;/a&amp;gt;&amp;lt;/li&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;li&amp;gt;&amp;lt;a&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"menu-item"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Sign out&lt;span class="nt"&gt;&amp;lt;/a&amp;gt;&amp;lt;/li&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/ul&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Hover trigger&lt;/strong&gt; (add &lt;code&gt;popover-hover&lt;/code&gt;, remove &lt;code&gt;popovertarget&lt;/code&gt; and &lt;code&gt;popover&lt;/code&gt; attributes):&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;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"popover popover-hover popover-bottom-start"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;button&lt;/span&gt; &lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"button"&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"popover-toggle btn"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Hover me&lt;span class="nt"&gt;&amp;lt;/button&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"popover-content"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;ul&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"menu"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;li&amp;gt;&amp;lt;a&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"menu-item"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;About Us&lt;span class="nt"&gt;&amp;lt;/a&amp;gt;&amp;lt;/li&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;li&amp;gt;&amp;lt;a&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"menu-item"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Contact&lt;span class="nt"&gt;&amp;lt;/a&amp;gt;&amp;lt;/li&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/ul&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  4. Drawer: 4 Positions Built on &lt;code&gt;&amp;lt;dialog&amp;gt;&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;Frutjam's drawer is built on the native &lt;code&gt;&amp;lt;dialog&amp;gt;&lt;/code&gt; element using the non-modal &lt;code&gt;.show()&lt;/code&gt; method — so the rest of the page stays interactive while the drawer is open. Four positions ship out of the box:&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="c"&gt;&amp;lt;!-- Slide from left --&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;dialog&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"drawer drawer-start"&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"sideNav"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"drawer-content w-64 p-4"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;ul&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"menu"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;li&amp;gt;&amp;lt;a&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"menu-item"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Dashboard&lt;span class="nt"&gt;&amp;lt;/a&amp;gt;&amp;lt;/li&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;li&amp;gt;&amp;lt;a&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"menu-item"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Settings&lt;span class="nt"&gt;&amp;lt;/a&amp;gt;&amp;lt;/li&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/ul&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;button&lt;/span&gt; &lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"button"&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"drawer-backdrop"&lt;/span&gt; &lt;span class="na"&gt;onclick=&lt;/span&gt;&lt;span class="s"&gt;"sideNav.close()"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Close&lt;span class="nt"&gt;&amp;lt;/button&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/dialog&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;button&lt;/span&gt; &lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"button"&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"btn"&lt;/span&gt; &lt;span class="na"&gt;onclick=&lt;/span&gt;&lt;span class="s"&gt;"sideNav.show()"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Open Menu&lt;span class="nt"&gt;&amp;lt;/button&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Open with &lt;code&gt;onclick="id.show()"&lt;/code&gt;, close with &lt;code&gt;onclick="id.close()"&lt;/code&gt; — one line of inline JS per trigger, no library, no initialization lifecycle. For programmatic control from anywhere in your code, &lt;code&gt;createDrawer&lt;/code&gt; from &lt;code&gt;frutjam/js&lt;/code&gt; handles it.&lt;/p&gt;

&lt;h3&gt;
  
  
  5. Combobox: Built-In Where DaisyUI Has Nothing
&lt;/h3&gt;

&lt;p&gt;The browser has no native combobox element. DaisyUI has no combobox component — if you need searchable select with keyboard navigation, you write it from scratch.&lt;/p&gt;

&lt;p&gt;Frutjam ships a complete combobox. CSS-only for open/close, built-in JS helper for filtering and keyboard navigation:&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="c"&gt;&amp;lt;!-- Opens on focus — zero JavaScript --&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"relative w-64 focus-within:combobox-open"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;input&lt;/span&gt; &lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"text"&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"input w-full"&lt;/span&gt; &lt;span class="na"&gt;placeholder=&lt;/span&gt;&lt;span class="s"&gt;"Search..."&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;ul&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"combobox-list"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;li&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"combobox-item"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;React&lt;span class="nt"&gt;&amp;lt;/li&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;li&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"combobox-item"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Vue&lt;span class="nt"&gt;&amp;lt;/li&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;li&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"combobox-item"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Django&lt;span class="nt"&gt;&amp;lt;/li&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;li&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"combobox-item"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Laravel&lt;span class="nt"&gt;&amp;lt;/li&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/ul&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Add live filtering and keyboard navigation (↑ ↓ Enter Escape) with the built-in helper:&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;createCombobox&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;frutjam/js&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;span class="nf"&gt;createCombobox&lt;/span&gt;&lt;span class="p"&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;.my-combobox&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;p&gt;&lt;code&gt;createCombobox&lt;/code&gt; also wires up all required &lt;code&gt;aria-*&lt;/code&gt; attributes automatically. You do not write filtering logic, keyboard handlers, or ARIA patterns — the library ships them.&lt;/p&gt;

&lt;h3&gt;
  
  
  6. Tailwind v4 Native
&lt;/h3&gt;

&lt;p&gt;Frutjam is designed around Tailwind v4 — the v4 import system, CSS-first configuration, and OKLCH color tokens. Install it and add two lines:&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;frutjam
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="k"&gt;@import&lt;/span&gt; &lt;span class="s1"&gt;"tailwindcss"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;@import&lt;/span&gt; &lt;span class="s1"&gt;"frutjam"&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;html&lt;/span&gt; &lt;span class="na"&gt;data-theme=&lt;/span&gt;&lt;span class="s"&gt;"snowberry"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every component is available immediately. No script tags, no initialization, no build step beyond Tailwind itself.&lt;/p&gt;




&lt;h2&gt;
  
  
  Component Coverage Comparison
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Component&lt;/th&gt;
&lt;th&gt;Frutjam&lt;/th&gt;
&lt;th&gt;DaisyUI v5&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Total components&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;53&lt;/td&gt;
&lt;td&gt;80&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Built-in themes&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;6 named Berry themes&lt;/td&gt;
&lt;td&gt;25+&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;WCAG AA/AAA guaranteed&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Yes — all themes&lt;/td&gt;
&lt;td&gt;Not guaranteed&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Button, Badge, Alert&lt;/td&gt;
&lt;td&gt;CSS-only&lt;/td&gt;
&lt;td&gt;CSS-only&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Accordion&lt;/td&gt;
&lt;td&gt;CSS-only (&lt;code&gt;&amp;lt;details&amp;gt;&lt;/code&gt; / &lt;code&gt;&amp;lt;summary&amp;gt;&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;CSS-only (radio / &lt;code&gt;&amp;lt;details&amp;gt;&lt;/code&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Modal&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;&amp;lt;dialog&amp;gt;&lt;/code&gt; + &lt;code&gt;showModal()&lt;/code&gt; — 9 positions, 4 animations, LTR/RTL&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;&amp;lt;dialog&amp;gt;&lt;/code&gt; + &lt;code&gt;showModal()&lt;/code&gt; or Popover API — 3 positions&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Drawer&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;&amp;lt;dialog&amp;gt;&lt;/code&gt; + &lt;code&gt;show()&lt;/code&gt; — 4 positions&lt;/td&gt;
&lt;td&gt;Checkbox toggle — CSS-only&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Popover / Dropdown&lt;/td&gt;
&lt;td&gt;Native Popover API — 12 positions, click or hover&lt;/td&gt;
&lt;td&gt;Native Popover API / &lt;code&gt;&amp;lt;details&amp;gt;&lt;/code&gt; / focus-based CSS&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Combobox&lt;/td&gt;
&lt;td&gt;CSS-only open/close + &lt;code&gt;createCombobox()&lt;/code&gt; helper&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;No combobox component&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;RTL support&lt;/td&gt;
&lt;td&gt;Yes — &lt;code&gt;ltr:&lt;/code&gt; / &lt;code&gt;rtl:&lt;/code&gt; prefixes on modal, drawer&lt;/td&gt;
&lt;td&gt;Partial&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;PageSpeed 100/100&lt;/td&gt;
&lt;td&gt;Yes — zero JS overhead, zero layout shift&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Framework-agnostic&lt;/td&gt;
&lt;td&gt;Django, HTMX, Laravel, React, Vue, any stack&lt;/td&gt;
&lt;td&gt;Django, HTMX, Laravel, React, Vue, any stack&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  Side-by-Side: Modal
&lt;/h2&gt;

&lt;p&gt;The same trigger pattern, different positioning depth.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;DaisyUI&lt;/strong&gt; — three positions, no animations, closes via form:&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;button&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"btn"&lt;/span&gt; &lt;span class="na"&gt;onclick=&lt;/span&gt;&lt;span class="s"&gt;"my_modal.showModal()"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Open Modal&lt;span class="nt"&gt;&amp;lt;/button&amp;gt;&lt;/span&gt;

&lt;span class="nt"&gt;&amp;lt;dialog&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"my_modal"&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"modal"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"modal-box"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;h3&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"font-bold text-lg"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Confirm action&lt;span class="nt"&gt;&amp;lt;/h3&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;p&amp;gt;&lt;/span&gt;This action cannot be undone.&lt;span class="nt"&gt;&amp;lt;/p&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"modal-action"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;form&lt;/span&gt; &lt;span class="na"&gt;method=&lt;/span&gt;&lt;span class="s"&gt;"dialog"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;button&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"btn"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Close&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;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;form&lt;/span&gt; &lt;span class="na"&gt;method=&lt;/span&gt;&lt;span class="s"&gt;"dialog"&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"modal-backdrop"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;button&amp;gt;&lt;/span&gt;close&lt;span class="nt"&gt;&amp;lt;/button&amp;gt;&amp;lt;/form&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/dialog&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Frutjam&lt;/strong&gt; — 9 positions, 4 animations, LTR/RTL, closes via &lt;code&gt;onclick&lt;/code&gt;:&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;button&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"btn"&lt;/span&gt; &lt;span class="na"&gt;onclick=&lt;/span&gt;&lt;span class="s"&gt;"myModal.showModal()"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Open Modal&lt;span class="nt"&gt;&amp;lt;/button&amp;gt;&lt;/span&gt;

&lt;span class="nt"&gt;&amp;lt;dialog&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"modal modal-center modal-middle modal-slide-up"&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"myModal"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"modal-content"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;h3&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"heading-lg"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Confirm action&lt;span class="nt"&gt;&amp;lt;/h3&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;p&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"para"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;This action cannot be undone.&lt;span class="nt"&gt;&amp;lt;/p&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"flex gap-2 justify-end mt-4"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;button&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"btn"&lt;/span&gt; &lt;span class="na"&gt;onclick=&lt;/span&gt;&lt;span class="s"&gt;"myModal.close()"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Cancel&lt;span class="nt"&gt;&amp;lt;/button&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;button&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"btn btn-error"&lt;/span&gt; &lt;span class="na"&gt;onclick=&lt;/span&gt;&lt;span class="s"&gt;"myModal.close()"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Delete&lt;span class="nt"&gt;&amp;lt;/button&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;button&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"modal-backdrop"&lt;/span&gt; &lt;span class="na"&gt;onclick=&lt;/span&gt;&lt;span class="s"&gt;"myModal.close()"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Close&lt;span class="nt"&gt;&amp;lt;/button&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/dialog&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Both use &lt;code&gt;showModal()&lt;/code&gt; — the difference is the placement and animation system. Frutjam's &lt;code&gt;modal-slide-up&lt;/code&gt; entrance animation is pure CSS. Changing to a top-right notification panel is one class change: &lt;code&gt;modal-end modal-top modal-slide-end&lt;/code&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  Migration from DaisyUI
&lt;/h2&gt;

&lt;p&gt;Swap the import in your CSS entry point:&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="c"&gt;/* Before */&lt;/span&gt;
&lt;span class="k"&gt;@plugin&lt;/span&gt; &lt;span class="s1"&gt;"daisyui"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c"&gt;/* After */&lt;/span&gt;
&lt;span class="k"&gt;@import&lt;/span&gt; &lt;span class="s1"&gt;"frutjam"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Most component class names carry over directly:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;DaisyUI&lt;/th&gt;
&lt;th&gt;Frutjam&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;btn&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;btn&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;badge&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;badge&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;card&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;card&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;alert&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;alert&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;modal&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;modal&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;drawer&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;drawer&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;navbar&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;navbar&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;menu&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;menu&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;accordion&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;accordion&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;tabs&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;tabs&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Modals use the same &lt;code&gt;showModal()&lt;/code&gt; / &lt;code&gt;close()&lt;/code&gt; pattern. Drawers switch from checkbox toggle to &lt;code&gt;dialog.show()&lt;/code&gt; / &lt;code&gt;dialog.close()&lt;/code&gt; — add an &lt;code&gt;id&lt;/code&gt; attribute and update the trigger &lt;code&gt;onclick&lt;/code&gt;. The &lt;a href="https://frutjam.com/components" rel="noopener noreferrer"&gt;Frutjam component docs&lt;/a&gt; show the exact HTML pattern for each component.&lt;/p&gt;




&lt;h2&gt;
  
  
  Frutjam Cherry: AI-Assisted Development Without Hallucinations
&lt;/h2&gt;

&lt;p&gt;One practical challenge with AI coding assistants and component libraries is accuracy. When you ask Claude, Cursor, or Copilot to build a UI with Frutjam components, the model may hallucinate class names or generate outdated syntax learned during training.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Frutjam Cherry&lt;/strong&gt; is a free MCP (Model Context Protocol) server that connects your AI assistant directly to Frutjam's live component documentation. Instead of guessing, the AI fetches exact class names, component structures, and working examples — on demand, always in sync with the latest Frutjam version.&lt;/p&gt;

&lt;p&gt;Supported editors: Claude Code, Cursor, VS Code, Windsurf, Zed, Cline, and any MCP-compatible environment. Setup takes under a minute.&lt;/p&gt;

&lt;p&gt;Cherry ships five built-in AI slash commands:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Command&lt;/th&gt;
&lt;th&gt;What it does&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;/figma_to_frutjam&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Converts a Figma design description to Frutjam components&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;/image_to_frutjam&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Converts a UI screenshot to Frutjam markup&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;/tailwind_to_frutjam&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Migrates raw Tailwind markup to Frutjam components&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;/bootstrap_to_frutjam&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Migrates Bootstrap markup to Frutjam components&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;/generate_theme&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Generates a custom Berry theme from your design tokens&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Cherry is token-efficient — it fetches specs only when needed rather than dumping the full documentation into context. A free tier is available with no account required. Pro and Team tiers unlock unlimited requests, premium blocks, and all slash commands.&lt;/p&gt;

&lt;p&gt;DaisyUI has no equivalent MCP integration. If AI-assisted development is part of your workflow, Cherry is a meaningful differentiator.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://frutjam.com/products/cherry" rel="noopener noreferrer"&gt;Get Frutjam Cherry&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Frequently Asked Questions
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Does Frutjam require any JavaScript?&lt;/strong&gt;&lt;br&gt;
No JavaScript required for core components. Accordion, tabs, popover, swap, and carousel are pure CSS. Modal and drawer use one line of inline &lt;code&gt;onclick&lt;/code&gt; — native browser JS, no library. Combobox ships a &lt;code&gt;createCombobox()&lt;/code&gt; helper for filtering and keyboard navigation.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Does DaisyUI v5 require a JavaScript plugin for interactive components?&lt;/strong&gt;&lt;br&gt;
No — DaisyUI v5 moved to CSS-based approaches. Drawer uses checkbox toggle, dropdown uses the Popover API, accordion uses &lt;code&gt;&amp;lt;details&amp;gt;&lt;/code&gt;. Comparisons describing a required JS bundle apply to DaisyUI v4, not v5.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Will HTMX break Frutjam components?&lt;/strong&gt;&lt;br&gt;
No. CSS-only components (accordion, tabs, swap) need no re-initialization. Modal and drawer use inline &lt;code&gt;onclick="id.showModal()"&lt;/code&gt; and &lt;code&gt;onclick="id.show()"&lt;/code&gt; — plain HTML attributes that the browser evaluates on every render, including after HTMX swaps.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Can I use a custom theme?&lt;/strong&gt;&lt;br&gt;
Yes. Define CSS variables in a &lt;code&gt;[data-theme="yourtheme"]&lt;/code&gt; block using OKLCH color values. Apply it with &lt;code&gt;data-theme="yourtheme"&lt;/code&gt; on any element. Themes can be scoped — a single page can have multiple themes nested in different sections.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How does Frutjam handle RTL languages?&lt;/strong&gt;&lt;br&gt;
RTL is built into the layout system. Use &lt;code&gt;ltr:modal-end rtl:modal-start&lt;/code&gt; on the modal to position it correctly in both directions. Drawer, popover, and other directional components follow the same &lt;code&gt;ltr:&lt;/code&gt; / &lt;code&gt;rtl:&lt;/code&gt; prefix pattern.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Bottom Line
&lt;/h2&gt;

&lt;p&gt;DaisyUI v5 is a capable, well-maintained library. If you need 80 components and 25+ themes and the community ecosystem matters, it is a strong choice.&lt;/p&gt;

&lt;p&gt;Frutjam is the better fit if you need WCAG-compliant themes out of the box, a modal system with 9 placement positions and 4 animations, a popover with 12 positions, a built-in combobox, and a codebase designed for Tailwind v4 from the ground up.&lt;/p&gt;

&lt;p&gt;Both are free, MIT licensed, and framework-agnostic. The question is how far the library goes on the details that matter for your project.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://frutjam.com/components" rel="noopener noreferrer"&gt;Browse all 65+ Frutjam components&lt;/a&gt; and see what the difference looks like in code.&lt;/p&gt;

&lt;h2&gt;
  
  
  Components mentioned in this post
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://dev.to/components/button"&gt;Button&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/components/card"&gt;Card&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/components/modal"&gt;Modal&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/components/badge"&gt;Badge&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/components/navbar"&gt;Navbar&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/components/alert"&gt;Alert&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;What finally made you switch component libraries — accessibility, bundle size,&lt;br&gt;
or just fighting the same override every project?&lt;/p&gt;

</description>
      <category>tailwindcss</category>
      <category>css</category>
      <category>webdev</category>
      <category>opensource</category>
    </item>
    <item>
      <title>Why I Stopped Using JavaScript-Heavy UI Libraries (And Built a CSS-First Tailwind Component Library)</title>
      <dc:creator>Mohammed Rashad</dc:creator>
      <pubDate>Tue, 22 Sep 2026 12:36:34 +0000</pubDate>
      <link>https://dev.to/rashadmehtab/why-i-stopped-using-javascript-heavy-ui-libraries-and-built-a-css-first-tailwind-component-library-h1p</link>
      <guid>https://dev.to/rashadmehtab/why-i-stopped-using-javascript-heavy-ui-libraries-and-built-a-css-first-tailwind-component-library-h1p</guid>
      <description>&lt;p&gt;I build websites to solve problems, but after delivering several major multilingual platforms, I realized I was spending more time auditing performance than actually writing code. I'm a developer, not a professional performance janitor, yet I found myself stuck in a constant loop of chasing PageSpeed scores and fixing layout shifts caused by the very tools I was using. Instead of building new functionality, I spent more time fighting Cumulative Layout Shift (CLS) and writing tedious RTL (Right-to-Left) CSS overrides than actually shipping features.&lt;/p&gt;

&lt;p&gt;I realized my 'shortcut' of using popular, JS-heavy component libraries was actually my biggest bottleneck. I was constantly battling render-blocking scripts just to open a simple dropdown and manually retrofitting WAI-ARIA tags to pass basic accessibility audits.&lt;/p&gt;

&lt;p&gt;I didn't want another bloated library. I wanted a &lt;strong&gt;CSS-first Tailwind component library&lt;/strong&gt; that made &lt;a href="https://pagespeed.web.dev/analysis?url=https://frutjam.com" rel="noopener noreferrer"&gt;100% PageSpeed Insights scores&lt;/a&gt; the default, not a struggle. That's why I spent the last few months building &lt;strong&gt;Frutjam&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Here is why shifting to &lt;strong&gt;native-first with CSS-first components&lt;/strong&gt; completely changed my workflow, and why it will fix your Core Web Vitals, too.&lt;/p&gt;




&lt;h2&gt;
  
  
  1. The Power of a CSS-First Tailwind Component Library
&lt;/h2&gt;

&lt;p&gt;Modern web technologies are increasingly approaching a "native-first" standard. Years ago, we had to rely on heavy JavaScript bundles just to create a functioning modal, a popover, or a toggleable accordion because the browser lacked built-in solutions. Today, the web platform has caught up. The browser's native rendering engine is now incredibly powerful, equipped with standardized elements designed specifically for these interactions.&lt;/p&gt;

&lt;p&gt;By ignoring these native features and continuing to ship megabytes of state-management JS just for basic UI, we are actively working against the browser.&lt;/p&gt;

&lt;p&gt;Frutjam takes a &lt;strong&gt;native-first with a CSS-first approach&lt;/strong&gt;. This means we trust the browser to handle the core logic (Native-First) while using modern CSS for the behavioral styling and visual identity (CSS-First).&lt;/p&gt;

&lt;p&gt;Take a standard accordion. Instead of writing complex state-management logic in React or Svelte, you can achieve a perfectly accessible, highly performant accordion using the native &lt;code&gt;&amp;lt;details&amp;gt;&lt;/code&gt; and &lt;code&gt;&amp;lt;summary&amp;gt;&lt;/code&gt; tags, styled cleanly with CSS:&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;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"accordion"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;details&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"default-accordion"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;summary&amp;gt;&lt;/span&gt;Is this accessible?&lt;span class="nt"&gt;&amp;lt;/summary&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"accordion-body"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
            &lt;span class="nt"&gt;&amp;lt;p&amp;gt;&lt;/span&gt;Yes, because the browser handles the state natively.&lt;span class="nt"&gt;&amp;lt;/p&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/details&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;By combining this native structure with advanced CSS features like &lt;code&gt;@starting-style&lt;/code&gt;, you get the exact same premium feel and smooth animations with zero JavaScript overhead. And once your markup is this clean, maintaining it becomes a completely different experience.&lt;/p&gt;




&lt;h2&gt;
  
  
  2. No More Ugly HTML: Fewer Classes, Faster Development
&lt;/h2&gt;

&lt;p&gt;We all love Tailwind CSS, but we've all seen the "ugly HTML" problem: a single button wrapped in 20 different utility classes. It creates a cluster of code that is hard to read, terrible to maintain, and prone to copy-paste errors.&lt;/p&gt;

&lt;p&gt;With Frutjam, you don't need to rewrite the whole Tailwind class string every time you need a button. I've grouped those utility clusters into clean, prebuilt component classes.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Fewer class names&lt;/strong&gt;: Keep your markup clean and readable.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Easily maintainable&lt;/strong&gt;: Update a component's style in one place without hunting down 50 different &lt;code&gt;&amp;lt;div&amp;gt;&lt;/code&gt; tags.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Faster development&lt;/strong&gt;: Drop in the component and move on to building your actual application logic.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Cleaner markup also means your theming system has less to fight against, which brings me to one of my favorite parts of Frutjam.&lt;/p&gt;




&lt;h2&gt;
  
  
  3. Unlimited Themes With Zero Effort
&lt;/h2&gt;

&lt;p&gt;Most UI libraries restrict you to a handful of predefined themes. Frutjam adds a set of highly customizable color names directly to your Tailwind CSS configuration. This allows you to generate &lt;strong&gt;unlimited themes&lt;/strong&gt; with almost zero effort.&lt;/p&gt;

&lt;p&gt;Whether your client needs a dark, neon cyberpunk aesthetic or a clean, corporate minimalist look, the design system adapts to your brand colors instantly without requiring you to write custom CSS overrides from scratch. But themes are only useful if the colors actually pass accessibility standards — and that's where most libraries quietly let you down.&lt;/p&gt;




&lt;h2&gt;
  
  
  4. Accessibility and Color Contrast by Default
&lt;/h2&gt;

&lt;p&gt;I was shocked to find that even "industry standard" libraries were failing basic WCAG contrast checks in their default themes. If you look at libraries like &lt;strong&gt;DaisyUI&lt;/strong&gt;, their color contrast often fails across various themes. In modern web development, poor contrast isn't just bad UX — it tanks your SEO.&lt;/p&gt;

&lt;p&gt;I built Frutjam so that color logic is handled automatically. Every theme you create within the library is designed to pass accessibility and color contrast checks out of the box. You get a production-ready, SEO-friendly UI without needing to be an accessibility expert. That kind of guarantee matters even more when you're building for global audiences — including right-to-left ones.&lt;/p&gt;




&lt;h2&gt;
  
  
  5. The "Big Three" Comparison
&lt;/h2&gt;

&lt;p&gt;When I was architecting Frutjam, I looked at what was slowing me down with other tools. Here is how taking a native-first approach compares:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Feature&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;Frutjam&lt;/strong&gt;&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;Preline&lt;/strong&gt;&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;Shadcn&lt;/strong&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Logic&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Native-First&lt;/strong&gt; (CSS/HTML)&lt;/td&gt;
&lt;td&gt;JS-Dependent&lt;/td&gt;
&lt;td&gt;Framework-Specific (React/Svelte)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;PageSpeed&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Ultra Fast (Zero JS CLS)&lt;/td&gt;
&lt;td&gt;Can cause render-blocking&lt;/td&gt;
&lt;td&gt;Depends on your JS bundle&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;A11y / Contrast&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Guaranteed Pass&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Manual Effort&lt;/td&gt;
&lt;td&gt;Manual Setup&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Setup&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Plug &amp;amp; Play&lt;/td&gt;
&lt;td&gt;Requires JS Plugins&lt;/td&gt;
&lt;td&gt;High Boilerplate&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;RTL Support&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Native Built-in&lt;/td&gt;
&lt;td&gt;Requires Overrides&lt;/td&gt;
&lt;td&gt;Manual Configuration&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Versus Preline&lt;/strong&gt;: Preline relies heavily on JavaScript for components, which directly contributes to layout shifts (CLS) as the JS kicks in. Frutjam uses native HTML, meaning zero shift and pristine PageSpeed scores.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Versus Shadcn&lt;/strong&gt;: Shadcn is beautiful but complex and highly opinionated. It requires copying massive amounts of boilerplate into your project. Frutjam is &lt;strong&gt;truly framework-agnostic&lt;/strong&gt;. Whether you are spinning up a Django backend, a Node application, or a Svelte frontend, it drops in flawlessly.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That framework-agnostic design also makes RTL support something you get automatically, rather than something you bolt on afterwards.&lt;/p&gt;




&lt;h2&gt;
  
  
  6. First-Class Multilingual (RTL) Support
&lt;/h2&gt;

&lt;p&gt;Building for the MENA region means RTL isn't an afterthought; it's a strict requirement. I got tired of writing &lt;code&gt;dir="rtl"&lt;/code&gt; overrides for every single component. Frutjam handles multi-language layouts naturally without requiring massive CSS hacks, saving hours of development time on internationalized projects.&lt;/p&gt;




&lt;h2&gt;
  
  
  Stop Fighting Your Tooling
&lt;/h2&gt;

&lt;p&gt;You shouldn't have to compromise between shipping fast, maintaining clean HTML, and hitting a 100% PageSpeed score. By shifting the heavy lifting back to the browser and utilizing a CSS-first Tailwind component library, high performance stops being a chore and becomes the default state of your application.&lt;/p&gt;

&lt;p&gt;I've shipped real production projects with this approach — multilingual, RTL-ready platforms that score green across the board without a single JavaScript workaround for UI state. That's not a promise; it's just what happens when you stop fighting the browser and start working with it.&lt;/p&gt;

&lt;p&gt;If you're building something where performance, accessibility, and clean code actually matter, this is the stack worth trying. The bloat you eliminate on day one pays dividends every time you ship.&lt;/p&gt;

&lt;p&gt;If you are tired of JS-heavy interfaces and messy class clusters slowing down your builds, &lt;a href="https://frutjam.com" rel="noopener noreferrer"&gt;view the live documentation&lt;/a&gt;, &lt;a href="https://github.com/nezanuha/frutjam" rel="noopener noreferrer"&gt;check the source code on GitHub&lt;/a&gt;, or &lt;a href="https://www.npmjs.com/package/frutjam" rel="noopener noreferrer"&gt;grab the package on npm&lt;/a&gt; to try the CSS-first approach for yourself.&lt;/p&gt;

&lt;h2&gt;
  
  
  Components mentioned in this post
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://dev.to/components/button"&gt;Button&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/components/modal"&gt;Modal&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/components/card"&gt;Card&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/components/navbar"&gt;Navbar&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/docs/installation"&gt;Installation guide&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/docs/themes"&gt;Themes&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>webdev</category>
      <category>css</category>
      <category>tailwindcss</category>
      <category>performance</category>
    </item>
  </channel>
</rss>
