<?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: Mohamed Sadok</title>
    <description>The latest articles on DEV Community by Mohamed Sadok (@mohamed_sadok_42edff3a1b3).</description>
    <link>https://dev.to/mohamed_sadok_42edff3a1b3</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%2F2130609%2F94d003fd-e2e2-475d-9b08-b326062707fe.jpg</url>
      <title>DEV Community: Mohamed Sadok</title>
      <link>https://dev.to/mohamed_sadok_42edff3a1b3</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/mohamed_sadok_42edff3a1b3"/>
    <language>en</language>
    <item>
      <title>I Got Tired of Rebuilding the Same Support Portal. So I Built One I Could Reuse.</title>
      <dc:creator>Mohamed Sadok</dc:creator>
      <pubDate>Fri, 16 Oct 2026 07:46:56 +0000</pubDate>
      <link>https://dev.to/mohamed_sadok_42edff3a1b3/i-got-tired-of-rebuilding-the-same-support-portal-so-i-built-one-i-could-reuse-2mba</link>
      <guid>https://dev.to/mohamed_sadok_42edff3a1b3/i-got-tired-of-rebuilding-the-same-support-portal-so-i-built-one-i-could-reuse-2mba</guid>
      <description>&lt;p&gt;&lt;em&gt;Every client wants a slightly different help desk. But underneath all those requests? The same familiar Laravel features, the same repetitive setup, and the same hours spent rebuilding things you’ve already built.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;At some point, you have to ask yourself: &lt;strong&gt;Why am I building the same application for the fifth time?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;If you work with Laravel, you probably know the situation.&lt;/p&gt;

&lt;p&gt;A client needs a support portal. Nothing too complicated, they say.&lt;/p&gt;

&lt;p&gt;Just authentication. Customer accounts. Support tickets. Priorities. Replies. File attachments. Email notifications. An admin dashboard. Agent permissions. Maybe internal notes so the support team can discuss a ticket without the customer seeing everything.&lt;/p&gt;

&lt;p&gt;Oh, and could we have a dashboard with a few useful statistics?&lt;/p&gt;

&lt;p&gt;Sure. No problem.&lt;/p&gt;

&lt;p&gt;Then you open your editor, create the Laravel project, configure the database, set up authentication, build the ticket system, write the policies, create the dashboard, and start connecting everything together.&lt;/p&gt;

&lt;p&gt;A few days later, you're looking at code you've written before. Not exactly the same code, of course. That would be too convenient. It's an older implementation from another project, with different assumptions, different dependencies, and just enough inconsistencies to make copying it more work than starting over.&lt;/p&gt;

&lt;p&gt;I've been there.&lt;/p&gt;

&lt;p&gt;So I decided to stop treating support portals as a new project every time and build a reusable foundation instead.&lt;/p&gt;

&lt;p&gt;That's how &lt;strong&gt;NEXORA&lt;/strong&gt;, my Laravel support portal starter kit, came together.&lt;/p&gt;

&lt;p&gt;The goal wasn't to build another flashy SaaS dashboard with beautiful cards and meaningless charts. It was to package the repetitive, easy-to-overlook engineering work into something developers could actually use as a starting point.&lt;/p&gt;

&lt;p&gt;And some of that work is more interesting than it sounds.&lt;/p&gt;

&lt;h2&gt;
  
  
  First, let's talk about the boring stuff
&lt;/h2&gt;

&lt;p&gt;I like modern JavaScript frameworks as much as the next developer, but not every application needs a separate frontend, an API layer, and another build pipeline to maintain.&lt;/p&gt;

&lt;p&gt;For this project, I kept things deliberately straightforward:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Laravel 13 and PHP 8.3&lt;/strong&gt; for the application.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;MariaDB 10.11&lt;/strong&gt; for the database.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Blade, Tailwind CSS, Alpine.js, and Vite&lt;/strong&gt; for the interface.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Laravel Breeze&lt;/strong&gt; for authentication.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;DDEV&lt;/strong&gt; for a reproducible development environment.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;No separate SPA. No unnecessary API layer. No complicated frontend architecture just to display a list of tickets.&lt;/p&gt;

&lt;p&gt;The objective was simple: give developers a foundation they can understand, customize, and extend without spending their first afternoon figuring out how everything fits together.&lt;/p&gt;

&lt;p&gt;The environment is containerized, dependencies are locked in &lt;code&gt;composer.lock&lt;/code&gt;, and the setup process is designed to be straightforward.&lt;/p&gt;

&lt;p&gt;Because the last thing you need when starting a new project is to spend two hours debugging your local environment before you can even see the login page.&lt;/p&gt;

&lt;h2&gt;
  
  
  Three roles. One rule: permissions must actually mean something.
&lt;/h2&gt;

&lt;p&gt;NEXORA has three main roles:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Customer:&lt;/strong&gt; Creates tickets, follows progress, and communicates with the support team.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Support Agent:&lt;/strong&gt; Manages tickets, responds to customers, and adds internal notes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Super Admin:&lt;/strong&gt; Manages users, configuration, and the overall support operation.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That sounds pretty standard. The interesting part is how those permissions are enforced.&lt;/p&gt;

&lt;p&gt;Hiding an admin button from a customer isn't security. Neither is hiding a URL in the navigation menu.&lt;/p&gt;

&lt;p&gt;The application uses Laravel policies, middleware, and user-scoped queries to enforce access on the server.&lt;/p&gt;

&lt;p&gt;For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight 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;view&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;User&lt;/span&gt; &lt;span class="nv"&gt;$user&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;Ticket&lt;/span&gt; &lt;span class="nv"&gt;$ticket&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kt"&gt;bool&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;$user&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;isStaff&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nv"&gt;$ticket&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;user_id&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nv"&gt;$user&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;id&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 rule is simple: staff can access tickets according to their permissions, while customers can only access their own.&lt;/p&gt;

&lt;p&gt;And that rule isn't just sitting in a PHP file looking impressive. It's backed by automated tests.&lt;/p&gt;

&lt;p&gt;If Alice tries to access Bob's ticket by manually changing the URL, the application rejects the request.&lt;/p&gt;

&lt;p&gt;The same principle applies to replies, attachments, and other customer-specific resources.&lt;/p&gt;

&lt;p&gt;I also included safeguards against a surprisingly unpleasant administrative problem: accidentally locking yourself out of your own application.&lt;/p&gt;

&lt;p&gt;The system prevents the final active Super Admin from being demoted or deactivated, and prevents an administrator from deactivating their own account.&lt;/p&gt;

&lt;p&gt;These aren't the features that make a product look impressive in a screenshot. They're the features you appreciate when something goes wrong.&lt;/p&gt;

&lt;h2&gt;
  
  
  The tiny problem that becomes a big problem: ticket numbers
&lt;/h2&gt;

&lt;p&gt;Imagine a customer submits a support request.&lt;/p&gt;

&lt;p&gt;They receive a reference number:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;SUP-2026-000123&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Looks simple enough.&lt;/p&gt;

&lt;p&gt;Now imagine two customers submit tickets at almost exactly the same time.&lt;/p&gt;

&lt;p&gt;If your application generates numbers using a naive &lt;code&gt;MAX(id) + 1&lt;/code&gt; approach, concurrent requests can interfere with one another. You might end up with duplicate numbers or unexpected failures.&lt;/p&gt;

&lt;p&gt;It's an easy detail to overlook when you're building a prototype. It's less amusing when a real customer contacts support about ticket number &lt;code&gt;SUP-2026-000123&lt;/code&gt; and another customer has the same reference.&lt;/p&gt;

&lt;p&gt;NEXORA uses a per-year sequence table, database transactions, and pessimistic row locking to coordinate number generation.&lt;/p&gt;

&lt;p&gt;The simplified idea looks like this:&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="nv"&gt;$sequence&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;DB&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;table&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'ticket_sequences'&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;where&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'year'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$year&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;lockForUpdate&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;first&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="nv"&gt;$next&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$sequence&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;last_number&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="no"&gt;DB&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;table&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'ticket_sequences'&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;where&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'year'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$year&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;update&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="s1"&gt;'last_number'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$next&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;

&lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nb"&gt;sprintf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'SUP-%d-%06d'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$year&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$next&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The sequence update happens within the ticket creation transaction, and a unique database constraint provides an additional safeguard.&lt;/p&gt;

&lt;p&gt;The important detail isn't the formatting of the reference number. It's the fact that generating it is treated as a concurrency problem rather than a string-formatting problem.&lt;/p&gt;

&lt;p&gt;That's the difference between code that works in a quick demo and code designed for real application behavior.&lt;/p&gt;

&lt;h2&gt;
  
  
  Internal notes: the feature that must stay internal
&lt;/h2&gt;

&lt;p&gt;Here's a familiar customer request:&lt;/p&gt;

&lt;p&gt;&lt;em&gt;“Can our support agents leave private notes on a ticket without the customer seeing them?”&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Of course.&lt;/p&gt;

&lt;p&gt;But consider what happens after you implement it.&lt;/p&gt;

&lt;p&gt;The note doesn't appear on the customer-facing ticket page. Great.&lt;/p&gt;

&lt;p&gt;Then a search query returns it. Or an email notification includes it. Or an export accidentally exposes it.&lt;/p&gt;

&lt;p&gt;Suddenly, a feature that seemed simple has become a privacy problem.&lt;/p&gt;

&lt;p&gt;For NEXORA, internal notes are handled as a data-access rule, not a visual preference.&lt;/p&gt;

&lt;p&gt;Customer-facing ticket queries explicitly exclude internal replies:&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="s1"&gt;'ticket'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$ticket&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;load&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;
    &lt;span class="s1"&gt;'replies'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$query&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$query&lt;/span&gt;
        &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;where&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'is_internal'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;with&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="s1"&gt;'user'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'attachments'&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 same separation applies to search results, notifications, and activity timelines.&lt;/p&gt;

&lt;p&gt;An internal note must not be sent to the customer through an email notification. Internal activity must not appear in the customer's timeline. Customer searches must not expose private note content.&lt;/p&gt;

&lt;p&gt;I wrote tests specifically for these cases, including a test that uses a secret string in an internal note and checks that it doesn't leak through the customer-facing page, search, or email notifications.&lt;/p&gt;

&lt;p&gt;This is the kind of feature where "it seems to work" isn't good enough.&lt;/p&gt;

&lt;p&gt;The system needs to behave correctly even when someone accesses it in an unexpected way.&lt;/p&gt;

&lt;h2&gt;
  
  
  File uploads: because not every attachment should be public
&lt;/h2&gt;

&lt;p&gt;Support tickets often contain screenshots, PDFs, documents, and other files.&lt;/p&gt;

&lt;p&gt;Convenient, until you realize that a file uploaded by one customer shouldn't automatically become accessible to everyone with a URL.&lt;/p&gt;

&lt;p&gt;NEXORA stores attachments on Laravel's private local disk, using UUID-based filenames. Original filenames are stored in the database rather than used directly as storage paths.&lt;/p&gt;

&lt;p&gt;Downloads pass through a controller that:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Authenticates the requester.&lt;/li&gt;
&lt;li&gt;Checks whether that user is authorized to access the attachment.&lt;/li&gt;
&lt;li&gt;Restricts internal attachments to staff.&lt;/li&gt;
&lt;li&gt;Streams the file as a download instead of rendering it inline in the browser.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Uploads are validated server-side against a configurable extension allowlist and file-size limit.&lt;/p&gt;

&lt;p&gt;For example, PHP files are rejected by the upload validation rules, and unauthorized download attempts are covered by tests.&lt;/p&gt;

&lt;p&gt;There's also an important limitation worth mentioning: &lt;strong&gt;the starter kit does not include malware scanning.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;An extension allowlist is useful, but it isn't a replacement for antivirus scanning or a complete file-security strategy. Applications with stricter requirements should integrate an appropriate scanning service, such as ClamAV, and configure storage and access controls for their deployment environment.&lt;/p&gt;

&lt;p&gt;I'd rather document that limitation clearly than pretend a validation rule solves every upload-security problem.&lt;/p&gt;

&lt;h2&gt;
  
  
  A Kanban board is easy. A correct workflow is harder.
&lt;/h2&gt;

&lt;p&gt;The agent dashboard includes a Kanban-style board for moving tickets between statuses.&lt;/p&gt;

&lt;p&gt;It's a familiar interaction: drag a card from &lt;em&gt;Open&lt;/em&gt; to &lt;em&gt;In Progress&lt;/em&gt;, and the board updates.&lt;/p&gt;

&lt;p&gt;But the browser shouldn't be the authority on whether a transition is allowed.&lt;/p&gt;

&lt;p&gt;A user can modify client-side code, send a direct HTTP request, or simply bypass the interface entirely.&lt;/p&gt;

&lt;p&gt;That's why NEXORA validates ticket transitions on the server.&lt;/p&gt;

&lt;p&gt;The lifecycle includes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Open&lt;/li&gt;
&lt;li&gt;In Progress&lt;/li&gt;
&lt;li&gt;Waiting for Customer&lt;/li&gt;
&lt;li&gt;Resolved&lt;/li&gt;
&lt;li&gt;Closed&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Not every transition is valid. Moving a resolved ticket directly back into progress, for example, shouldn't be allowed simply because someone managed to drag a card there.&lt;/p&gt;

&lt;p&gt;A dedicated &lt;code&gt;TicketService::changeStatus()&lt;/code&gt; method validates the transition, updates the relevant timestamps, records an activity entry, and performs the operation inside a database transaction.&lt;/p&gt;

&lt;p&gt;The Kanban board uses that same service.&lt;/p&gt;

&lt;p&gt;If a transition is invalid, the server rejects it, the interface displays feedback, and the card returns to its previous position.&lt;/p&gt;

&lt;p&gt;The principle is straightforward: &lt;strong&gt;the interface can make an action convenient, but the backend must decide whether it's permitted.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;That keeps the workflow consistent whether a status change comes from a drag-and-drop interaction or another request to the application.&lt;/p&gt;

&lt;h2&gt;
  
  
  Dashboard statistics shouldn't be works of fiction
&lt;/h2&gt;

&lt;p&gt;We've all seen dashboards that look impressive until someone asks where the numbers came from.&lt;/p&gt;

&lt;p&gt;NEXORA calculates first-response and resolution averages from actual timestamps.&lt;/p&gt;

&lt;p&gt;When there isn't enough data, it displays a message explaining that rather than inventing a statistic to fill an empty card.&lt;/p&gt;

&lt;p&gt;It sounds obvious, but there's a meaningful difference between a dashboard that looks complete and one that tells the truth.&lt;/p&gt;

&lt;p&gt;Seed data is useful for exploring the interface locally. It shouldn't become a substitute for real application metrics.&lt;/p&gt;

&lt;p&gt;For a support team, these numbers can eventually influence staffing, workload planning, and service improvements. Their definitions should be clear, and their calculations should be grounded in actual events.&lt;/p&gt;

&lt;h2&gt;
  
  
  The test suite is part of the product
&lt;/h2&gt;

&lt;p&gt;I didn't want NEXORA to be just a collection of controllers, models, and attractive dashboard screens.&lt;/p&gt;

&lt;p&gt;The starter kit includes &lt;strong&gt;78 automated tests covering 177 assertions&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The suite covers scenarios such as:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Guests being blocked from protected areas.&lt;/li&gt;
&lt;li&gt;Disabled accounts losing access.&lt;/li&gt;
&lt;li&gt;Customers being prevented from viewing or modifying another customer's tickets.&lt;/li&gt;
&lt;li&gt;Unauthorized attachment downloads being rejected.&lt;/li&gt;
&lt;li&gt;Internal notes being excluded from customer-facing pages, search, and notifications.&lt;/li&gt;
&lt;li&gt;Role-escalation attempts being rejected.&lt;/li&gt;
&lt;li&gt;Invalid ticket transitions being blocked.&lt;/li&gt;
&lt;li&gt;Oversized or disallowed uploads being rejected.&lt;/li&gt;
&lt;li&gt;Administrative safeguards preventing the last active Super Admin from being removed.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The tests run against an in-memory SQLite database, keeping the test environment isolated from the application's normal data.&lt;/p&gt;

&lt;p&gt;Before releasing a version, I run the suite with:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ddev &lt;span class="nb"&gt;exec &lt;/span&gt;php artisan &lt;span class="nb"&gt;test&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Of course, a passing test suite doesn't prove that an application is free of vulnerabilities. It does provide something valuable: repeatable evidence that important behaviors work as intended.&lt;/p&gt;

&lt;p&gt;It also makes the code easier to understand.&lt;/p&gt;

&lt;p&gt;Want to know what happens when a customer tries to reopen a closed ticket? There's a test. Wondering whether internal notes trigger customer emails? There's a test for that, too.&lt;/p&gt;

&lt;p&gt;Documentation explains how a system is supposed to work. Tests help demonstrate how it actually behaves.&lt;/p&gt;

&lt;h2&gt;
  
  
  What NEXORA doesn't include
&lt;/h2&gt;

&lt;p&gt;I could describe this as a complete, production-ready support platform and fill the product page with ambitious promises.&lt;/p&gt;

&lt;p&gt;That wouldn't be particularly useful.&lt;/p&gt;

&lt;p&gt;NEXORA is a starter kit, not a finished enterprise help desk. It provides a foundation you can adapt to your own requirements.&lt;/p&gt;

&lt;p&gt;It deliberately leaves some things outside its current scope:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;No built-in malware scanning for uploaded attachments.&lt;/li&gt;
&lt;li&gt;No rich-text message rendering; messages use escaped plain text.&lt;/li&gt;
&lt;li&gt;No two-factor authentication.&lt;/li&gt;
&lt;li&gt;No multilingual help-desk interface.&lt;/li&gt;
&lt;li&gt;No dedicated SLA-management or advanced reporting module.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You'll also need to handle the usual deployment responsibilities: HTTPS, backups, secret management, server patching, and environment-specific security configuration.&lt;/p&gt;

&lt;p&gt;The idea isn't to solve every possible support workflow. It's to avoid making you rebuild the same core functionality before you can start solving the problems that make your project different.&lt;/p&gt;

&lt;h2&gt;
  
  
  Who is this for?
&lt;/h2&gt;

&lt;p&gt;I built NEXORA with a few types of developers in mind.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Freelancers and agencies&lt;/strong&gt; who repeatedly build customer portals for different clients and want a reusable foundation instead of another from-scratch implementation.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;SaaS founders&lt;/strong&gt; who need a support workflow without spending their first development sprint rebuilding authentication, ticket management, and role-based access.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Small teams&lt;/strong&gt; that need an internal help desk for tracking requests, assigning responsibility, and managing conversations.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Laravel developers&lt;/strong&gt; who prefer starting with a structured codebase and adapting it rather than assembling every familiar feature again.&lt;/p&gt;

&lt;p&gt;There's also a static HTML/CSS/JavaScript demo included in the bundle, so you can explore the interface and present the visual direction to a client before setting up the Laravel application.&lt;/p&gt;

&lt;h2&gt;
  
  
  Getting started
&lt;/h2&gt;

&lt;p&gt;The development workflow uses DDEV, so you don't need to install the application's PHP and database dependencies directly on your host machine.&lt;/p&gt;

&lt;p&gt;The basic setup looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ddev start
ddev composer &lt;span class="nb"&gt;install
&lt;/span&gt;ddev &lt;span class="nb"&gt;exec &lt;/span&gt;php artisan key:generate
ddev &lt;span class="nb"&gt;exec &lt;/span&gt;php artisan migrate &lt;span class="nt"&gt;--seed&lt;/span&gt;
ddev npm &lt;span class="nb"&gt;install
&lt;/span&gt;ddev npm run build
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Once the environment is running and the setup is complete, you can explore the portal, review the code, and start adapting it to your project.&lt;/p&gt;

&lt;p&gt;For local development, the seeded demo accounts are:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Admin: &lt;code&gt;admin@nexora.test&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Support agent: &lt;code&gt;agent1@nexora.test&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Customer: &lt;code&gt;alice@nexora.test&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The demo password is &lt;code&gt;password&lt;/code&gt;. These credentials are intended for local testing only and must never be used for a production deployment.&lt;/p&gt;

&lt;h2&gt;
  
  
  So, why did I package it?
&lt;/h2&gt;

&lt;p&gt;Because I'd rather spend my time solving new problems than repeatedly rebuilding the same foundation.&lt;/p&gt;

&lt;p&gt;Every support portal will eventually develop its own requirements. One client will need a different workflow. Another will need custom fields. Someone else will want integrations, SLA rules, or a completely different dashboard.&lt;/p&gt;

&lt;p&gt;That's normal.&lt;/p&gt;

&lt;p&gt;But authentication, ticket ownership, private notes, attachment permissions, status transitions, and basic administration shouldn't have to start from zero every time.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;NEXORA is my attempt to turn that repetitive groundwork into something reusable, understandable, and easier to extend.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;If you're building a support portal, an internal help desk, or a SaaS application with customer support, you can explore the demo and decide whether it's a useful starting point for your next project.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Explore the live demo:&lt;/strong&gt; &lt;a href="https://supportticketdemo.vercel.app/" rel="noopener noreferrer"&gt;https://supportticketdemo.vercel.app/&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Get NEXORA on Gumroad:&lt;/strong&gt; [Insert your Gumroad product URL]&lt;/p&gt;

&lt;p&gt;And if you do build something with it, I'd be interested to see what you create.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Tags: Laravel, PHP, Web Development, SaaS, Software Engineering&lt;/em&gt;&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>programming</category>
      <category>productivity</category>
      <category>javascript</category>
    </item>
    <item>
      <title>I Built a Laravel RBAC Starter So I Could Stop Rebuilding Admin Panels</title>
      <dc:creator>Mohamed Sadok</dc:creator>
      <pubDate>Thu, 15 Oct 2026 09:08:41 +0000</pubDate>
      <link>https://dev.to/mohamed_sadok_42edff3a1b3/i-built-a-laravel-rbac-starter-so-i-could-stop-rebuilding-admin-panels-1819</link>
      <guid>https://dev.to/mohamed_sadok_42edff3a1b3/i-built-a-laravel-rbac-starter-so-i-could-stop-rebuilding-admin-panels-1819</guid>
      <description>&lt;p&gt;Every new Laravel project starts with a simple idea.&lt;/p&gt;

&lt;p&gt;Build a SaaS app. Create a CRM. Launch a client portal.&lt;/p&gt;

&lt;p&gt;Then the requirements arrive:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Admins can manage everything.&lt;/li&gt;
&lt;li&gt;Managers can manage some things.&lt;/li&gt;
&lt;li&gt;Editors can edit content but cannot delete users.&lt;/li&gt;
&lt;li&gt;Regular users should only access their own features.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Suddenly, your weekend project needs a role management system, a permissions catalog, protected routes, user administration, and authorization tests.&lt;/p&gt;

&lt;p&gt;Congratulations. You are building an administration framework before building your actual product.&lt;/p&gt;

&lt;p&gt;I've been there. So I built something to make that setup less repetitive.&lt;/p&gt;

&lt;h2&gt;
  
  
  The problem isn't authentication. It's everything around it.
&lt;/h2&gt;

&lt;p&gt;Laravel provides excellent authentication and authorization tools. Spatie Laravel Permission makes managing roles and permissions much easier.&lt;/p&gt;

&lt;p&gt;But integrating the pieces into a usable application still takes work.&lt;/p&gt;

&lt;p&gt;You need to connect permissions to roles, build an interface to manage them, protect sensitive actions, and verify that unauthorized users cannot bypass the UI by calling an endpoint directly.&lt;/p&gt;

&lt;p&gt;That last part is important.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Hiding a button is not access control.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;If a user cannot see the Delete button but can still submit a request to the deletion endpoint, your authorization implementation has a problem.&lt;/p&gt;

&lt;p&gt;The server must enforce the rules.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I built
&lt;/h2&gt;

&lt;p&gt;I created &lt;strong&gt;Laravel Roles &amp;amp; Permissions Starter&lt;/strong&gt;, a reusable foundation for Laravel applications that need role-based access control and an admin panel.&lt;/p&gt;

&lt;p&gt;The stack includes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Laravel 12 and PHP 8.2+&lt;/li&gt;
&lt;li&gt;Spatie Laravel Permission 8&lt;/li&gt;
&lt;li&gt;Laravel Breeze&lt;/li&gt;
&lt;li&gt;Blade, Tailwind CSS, and Alpine.js&lt;/li&gt;
&lt;li&gt;Vite and PHPUnit&lt;/li&gt;
&lt;li&gt;DDEV configuration for local development&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Here's what is included.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Five predefined roles
&lt;/h3&gt;

&lt;p&gt;The starter includes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Super Admin&lt;/li&gt;
&lt;li&gt;Admin&lt;/li&gt;
&lt;li&gt;Manager&lt;/li&gt;
&lt;li&gt;Editor&lt;/li&gt;
&lt;li&gt;User&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It uses permission keys such as &lt;code&gt;users.view&lt;/code&gt;, &lt;code&gt;roles.delete&lt;/code&gt;, and &lt;code&gt;permissions.manage&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;You can create, edit, and duplicate roles, assign permissions, and manage users with multiple roles. Protected-role safeguards help prevent accidental deletion of important roles.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Server-side authorization
&lt;/h3&gt;

&lt;p&gt;The application uses Laravel Gates and Policies to protect administrative functionality.&lt;/p&gt;

&lt;p&gt;Super Admin bypass logic is centralized, and protected roles and accounts have additional safeguards.&lt;/p&gt;

&lt;p&gt;The sidebar also adapts to the current user's permissions, but the backend remains responsible for enforcing access.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. User administration
&lt;/h3&gt;

&lt;p&gt;Manage registered users, assign roles, change account status, and delete users with confirmation.&lt;/p&gt;

&lt;p&gt;These are useful building blocks for SaaS applications, internal tools, CRMs, and business dashboards.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Audit logging
&lt;/h3&gt;

&lt;p&gt;Role, user, and permission changes are recorded with details such as the actor, action, subject, metadata, IP address, and user agent.&lt;/p&gt;

&lt;p&gt;Recent activity appears on the dashboard, and the underlying records can be queried from the database.&lt;/p&gt;

&lt;p&gt;A dedicated audit-log browsing interface is not included in this release.&lt;/p&gt;

&lt;h3&gt;
  
  
  5. A dashboard with real database data
&lt;/h3&gt;

&lt;p&gt;The dashboard displays counts for users, roles, permissions, and audit events, alongside an access overview and recent activity.&lt;/p&gt;

&lt;p&gt;No hardcoded dashboard numbers pretending to be production analytics.&lt;/p&gt;

&lt;h3&gt;
  
  
  6. Thirty-three automated tests
&lt;/h3&gt;

&lt;p&gt;The starter includes 33 tests covering authentication, profile management, and role-based access control.&lt;/p&gt;

&lt;p&gt;Examples include guest restrictions, authorization failures, role assignments, permission persistence, and default-role assignment during registration.&lt;/p&gt;

&lt;p&gt;Run them with:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;composer &lt;span class="nb"&gt;test&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Tests are not a guarantee that every future customization will be secure, but they give you a useful baseline for ongoing development.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try the demo before looking at the code
&lt;/h2&gt;

&lt;p&gt;I prefer showing developers what they're getting rather than asking them to trust a sales page.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Live demo:&lt;/strong&gt; &lt;a href="https://laravelrolesstarter.vercel.app/" rel="noopener noreferrer"&gt;https://laravelrolesstarter.vercel.app/&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Explore the interface and see whether it fits your next project.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Get the source code:&lt;/strong&gt; &lt;a href="https://boukataya.gumroad.com/l/xqmkag" rel="noopener noreferrer"&gt;https://boukataya.gumroad.com/l/xqmkag&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The package includes the application source code, migrations, seeders, factories, tests, setup instructions, and DDEV configuration.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it doesn't include
&lt;/h2&gt;

&lt;p&gt;To keep expectations clear, this release does not include REST API endpoints, a dedicated audit-log browser, an administration settings screen, or Docker Compose configuration.&lt;/p&gt;

&lt;p&gt;It focuses on the existing Laravel web application and its authentication, RBAC, user management, dashboard, and audit-logging functionality.&lt;/p&gt;

&lt;p&gt;You can extend it to fit your own architecture and requirements.&lt;/p&gt;

&lt;h2&gt;
  
  
  When is this useful?
&lt;/h2&gt;

&lt;p&gt;Consider it if you're building a SaaS application, CRM, client portal, e-commerce back office, or internal business tool and don't want to assemble the same administration foundation from scratch.&lt;/p&gt;

&lt;p&gt;The goal isn't to eliminate development. It's to reduce repetitive setup so you can focus on the features that make your application different.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Demo:&lt;/strong&gt; &lt;a href="https://laravelrolesstarter.vercel.app/" rel="noopener noreferrer"&gt;https://laravelrolesstarter.vercel.app/&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Product:&lt;/strong&gt; &lt;a href="https://boukataya.gumroad.com/l/xqmkag" rel="noopener noreferrer"&gt;https://boukataya.gumroad.com/l/xqmkag&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;What part of a new Laravel project do you find yourself rebuilding most often: authentication, authorization, user management, or something else?&lt;/p&gt;

</description>
      <category>laravel</category>
      <category>php</category>
      <category>webdev</category>
      <category>beginners</category>
    </item>
    <item>
      <title>I Built 8 SaaS Landing Pages. Here Are the 11 Sections That Make Them Work.</title>
      <dc:creator>Mohamed Sadok</dc:creator>
      <pubDate>Wed, 14 Oct 2026 14:24:08 +0000</pubDate>
      <link>https://dev.to/mohamed_sadok_42edff3a1b3/i-built-8-saas-landing-pages-here-are-the-11-sections-that-make-them-work-2dkd</link>
      <guid>https://dev.to/mohamed_sadok_42edff3a1b3/i-built-8-saas-landing-pages-here-are-the-11-sections-that-make-them-work-2dkd</guid>
      <description>&lt;p&gt;What I learned from building eight different landing page templates about structure, trust, product demos, pricing, and turning visitors into potential customers.&lt;/p&gt;

&lt;p&gt;Most SaaS landing pages don’t have a design problem.&lt;/p&gt;

&lt;p&gt;They have a clarity problem.&lt;/p&gt;

&lt;p&gt;The headline sounds impressive but says very little. The features section lists everything the product can do. The pricing page hides behind a “Contact us” button. And somewhere between the hero section and the footer, visitors lose interest.&lt;/p&gt;

&lt;p&gt;I’ve seen this pattern often enough that I wanted to approach it differently.&lt;/p&gt;

&lt;p&gt;Instead of building another landing page and tweaking it endlessly, I built eight complete SaaS landing page templates for different types of products: AI tools, B2B platforms, developer tools, analytics software, billing systems, automation products, startups, and pre-launch waitlists.&lt;/p&gt;

&lt;p&gt;The goal was simple: understand what makes each page useful, what changes depending on the product, and what stays the same.&lt;/p&gt;

&lt;p&gt;One thing became clear.&lt;/p&gt;

&lt;p&gt;Good SaaS landing pages don’t need to look alike. But they do need to answer the right questions in the right order.&lt;/p&gt;

&lt;p&gt;Here are the 11 sections I focused on.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Navigation: Make the next step obvious
Your navigation should help visitors understand where they are and where they can go next.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;It doesn’t need nine links, three dropdowns, and a button competing for attention.&lt;/p&gt;

&lt;p&gt;Start with the essentials:&lt;/p&gt;

&lt;p&gt;Your product name or logo&lt;br&gt;
A few useful links&lt;br&gt;
Pricing or documentation, where relevant&lt;br&gt;
One clear call to action&lt;br&gt;
Also, don’t forget mobile users. The menu should open, close properly, and support keyboard interaction.&lt;/p&gt;

&lt;p&gt;Small details matter because they reveal whether the website was built carefully or merely made to look good in a screenshot.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Hero section: Explain your product without making people think
The hero section is your first opportunity to answer three questions:&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;What is this? Who is it for? Why should I care?&lt;/p&gt;

&lt;p&gt;Compare these two headlines:&lt;/p&gt;

&lt;p&gt;“An AI-powered platform for the modern enterprise.”&lt;/p&gt;

&lt;p&gt;“Automate repetitive support tasks without building workflows from scratch.”&lt;/p&gt;

&lt;p&gt;The first sounds like marketing. The second tells me what the product does and why I might need it.&lt;/p&gt;

&lt;p&gt;A practical hero section includes:&lt;/p&gt;

&lt;p&gt;A specific headline&lt;br&gt;
A short supporting sentence&lt;br&gt;
One primary call to action&lt;br&gt;
A secondary action if necessary&lt;br&gt;
A reassurance line when relevant&lt;br&gt;
Don’t try to explain your entire business in the headline. Give visitors a reason to keep reading.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Product preview: Show what you’re actually selling
A landing page can make almost any product sound impressive.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;A real product interface makes the promise easier to understand.&lt;/p&gt;

&lt;p&gt;Depending on what you’re building, show a dashboard, terminal, workflow editor, analytics chart, or chat interface.&lt;/p&gt;

&lt;p&gt;This gives visitors something concrete to evaluate before they sign up.&lt;/p&gt;

&lt;p&gt;When building landing pages, I prefer interface mockups made with editable code when practical. They can adapt to different screen sizes, and the sample content can be changed without redesigning an image.&lt;/p&gt;

&lt;p&gt;One important distinction: demonstration data should be clearly labelled. A convincing mockup is useful; pretending it represents real customers or actual performance is not.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Trust section: Don’t manufacture credibility
Social proof can be powerful when it’s genuine.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Real customer logos, verified testimonials, relevant certifications, and credible case studies can help people feel more comfortable choosing your product.&lt;/p&gt;

&lt;p&gt;But what if you’re launching your first SaaS?&lt;/p&gt;

&lt;p&gt;Don’t invent customers. Don’t create fictional testimonials and present them as real. Don’t display compliance badges you haven’t earned.&lt;/p&gt;

&lt;p&gt;Instead, communicate things you can actually substantiate.&lt;/p&gt;

&lt;p&gt;Explain your pricing clearly. Show the product. Link to documentation. Describe your cancellation policy. Make it easy to understand how the service works.&lt;/p&gt;

&lt;p&gt;Trust isn’t something you can manufacture with a row of logos.&lt;/p&gt;

&lt;p&gt;It’s the result of giving visitors good reasons to believe you.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Features: Focus on what deserves attention
Your product might have dozens of features.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Your landing page doesn’t need to explain all of them.&lt;/p&gt;

&lt;p&gt;Start with the capabilities most relevant to your ideal customer. For each feature, explain what it does and why the visitor should care.&lt;/p&gt;

&lt;p&gt;For example:&lt;/p&gt;

&lt;p&gt;Feature: Automated payment retries.&lt;/p&gt;

&lt;p&gt;Why it matters: Help recover failed payments without manually chasing every transaction.&lt;/p&gt;

&lt;p&gt;The second sentence connects the functionality to a practical outcome.&lt;/p&gt;

&lt;p&gt;If a feature doesn’t support the main reason someone would buy your product, consider moving it to the documentation.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Benefits: Translate functionality into outcomes
Features describe capabilities. Benefits describe what those capabilities make possible.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Consider a project management tool.&lt;/p&gt;

&lt;p&gt;“Real-time task synchronization” describes functionality.&lt;/p&gt;

&lt;p&gt;“Keep everyone working from the same up-to-date task list” explains the benefit.&lt;/p&gt;

&lt;p&gt;Neither statement is inherently wrong. They simply serve different purposes.&lt;/p&gt;

&lt;p&gt;A good landing page connects the two so visitors can understand both what the product does and how it fits into their work.&lt;/p&gt;

&lt;p&gt;One simple editing technique is to ask “So what?” after every feature statement.&lt;/p&gt;

&lt;p&gt;If the answer isn’t obvious, rewrite it.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;How it works: Reduce uncertainty about getting started
Visitors don’t just evaluate whether your product is useful.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;They also consider how much effort it will take to get value from it.&lt;/p&gt;

&lt;p&gt;Become a Medium member&lt;br&gt;
If your product is easy to set up, explain the process in three or four steps.&lt;/p&gt;

&lt;p&gt;For example:&lt;/p&gt;

&lt;p&gt;Connect your existing tools.&lt;br&gt;
Configure your workflow.&lt;br&gt;
Review the results.&lt;br&gt;
Adjust the setup to fit your needs.&lt;br&gt;
Use steps that reflect the actual product. If implementation takes a day, don’t promise that it takes five minutes.&lt;/p&gt;

&lt;p&gt;The purpose of this section is to make the onboarding process understandable, not to make it sound magically effortless.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Pricing: Answer the question visitors are already asking
People want to understand what a product costs before investing time in it.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If your business model supports transparent self-service pricing, put the information on the page.&lt;/p&gt;

&lt;p&gt;Explain what each plan includes, who it suits, and which limitations apply.&lt;/p&gt;

&lt;p&gt;A monthly/annual toggle can help visitors compare billing options, but it should actually update the displayed prices. Decorative interactions create confusion rather than confidence.&lt;/p&gt;

&lt;p&gt;If you’re still validating your business model, label example prices clearly.&lt;/p&gt;

&lt;p&gt;And if your product requires custom enterprise pricing, explain why and make the next step straightforward.&lt;/p&gt;

&lt;p&gt;The goal isn’t to force every business into the same pricing layout. It’s to remove unnecessary uncertainty.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Testimonials: Use real experiences or leave them out
A testimonial should represent an actual person’s experience with your product.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;When you have permission to publish genuine customer feedback, include relevant context such as the person’s name, role, or organization where appropriate.&lt;/p&gt;

&lt;p&gt;When you don’t have testimonials yet, focus on demonstrating the product and explaining its value.&lt;/p&gt;

&lt;p&gt;You don’t need to fill every section just because another SaaS website has it.&lt;/p&gt;

&lt;p&gt;A landing page is not a checklist of fashionable components. Every section should earn its place.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;FAQ: Address the objections that prevent a decision
The FAQ section is an opportunity to answer questions that might otherwise stop someone from moving forward.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Depending on your product, visitors might want to know:&lt;/p&gt;

&lt;p&gt;Does this work with my existing tools?&lt;br&gt;
Can I cancel whenever I want?&lt;br&gt;
What happens to my data if I leave?&lt;br&gt;
Is there a free trial?&lt;br&gt;
What is included in each plan?&lt;br&gt;
Do I need technical knowledge to use it?&lt;br&gt;
Answer these questions directly.&lt;/p&gt;

&lt;p&gt;If a feature isn’t available yet, say so. If a particular integration isn’t supported, don’t imply otherwise.&lt;/p&gt;

&lt;p&gt;Clear limitations help the right customers make informed decisions and reduce misunderstandings after purchase.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Final call to action: Make the next step clear
After visitors have explored your product, features, pricing, and FAQs, give them a clear next step.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;It could be starting a trial, creating an account, booking a demo, joining a waitlist, or purchasing a product.&lt;/p&gt;

&lt;p&gt;Keep the message consistent with the rest of the page.&lt;/p&gt;

&lt;p&gt;Don’t introduce a completely new promise in the final section. Reinforce the value you’ve already explained and make the action easy to find.&lt;/p&gt;

&lt;p&gt;A visitor who has read the entire page shouldn’t have to search for what to do next.&lt;/p&gt;

&lt;p&gt;What building eight landing pages taught me&lt;br&gt;
The biggest lesson wasn’t about gradients, typography, animations, or which button color looks best.&lt;/p&gt;

&lt;p&gt;It was about structure.&lt;/p&gt;

&lt;p&gt;Different SaaS products need different visual identities. An AI assistant shouldn’t necessarily look like a billing dashboard, and a developer tool doesn’t need to imitate a productivity platform.&lt;/p&gt;

&lt;p&gt;But each page still needs to explain the product, demonstrate its value, establish credibility, answer questions, and help visitors decide what to do next.&lt;/p&gt;

&lt;p&gt;That common structure makes the design process easier to reason about.&lt;/p&gt;

&lt;p&gt;It also gives you a useful starting point when you’re building a new product and don’t want to spend days deciding what sections to add.&lt;/p&gt;

&lt;p&gt;The visual design still matters. Accessibility matters. Performance matters. And none of these sections guarantees a particular conversion rate.&lt;/p&gt;

&lt;p&gt;But a clear page gives visitors a better opportunity to understand your offer.&lt;/p&gt;

&lt;p&gt;That’s a worthwhile place to start.&lt;/p&gt;

&lt;p&gt;I turned that approach into NEXORA&lt;br&gt;
While working on these eight designs, I wanted more than a folder of screenshots or a collection of empty starter components.&lt;/p&gt;

&lt;p&gt;I wanted a kit that I could explore, preview, and customize.&lt;/p&gt;

&lt;p&gt;That’s why I built NEXORA — a SaaS Landing Page Kit featuring eight complete landing page templates for different product categories.&lt;/p&gt;

&lt;p&gt;The kit includes:&lt;/p&gt;

&lt;p&gt;Eight distinct SaaS landing page designs&lt;br&gt;
A built-in gallery with live template previews&lt;br&gt;
Desktop, laptop, tablet, and mobile-width testing&lt;br&gt;
Editable product interface mockups&lt;br&gt;
Interactive pricing toggles and FAQ accordions&lt;br&gt;
Search and category filtering&lt;br&gt;
Documentation covering installation, customization, and deployment&lt;br&gt;
The technology stack is React 19, Vite 7, Tailwind CSS v4, and TypeScript. This is a React-based project, not a plain HTML/CSS template pack, so you’ll need Node.js 20+ to run and build it.&lt;/p&gt;

&lt;p&gt;Sample content is identified as demonstration content, and the waitlist form requires a provider or backend integration before it can collect submissions.&lt;/p&gt;

&lt;p&gt;If you’re building a SaaS product and want a starting point you can adapt rather than designing every section from scratch, you can explore the kit here:&lt;/p&gt;

&lt;p&gt;Get NEXORA on Gumroad &lt;a href="https://boukataya.gumroad.com/l/hrpgjy" rel="noopener noreferrer"&gt;https://boukataya.gumroad.com/l/hrpgjy&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;You can also explore the project before purchasing:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://nexora-saas-landing-page-kit.vercel.app/" rel="noopener noreferrer"&gt;https://nexora-saas-landing-page-kit.vercel.app/&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;And if you’d rather build everything yourself, start with the eleven sections in this article. You don’t need to buy a template to put the principles into practice.&lt;/p&gt;

&lt;p&gt;One question for other builders&lt;br&gt;
When you’re creating a landing page for a new SaaS product, which section takes you the longest to get right: the headline, the product preview, the features, or the pricing?&lt;/p&gt;

&lt;p&gt;I’d be interested to hear what other founders and developers struggle with most.&lt;/p&gt;

</description>
      <category>design</category>
      <category>saas</category>
      <category>startup</category>
      <category>webdev</category>
    </item>
    <item>
      <title>I Was Tired of Rebuilding SaaS Dashboards. So I Built NEXORA.</title>
      <dc:creator>Mohamed Sadok</dc:creator>
      <pubDate>Wed, 14 Oct 2026 10:40:26 +0000</pubDate>
      <link>https://dev.to/mohamed_sadok_42edff3a1b3/i-was-tired-of-rebuilding-saas-dashboards-so-i-built-nexora-2eg0</link>
      <guid>https://dev.to/mohamed_sadok_42edff3a1b3/i-was-tired-of-rebuilding-saas-dashboards-so-i-built-nexora-2eg0</guid>
      <description>&lt;p&gt;&lt;em&gt;Every new project seemed to start the same way: an empty dashboard, hours of repetitive CSS, and the same components I had already built dozens of times. I decided to turn that repetition into something reusable.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The Problem With Starting Every SaaS Project From Scratch
&lt;/h2&gt;

&lt;p&gt;If you've ever built a SaaS application, you probably know the routine.&lt;/p&gt;

&lt;p&gt;You start with a promising idea. You set up the project, plan the architecture, and get ready to build the features that make your product unique.&lt;/p&gt;

&lt;p&gt;Then comes the dashboard.&lt;/p&gt;

&lt;p&gt;You need a sidebar, navigation, statistics cards, charts, tables, filters, settings pages, and a consistent design system. Before you know it, you've spent days building an interface that looks remarkably similar to the one you built for your last project.&lt;/p&gt;

&lt;p&gt;And the frustrating part? Most of that work doesn't make your product more valuable. It simply gets you to the starting line.&lt;/p&gt;

&lt;p&gt;The usual alternatives aren't always ideal:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Complex starter kits:&lt;/strong&gt; Powerful, but they can introduce frameworks, dependencies, and setup requirements you don't actually need.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Free admin templates:&lt;/strong&gt; Useful for getting started, but customization, responsiveness, and consistent styling can become a project of their own.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Design mockups:&lt;/strong&gt; Great for planning, but someone still needs to turn those designs into working code.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I wanted something different: a professional dashboard that was ready to customize, easy to understand, and simple to integrate into different projects.&lt;/p&gt;

&lt;p&gt;That's why I built &lt;strong&gt;NEXORA&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Introducing NEXORA — A SaaS Dashboard UI Kit
&lt;/h2&gt;

&lt;p&gt;NEXORA is a reusable SaaS admin dashboard built with HTML, CSS, vanilla JavaScript, Tailwind CSS, Chart.js, and Lucide icons.&lt;/p&gt;

&lt;p&gt;It includes &lt;strong&gt;12 complete pages, 20+ reusable components, interactive charts, data tables, and light and dark themes&lt;/strong&gt; in one organized package.&lt;/p&gt;

&lt;p&gt;No React setup. No npm installation. No build pipeline.&lt;/p&gt;

&lt;p&gt;Open &lt;code&gt;index.html&lt;/code&gt; and explore the interface.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Try the live demo:&lt;/strong&gt; &lt;a href="https://nexora-saas-dashboard-template.vercel.app/" rel="noopener noreferrer"&gt;https://nexora-saas-dashboard-template.vercel.app/&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Get the complete UI kit:&lt;/strong&gt; &lt;a href="https://boukataya.gumroad.com/l/qydmik" rel="noopener noreferrer"&gt;https://boukataya.gumroad.com/l/qydmik&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What's Included?
&lt;/h2&gt;

&lt;p&gt;NEXORA isn't just a dashboard homepage. It's a collection of coordinated pages designed to cover common SaaS administration needs.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Overview Dashboard
&lt;/h3&gt;

&lt;p&gt;Start with a complete overview featuring KPI cards, revenue charts, traffic breakdowns, recent transactions, and activity feeds.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Analytics
&lt;/h3&gt;

&lt;p&gt;Explore acquisition metrics, conversion funnels, growth charts, date-range controls, and period comparisons.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Customer Management
&lt;/h3&gt;

&lt;p&gt;Work with searchable, sortable, and filterable customer tables, pagination, and add/edit modals.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Transactions
&lt;/h3&gt;

&lt;p&gt;Display payment histories, filter transactions by status, and export data to CSV.&lt;/p&gt;

&lt;h3&gt;
  
  
  5. Plans and Billing
&lt;/h3&gt;

&lt;p&gt;Build subscription management interfaces with pricing cards, monthly and annual billing options, feature comparisons, and usage indicators.&lt;/p&gt;

&lt;h3&gt;
  
  
  6. Team Management
&lt;/h3&gt;

&lt;p&gt;Organize team members, display roles, and manage pending invitations through a consistent interface.&lt;/p&gt;

&lt;h3&gt;
  
  
  7. Notifications
&lt;/h3&gt;

&lt;p&gt;Handle notification lists, read and unread states, filters, and bulk actions.&lt;/p&gt;

&lt;h3&gt;
  
  
  8. Profile and Settings
&lt;/h3&gt;

&lt;p&gt;Provide personal profile information and five organized settings panels covering general preferences, profiles, notifications, security, and appearance.&lt;/p&gt;

&lt;h3&gt;
  
  
  9. Help and Support
&lt;/h3&gt;

&lt;p&gt;Include an FAQ accordion and a contact form for common support workflows.&lt;/p&gt;

&lt;h3&gt;
  
  
  10. Components Library
&lt;/h3&gt;

&lt;p&gt;Explore reusable buttons, badges, inputs, switches, modals, dropdowns, toasts, tooltips, tabs, alerts, avatars, progress bars, and more.&lt;/p&gt;

&lt;h3&gt;
  
  
  11. Documentation
&lt;/h3&gt;

&lt;p&gt;Keep integration guidance close to the code, making customization easier to follow.&lt;/p&gt;

&lt;p&gt;All of this is designed to work together as one consistent dashboard experience.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Real Advantage: A Customizable Design System
&lt;/h2&gt;

&lt;p&gt;A dashboard can have dozens of components, but changing its visual identity shouldn't mean editing every component individually.&lt;/p&gt;

&lt;p&gt;NEXORA centralizes its design tokens in CSS variables.&lt;/p&gt;

&lt;p&gt;For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="nd"&gt;:root&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="py"&gt;--accent&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;#6366f1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="py"&gt;--radius-lg&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;16px&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="py"&gt;--bg&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;#f6f7f9&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;Want a different accent color? Change the variable.&lt;/p&gt;

&lt;p&gt;Prefer sharper corners? Adjust the radius.&lt;/p&gt;

&lt;p&gt;Need to adapt the interface to your client's branding? Customize the design tokens instead of hunting through the entire stylesheet.&lt;/p&gt;

&lt;p&gt;The same design system supports both light and dark themes, helping maintain visual consistency across the dashboard.&lt;/p&gt;

&lt;p&gt;This makes NEXORA useful as more than a ready-made interface. It becomes a starting point you can adapt to your own product.&lt;/p&gt;

&lt;h2&gt;
  
  
  Interactive Components, Not Just Pretty Screens
&lt;/h2&gt;

&lt;p&gt;A good dashboard template should provide more than static cards and placeholder layouts.&lt;/p&gt;

&lt;p&gt;NEXORA includes interactive frontend functionality such as:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Search, sorting, filtering, and pagination for data tables.&lt;/li&gt;
&lt;li&gt;CSV export for transaction data.&lt;/li&gt;
&lt;li&gt;Interactive charts powered by Chart.js.&lt;/li&gt;
&lt;li&gt;Modals, dropdowns, tabs, and toast notifications.&lt;/li&gt;
&lt;li&gt;Light and dark theme switching.&lt;/li&gt;
&lt;li&gt;Responsive sidebar and mobile navigation.&lt;/li&gt;
&lt;li&gt;Keyboard-accessible interactions and visible focus states.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The goal is to reduce the repetitive frontend work that usually comes before connecting a dashboard to real application data.&lt;/p&gt;

&lt;h2&gt;
  
  
  One Central Place for Sample Data
&lt;/h2&gt;

&lt;p&gt;The dashboard's sample data is organized centrally, making it easier to understand where the displayed information comes from.&lt;/p&gt;

&lt;p&gt;Instead of searching through multiple page components to locate customer records or transactions, you can start with the existing data structure and replace it with your own API responses.&lt;/p&gt;

&lt;p&gt;For example, the integration workflow could look like this:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Identify the sample data used by a page.&lt;/li&gt;
&lt;li&gt;Replace the mock data with data from your backend.&lt;/li&gt;
&lt;li&gt;Connect the appropriate actions to your API.&lt;/li&gt;
&lt;li&gt;Adapt the fields and validation rules to your application.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The template provides the frontend foundation. Your backend, authentication, database, and business logic remain yours to implement.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Vanilla JavaScript?
&lt;/h2&gt;

&lt;p&gt;Modern frontend frameworks are excellent tools. But not every dashboard needs another framework or a complex build process.&lt;/p&gt;

&lt;p&gt;I chose vanilla JavaScript for three practical reasons.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Simplicity:&lt;/strong&gt; You can explore the code without first learning a framework-specific architecture.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Portability:&lt;/strong&gt; The HTML, CSS, and JavaScript can serve as a starting point for projects built with Laravel, PHP, Django, Rails, or other backend technologies that render HTML.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Control:&lt;/strong&gt; You can customize the interface without depending on a particular frontend framework or component library.&lt;/p&gt;

&lt;p&gt;NEXORA also uses Tailwind CSS through a CDN, along with Chart.js and Lucide icons. Those external libraries require an internet connection in the default configuration.&lt;/p&gt;

&lt;p&gt;The objective isn't to replace every frontend stack. It's to provide a straightforward option when you want a reusable dashboard without a full frontend build setup.&lt;/p&gt;

&lt;h2&gt;
  
  
  Small Details Make a Difference
&lt;/h2&gt;

&lt;p&gt;The difference between a basic template and a useful development starting point often comes down to the details.&lt;/p&gt;

&lt;p&gt;NEXORA includes features such as:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Skeleton loading states for dashboard metrics.&lt;/li&gt;
&lt;li&gt;Global search with a &lt;code&gt;Ctrl+K&lt;/code&gt; keyboard shortcut.&lt;/li&gt;
&lt;li&gt;Visible keyboard focus indicators.&lt;/li&gt;
&lt;li&gt;Empty states with useful actions.&lt;/li&gt;
&lt;li&gt;Toast notifications that stack and dismiss automatically.&lt;/li&gt;
&lt;li&gt;Comfortable and compact density options.&lt;/li&gt;
&lt;li&gt;Semantic HTML and ARIA labels for interactive elements.&lt;/li&gt;
&lt;li&gt;Reduced-motion considerations.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These details are easy to overlook when building a dashboard from scratch, yet they contribute to a more complete interface.&lt;/p&gt;

&lt;h2&gt;
  
  
  Who Is NEXORA For?
&lt;/h2&gt;

&lt;p&gt;NEXORA can be a useful starting point for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Developers building a SaaS MVP.&lt;/li&gt;
&lt;li&gt;Freelancers creating client dashboards.&lt;/li&gt;
&lt;li&gt;Indie hackers validating a new product.&lt;/li&gt;
&lt;li&gt;Developers building internal business tools.&lt;/li&gt;
&lt;li&gt;Teams that need a reusable admin interface.&lt;/li&gt;
&lt;li&gt;Designers and developers who want to customize an existing UI.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You can adapt the interface to your own branding, connect your data sources, and build the application-specific functionality around it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Get NEXORA
&lt;/h2&gt;

&lt;p&gt;If you're tired of rebuilding the same dashboard components for every project, NEXORA gives you a reusable foundation to start from.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Explore the live demo first:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://nexora-saas-dashboard-template.vercel.app/" rel="noopener noreferrer"&gt;https://nexora-saas-dashboard-template.vercel.app/&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Get NEXORA on Gumroad:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://boukataya.gumroad.com/l/qydmik" rel="noopener noreferrer"&gt;https://boukataya.gumroad.com/l/qydmik&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Your purchase includes the dashboard source files, centralized styling, JavaScript application logic, and documentation.&lt;/p&gt;

&lt;p&gt;NEXORA comes with a single-seat commercial license for personal and client projects. You can customize it and integrate it into your applications, but you cannot redistribute or resell the original template as a standalone product.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Please note:&lt;/strong&gt; NEXORA is a frontend UI kit, not a complete SaaS application. Backend integration, authentication, database connectivity, and payment processing must be implemented separately.&lt;/p&gt;

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

&lt;p&gt;I built NEXORA to solve a repetitive problem: spending valuable development time recreating dashboard interfaces instead of building the features that make an application different.&lt;/p&gt;

&lt;p&gt;A reusable UI kit won't eliminate every development task, but it can give you a more structured place to begin.&lt;/p&gt;

&lt;p&gt;If you're working on your next SaaS project, take a look at the demo and see whether NEXORA fits your workflow.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Live demo:&lt;/strong&gt; &lt;a href="https://nexora-saas-dashboard-template.vercel.app/" rel="noopener noreferrer"&gt;https://nexora-saas-dashboard-template.vercel.app/&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Get the UI kit:&lt;/strong&gt; &lt;a href="https://boukataya.gumroad.com/l/qydmik" rel="noopener noreferrer"&gt;https://boukataya.gumroad.com/l/qydmik&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Build faster. Customize freely. Spend more time on what makes your product unique.&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Tags: Web Development · SaaS · HTML · Tailwind CSS · JavaScript · Dashboard UI · Developer Tools&lt;/em&gt;&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>productivity</category>
      <category>saas</category>
      <category>programming</category>
    </item>
    <item>
      <title>I Built AssetOps Images: A Full Laravel + React App That Optimizes 2,000 Images in Batches</title>
      <dc:creator>Mohamed Sadok</dc:creator>
      <pubDate>Mon, 12 Oct 2026 08:49:58 +0000</pubDate>
      <link>https://dev.to/mohamed_sadok_42edff3a1b3/i-built-assetops-images-a-full-laravel-react-app-that-optimizes-2000-images-in-batches-4204</link>
      <guid>https://dev.to/mohamed_sadok_42edff3a1b3/i-built-assetops-images-a-full-laravel-react-app-that-optimizes-2000-images-in-batches-4204</guid>
      <description>&lt;p&gt;WebP and AVIF conversion, image resizing, metadata removal, SEO-friendly filenames, background jobs, usage tracking, and an admin dashboard. Here’s how I put the pieces together.&lt;/p&gt;

&lt;p&gt;Imagine receiving a folder containing 1,500 product images.&lt;/p&gt;

&lt;p&gt;They need different dimensions, smaller file sizes, SEO-friendly filenames, and no sensitive GPS metadata. Once everything is processed, the files need to be organized, packaged, and delivered to the client.&lt;/p&gt;

&lt;p&gt;You could handle everything manually. You could write a collection of scripts. Or you could build a proper application that handles the entire workflow.&lt;/p&gt;

&lt;p&gt;I chose the third option.&lt;/p&gt;

&lt;p&gt;Meet AssetOps Images — a complete, self-hosted image optimization web application built with Laravel 13, React 18, and TypeScript.&lt;/p&gt;

&lt;p&gt;It started with a straightforward problem: image processing is easy until you need to do it consistently, at scale, and without losing track of what happened to every file.&lt;/p&gt;

&lt;p&gt;The interesting part wasn’t converting JPEGs to WebP. It was building everything around that process.&lt;/p&gt;

&lt;p&gt;Want to explore the finished product?&lt;/p&gt;

&lt;p&gt;Get AssetOps Images on Gumroad&lt;/p&gt;

&lt;p&gt;The real problem isn’t resizing images&lt;br&gt;
Resizing an image takes a few lines of code. Building a reliable batch-processing workflow is a different challenge.&lt;/p&gt;

&lt;p&gt;A useful image optimization platform needs to:&lt;/p&gt;

&lt;p&gt;Accept large batches of images in different formats.&lt;br&gt;
Analyze dimensions, orientation, EXIF data, GPS information, and color profiles.&lt;br&gt;
Apply reusable processing configurations.&lt;br&gt;
Resize images without unnecessarily upscaling them.&lt;br&gt;
Convert images to WebP, AVIF, JPG, or PNG.&lt;br&gt;
Remove sensitive metadata.&lt;br&gt;
Generate consistent, SEO-friendly filenames.&lt;br&gt;
Package the results into downloadable ZIP archives.&lt;br&gt;
Track usage and enforce configurable quotas.&lt;br&gt;
Handle failures without forcing users to restart an entire batch.&lt;br&gt;
And there’s one more requirement: the application must remain responsive while all this work happens.&lt;/p&gt;

&lt;p&gt;That last point influenced almost every architectural decision.&lt;/p&gt;

&lt;p&gt;The stack behind AssetOps Images&lt;br&gt;
I wanted a clear separation between the frontend, backend, and image-processing infrastructure.&lt;/p&gt;

&lt;p&gt;LayerTechnologyBackendLaravel 13, PHP 8.3+FrontendReact 18, TypeScriptStylingTailwind CSSRouting and stateReact Router, ZustandDatabasePostgreSQL 16Background jobsRedis and Laravel queuesAuthenticationLaravel SanctumImage processingIntervention ImageDevelopment environmentDocker / DDEVBuild toolingVite&lt;/p&gt;

&lt;p&gt;The frontend is a single-page application, while Laravel handles the API, authentication, business logic, and static frontend delivery.&lt;/p&gt;

&lt;p&gt;The API exposes more than 60 documented endpoints, with API Resources providing consistent response structures.&lt;/p&gt;

&lt;p&gt;The result is a straightforward deployment model: one application, clearly separated responsibilities, and infrastructure that can be configured without rewriting the core logic.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Background jobs: the foundation of the entire workflow
This was one of the most important decisions.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Imagine processing 2,000 images inside a single HTTP request. Even if it works locally, you’re creating an unnecessary risk of timeouts, memory pressure, and failed operations.&lt;/p&gt;

&lt;p&gt;Instead, AssetOps splits the workflow into three stages.&lt;/p&gt;

&lt;p&gt;Stage 1 — Analyze the files&lt;/p&gt;

&lt;p&gt;The AnalyzeBatchFiles job inspects the uploaded images, extracts relevant metadata, detects GPS information, and generates thumbnails for the interface.&lt;/p&gt;

&lt;p&gt;Stage 2 — Transform the images&lt;/p&gt;

&lt;p&gt;The TransformBatchFiles job applies the selected settings: resizing, format conversion, quality adjustments, and metadata removal.&lt;/p&gt;

&lt;p&gt;Stage 3 — Package the results&lt;/p&gt;

&lt;p&gt;The PackageBatch job creates a ZIP archive containing the processed images, a metadata.csv manifest, and a processing-report.json file.&lt;/p&gt;

&lt;p&gt;Processing is divided into chunks of 25 files per job. A batch of 2,000 images can therefore be distributed across approximately 80 manageable chunks rather than handled by one oversized job.&lt;/p&gt;

&lt;p&gt;That provides a more practical foundation for retries, error isolation, and per-file failure reporting.&lt;/p&gt;

&lt;p&gt;Users can retry individual files instead of starting the entire process again.&lt;/p&gt;

&lt;p&gt;The takeaway: queues aren’t just a performance optimization. For workloads like this, they’re part of the product design.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Upload validation: never trust the browser
A file extension doesn’t prove what a file actually contains.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;A file named product.jpg isn't necessarily a JPEG. Likewise, a client-supplied MIME type isn't a security guarantee.&lt;/p&gt;

&lt;p&gt;AssetOps validates uploaded files using content inspection with PHP’s finfo, rather than relying solely on information supplied by the browser.&lt;/p&gt;

&lt;p&gt;The application supports JPG, PNG, WebP, GIF, AVIF, HEIC, and TIFF inputs, subject to the capabilities of the configured processing environment.&lt;/p&gt;

&lt;p&gt;This is a small implementation detail, but small details like this matter when you’re accepting files from users.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Make infrastructure configurable, not hard-coded
A self-hosted application should be usable in different environments without requiring developers to modify application code for every deployment.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;AssetOps uses configurable image-processing and storage options.&lt;/p&gt;

&lt;p&gt;For image processing, Intervention Image provides a common interface for supported drivers such as GD and libvips.&lt;/p&gt;

&lt;p&gt;For storage, the application can use local storage or an S3-compatible configuration.&lt;/p&gt;

&lt;p&gt;The principle is simple: deployment-specific decisions belong in configuration wherever practical.&lt;/p&gt;

&lt;p&gt;A small deployment shouldn’t need the same infrastructure as a high-volume production environment. Keeping these concerns configurable makes the application easier to adapt.&lt;/p&gt;

&lt;p&gt;Of course, actual performance depends on the server, image sizes, available memory, and selected processing driver.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;SEO-friendly filenames without the manual cleanup
Image optimization isn’t only about file size.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;For e-commerce stores, descriptive filenames also help keep product assets organized and provide useful context for search engines.&lt;/p&gt;

&lt;p&gt;AssetOps supports customizable filename templates, for example:&lt;/p&gt;

&lt;p&gt;{brand}-{product}-{color}-{index}&lt;/p&gt;

&lt;p&gt;A filename might become:&lt;/p&gt;

&lt;p&gt;acme-running-shoes-black-001.webp&lt;/p&gt;

&lt;p&gt;A dedicated filename-planning service handles slug generation and collision-safe indexing to avoid duplicate names within the generated output.&lt;/p&gt;

&lt;p&gt;Users can also rename individual files or make bulk changes through the interface.&lt;/p&gt;

&lt;p&gt;Become a Medium member&lt;br&gt;
It’s a relatively small feature, but it eliminates a tedious part of preparing large product catalogs.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Privacy, secure downloads, and usage tracking
There are three additional concerns that are easy to overlook when building image-processing software.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Metadata privacy&lt;/p&gt;

&lt;p&gt;Images can contain more information than their visible pixels. GPS coordinates and other EXIF, IPTC, or XMP metadata may reveal details users don’t intend to publish.&lt;/p&gt;

&lt;p&gt;AssetOps provides metadata-removal options to help reduce that risk. The resulting files should still be verified against the application’s configured stripping behavior and the desired privacy requirements.&lt;/p&gt;

&lt;p&gt;Secure downloads&lt;/p&gt;

&lt;p&gt;Processed files are delivered through signed, expiring download URLs rather than relying on permanently public file links.&lt;/p&gt;

&lt;p&gt;The configured expiration limits how long a generated download link remains valid.&lt;/p&gt;

&lt;p&gt;Usage tracking&lt;/p&gt;

&lt;p&gt;The application records processing activity and tracks usage against configurable limits.&lt;/p&gt;

&lt;p&gt;Users can review usage statistics and savings, while administrators can manage quotas and monitor platform activity.&lt;/p&gt;

&lt;p&gt;Together, these features help turn an image-processing utility into a more complete application.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The admin dashboard: the difference between a script and a product
A working processing pipeline is only part of the job.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;AssetOps also includes:&lt;/p&gt;

&lt;p&gt;User registration, authentication, and password recovery.&lt;br&gt;
Projects for organizing processing work.&lt;br&gt;
Reusable presets for common optimization settings.&lt;br&gt;
Batch and per-file management.&lt;br&gt;
Usage statistics and configurable quotas.&lt;br&gt;
Account settings and account deletion.&lt;br&gt;
Administrative user management and quota overrides.&lt;br&gt;
Failed-job monitoring.&lt;br&gt;
Gumroad license activation.&lt;br&gt;
Optional AI-generated ALT-text suggestions through an OpenAI integration.&lt;br&gt;
None of these features is particularly exciting in isolation. But without them, users are left with a processing engine instead of a usable application.&lt;/p&gt;

&lt;p&gt;The dashboard, error states, download workflow, and administration tools are what connect the underlying services into a complete user experience.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Thirty-two automated tests — and why they matter
The backend includes 32 automated tests covering important application workflows, including:&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Authentication.&lt;br&gt;
Batch upload, processing, and download.&lt;br&gt;
File-content validation.&lt;br&gt;
Quota enforcement.&lt;br&gt;
Projects and presets.&lt;br&gt;
License activation and deactivation.&lt;br&gt;
Download-token expiration.&lt;br&gt;
SEO filename generation.&lt;br&gt;
The frontend is built using strict TypeScript settings, with type checking included in the build process.&lt;/p&gt;

&lt;p&gt;Tests don’t prove that every deployment scenario is flawless. They do provide a repeatable way to verify key behaviors and reduce the risk of breaking existing functionality during future development.&lt;/p&gt;

&lt;p&gt;When evaluating a codebase, I’d rather see meaningful tests around the core workflows than a long feature list with no verification behind it.&lt;/p&gt;

&lt;p&gt;What I learned building AssetOps Images&lt;br&gt;
A few principles stood out during development.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Design for failure, not just the happy path.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Large batches will encounter invalid files, processing errors, and interrupted jobs. Independent jobs and retryable operations make those failures easier to manage.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Keep deployment choices configurable.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Storage drivers, processing engines, and other infrastructure settings shouldn’t require unnecessary code changes.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Treat security as part of the workflow.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Upload validation, authorization, quotas, and expiring downloads aren’t optional extras when users are uploading and processing files.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Don’t stop when the core feature works.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Authentication, projects, presets, administration, usage reporting, and useful error messages are what make the application practical to use.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Build the entire workflow, not just the impressive part.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Converting an image is easy. Getting thousands of images from upload to a reliable, organized download is the actual engineering challenge.&lt;/p&gt;

&lt;p&gt;Want the complete source code?&lt;br&gt;
AssetOps Images is available as a complete source-code product.&lt;/p&gt;

&lt;p&gt;The package includes the Laravel backend, React and TypeScript frontend, database migrations, seeders, demo data, development setup instructions, API documentation, and a launch-and-test guide.&lt;/p&gt;

&lt;p&gt;It’s intended for developers, agencies, e-commerce teams, and builders who want to explore, customize, or deploy a self-hosted image optimization application instead of starting from an empty repository.&lt;/p&gt;

&lt;p&gt;Get AssetOps Images here:&lt;/p&gt;

&lt;p&gt;→ View AssetOps Images on Gumroad&lt;br&gt;
If you’re building a Laravel application that handles uploads, background processing, or large batches of files, I’d also be interested to hear how you’re approaching job retries, storage, and processing limits.&lt;/p&gt;

&lt;p&gt;Tags: Laravel, PHP, React, TypeScript, Image Optimization, WebP, AVIF, Software Architecture, Side Projects, SaaS Development&lt;/p&gt;

</description>
      <category>laravel</category>
      <category>ai</category>
      <category>image</category>
      <category>react</category>
    </item>
    <item>
      <title>Animations Play. Interactions Respond.</title>
      <dc:creator>Mohamed Sadok</dc:creator>
      <pubDate>Mon, 05 Oct 2026 15:25:03 +0000</pubDate>
      <link>https://dev.to/mohamed_sadok_42edff3a1b3/animations-play-interactions-respond-4h70</link>
      <guid>https://dev.to/mohamed_sadok_42edff3a1b3/animations-play-interactions-respond-4h70</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ftbgre21af67cw7lo1lzg.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ftbgre21af67cw7lo1lzg.png" alt=" " width="800" height="381"&gt;&lt;/a&gt;&lt;/p&gt;

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

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

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

&lt;p&gt;&lt;em&gt;I kept rebuilding the same web effects for different projects. Eventually, I stopped rebuilding them and started building a library.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;There is a folder on my machine where most of my motion experiments go to die.&lt;/p&gt;

&lt;p&gt;Magnetic buttons.&lt;br&gt;
Distorted images.&lt;br&gt;
Liquid text.&lt;br&gt;
Particle fields.&lt;br&gt;
Scroll effects.&lt;br&gt;
Shader experiments.&lt;/p&gt;

&lt;p&gt;Some were useful. Some were terrible. Some looked impressive for about five minutes.&lt;/p&gt;

&lt;p&gt;And some made me realize something:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Animations play. Interactions respond.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That distinction became the foundation for &lt;strong&gt;LIQUID REALITY&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Instead of building another collection of fade-ins, slide-ups and hover animations, I wanted to explore interactions that actually react to the person using the website.&lt;/p&gt;

&lt;p&gt;Move the cursor and something changes.&lt;/p&gt;

&lt;p&gt;Scroll faster and the interface responds differently.&lt;/p&gt;

&lt;p&gt;Touch an image and it distorts.&lt;/p&gt;

&lt;p&gt;Move toward a button and it pulls back.&lt;/p&gt;

&lt;p&gt;Drag an object and it behaves like something with weight.&lt;/p&gt;

&lt;p&gt;The interface isn't simply performing an animation.&lt;/p&gt;

&lt;p&gt;It is reacting to you.&lt;/p&gt;


&lt;h2&gt;
  
  
  The problem with most "premium" animation
&lt;/h2&gt;

&lt;p&gt;Modern websites have become very good at motion.&lt;/p&gt;

&lt;p&gt;Almost too good.&lt;/p&gt;

&lt;p&gt;You can add a scroll library, write a few GSAP timelines and suddenly everything moves.&lt;/p&gt;

&lt;p&gt;Elements fade in.&lt;/p&gt;

&lt;p&gt;Headlines slide upward.&lt;/p&gt;

&lt;p&gt;Cards scale slightly.&lt;/p&gt;

&lt;p&gt;Images reveal themselves.&lt;/p&gt;

&lt;p&gt;The problem isn't that these techniques are bad.&lt;/p&gt;

&lt;p&gt;They're useful.&lt;/p&gt;

&lt;p&gt;They're just predictable.&lt;/p&gt;

&lt;p&gt;After seeing hundreds of websites using the same patterns, users stop consciously noticing them.&lt;/p&gt;

&lt;p&gt;I wanted to build interactions that were harder to ignore.&lt;/p&gt;

&lt;p&gt;Not because they were louder.&lt;/p&gt;

&lt;p&gt;Because they were &lt;strong&gt;responsive&lt;/strong&gt;.&lt;/p&gt;


&lt;h2&gt;
  
  
  1. Input is easy. Making it perceptible is the hard part.
&lt;/h2&gt;

&lt;p&gt;A pointer position is easy to capture.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;x&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;clientX&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;y&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;clientY&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's not interesting.&lt;/p&gt;

&lt;p&gt;What happens with that information is where the interaction begins.&lt;/p&gt;

&lt;p&gt;For example, pointer velocity can control multiple properties simultaneously.&lt;/p&gt;

&lt;p&gt;A fast movement can increase distortion while also increasing the intensity of a visual effect.&lt;/p&gt;

&lt;p&gt;A slow movement can make the same interface settle back into a calmer state.&lt;/p&gt;

&lt;p&gt;The code isn't necessarily complicated.&lt;/p&gt;

&lt;p&gt;The important part is the relationship between the input and the visual response.&lt;/p&gt;

&lt;p&gt;That relationship is what makes an interface feel intelligent.&lt;/p&gt;




&lt;h2&gt;
  
  
  2. Magnetic buttons need more than &lt;code&gt;translate()&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;Magnetic buttons are everywhere.&lt;/p&gt;

&lt;p&gt;Most implementations are essentially:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nx"&gt;transform&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;translate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;pointerX&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mf"&gt;0.3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;pointerY&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mf"&gt;0.3&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It works.&lt;/p&gt;

&lt;p&gt;But it doesn't necessarily feel good.&lt;/p&gt;

&lt;p&gt;The difference comes from introducing different physical behaviours.&lt;/p&gt;

&lt;p&gt;The button shell can respond quickly.&lt;/p&gt;

&lt;p&gt;The label can follow more slowly.&lt;/p&gt;

&lt;p&gt;The two can settle at different speeds.&lt;/p&gt;

&lt;p&gt;Now the button doesn't just move.&lt;/p&gt;

&lt;p&gt;It has &lt;strong&gt;weight&lt;/strong&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;shell&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;Spring2&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;210&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;17&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;text&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;Spring2&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;90&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;12&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="nx"&gt;shell&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;to&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;x&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;strength&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;y&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;strength&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;to&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;x&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;strength&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mf"&gt;0.55&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;y&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;strength&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mf"&gt;0.55&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Add a little skew based on movement and the interaction starts feeling less like CSS and more like a physical object.&lt;/p&gt;

&lt;p&gt;That's the type of detail I wanted throughout the library.&lt;/p&gt;




&lt;h2&gt;
  
  
  3. The cursor should be an input system, not an animation trigger
&lt;/h2&gt;

&lt;p&gt;Once you start building interactive effects, you quickly run into a problem.&lt;/p&gt;

&lt;p&gt;Every component wants to listen to the cursor.&lt;/p&gt;

&lt;p&gt;The hero wants it.&lt;/p&gt;

&lt;p&gt;The button wants it.&lt;/p&gt;

&lt;p&gt;The image wants it.&lt;/p&gt;

&lt;p&gt;The particle field wants it.&lt;/p&gt;

&lt;p&gt;The shader wants it.&lt;/p&gt;

&lt;p&gt;You can easily end up with dozens of listeners and animation loops doing almost the same work.&lt;/p&gt;

&lt;p&gt;So I started treating pointer data as a shared input system.&lt;/p&gt;

&lt;p&gt;One pointer listener.&lt;/p&gt;

&lt;p&gt;One animation loop.&lt;/p&gt;

&lt;p&gt;Shared mutable values.&lt;/p&gt;

&lt;p&gt;Effects consume the data they need.&lt;/p&gt;

&lt;p&gt;That means the interaction architecture can stay simple even when the page becomes visually complex.&lt;/p&gt;

&lt;p&gt;The same pointer can drive:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Position&lt;/li&gt;
&lt;li&gt;Velocity&lt;/li&gt;
&lt;li&gt;Distance&lt;/li&gt;
&lt;li&gt;Direction&lt;/li&gt;
&lt;li&gt;Distortion&lt;/li&gt;
&lt;li&gt;Magnetic force&lt;/li&gt;
&lt;li&gt;Particle movement&lt;/li&gt;
&lt;li&gt;Shader uniforms&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;One input.&lt;/p&gt;

&lt;p&gt;Multiple responses.&lt;/p&gt;




&lt;h2&gt;
  
  
  4. One shader can become many experiences
&lt;/h2&gt;

&lt;p&gt;One of my favourite parts of building LIQUID REALITY was experimenting with shaders.&lt;/p&gt;

&lt;p&gt;A basic image distortion effect doesn't need an enormous rendering system.&lt;/p&gt;

&lt;p&gt;You can start with a texture, a plane and a fragment shader.&lt;/p&gt;

&lt;p&gt;Then expose a few useful parameters:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;uTexture
uPointer
uVelocity
uProgress
uIntensity
uRadius
uDetail
uChroma
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;From there, completely different experiences can emerge.&lt;/p&gt;

&lt;p&gt;Liquid distortion.&lt;/p&gt;

&lt;p&gt;Ripple.&lt;/p&gt;

&lt;p&gt;Melt.&lt;/p&gt;

&lt;p&gt;Chromatic displacement.&lt;/p&gt;

&lt;p&gt;Refraction.&lt;/p&gt;

&lt;p&gt;Fragmentation.&lt;/p&gt;

&lt;p&gt;Glass-like distortion.&lt;/p&gt;

&lt;p&gt;The interesting part isn't using GLSL because GLSL sounds impressive.&lt;/p&gt;

&lt;p&gt;The interesting part is using the GPU to create behaviour that would be difficult or expensive to reproduce with ordinary DOM animation.&lt;/p&gt;

&lt;p&gt;That's the rule I kept coming back to:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;If the shader can only be described by the technique used to build it, it probably isn't interesting enough.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;"Voronoi experiment" isn't a product feature.&lt;/p&gt;

&lt;p&gt;"Image fragmentation" is.&lt;/p&gt;

&lt;p&gt;The technology is underneath.&lt;/p&gt;

&lt;p&gt;The interaction is what the user experiences.&lt;/p&gt;




&lt;h2&gt;
  
  
  5. Motion should never become a usability requirement
&lt;/h2&gt;

&lt;p&gt;There is a trap with interactive websites.&lt;/p&gt;

&lt;p&gt;You can become so focused on the visual experience that you forget the website still has to work.&lt;/p&gt;

&lt;p&gt;That's why reduced motion became part of the design process rather than something added at the end.&lt;/p&gt;

&lt;p&gt;If a user prefers reduced motion, the page should not simply stop halfway through an animation.&lt;/p&gt;

&lt;p&gt;The interface should resolve into its usable state.&lt;/p&gt;

&lt;p&gt;Motion can disappear.&lt;/p&gt;

&lt;p&gt;Content shouldn't.&lt;/p&gt;

&lt;p&gt;For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;intensity&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;reducedMotion&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;intensity&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And instead of leaving an element hidden because its reveal animation never happened, its final state should be applied directly.&lt;/p&gt;

&lt;p&gt;The principle is simple:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Motion enhances the interface. Motion should never be the interface.&lt;/strong&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  6. Mobile isn't desktop with less space
&lt;/h2&gt;

&lt;p&gt;Interactive effects become particularly interesting on mobile.&lt;/p&gt;

&lt;p&gt;A cursor doesn't exist.&lt;/p&gt;

&lt;p&gt;Hover doesn't work in the same way.&lt;/p&gt;

&lt;p&gt;Performance constraints are different.&lt;/p&gt;

&lt;p&gt;So instead of treating mobile as a smaller desktop, I started thinking about different inputs.&lt;/p&gt;

&lt;p&gt;Touch can replace pointer interaction.&lt;/p&gt;

&lt;p&gt;Device orientation can become an input.&lt;/p&gt;

&lt;p&gt;An effect can automatically drift when the user isn't interacting.&lt;/p&gt;

&lt;p&gt;For example, a background can react to device movement instead of cursor movement.&lt;/p&gt;

&lt;p&gt;The goal isn't to perfectly reproduce desktop.&lt;/p&gt;

&lt;p&gt;The goal is to preserve the &lt;strong&gt;feeling of interaction&lt;/strong&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  7. Performance is part of the interaction
&lt;/h2&gt;

&lt;p&gt;A beautiful effect that destroys the user's laptop isn't a premium interaction.&lt;/p&gt;

&lt;p&gt;It's a benchmark.&lt;/p&gt;

&lt;p&gt;That meant being deliberate about how the effects run.&lt;/p&gt;

&lt;p&gt;Avoid unnecessary layout reads inside animation loops.&lt;/p&gt;

&lt;p&gt;Measure elements when their dimensions change rather than repeatedly calling:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nf"&gt;getBoundingClientRect&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;during every frame.&lt;/p&gt;

&lt;p&gt;Cache what can be cached.&lt;/p&gt;

&lt;p&gt;Use transforms where possible.&lt;/p&gt;

&lt;p&gt;Pause expensive work when the effect isn't visible.&lt;/p&gt;

&lt;p&gt;Use &lt;code&gt;IntersectionObserver&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Limit pixel density when necessary.&lt;/p&gt;

&lt;p&gt;Clean up animation loops, listeners, textures and WebGL resources.&lt;/p&gt;

&lt;p&gt;And most importantly:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Don't animate something just because you can.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Performance is part of the design.&lt;/p&gt;




&lt;h1&gt;
  
  
  What I actually wanted to build
&lt;/h1&gt;

&lt;p&gt;After enough experiments, I realized I didn't want to make a giant animation framework.&lt;/p&gt;

&lt;p&gt;I wanted something closer to a &lt;strong&gt;box of interaction ideas&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Something a frontend developer could open when a normal implementation felt too ordinary.&lt;/p&gt;

&lt;p&gt;Need an image that reacts to the cursor?&lt;/p&gt;

&lt;p&gt;There's an interaction for that.&lt;/p&gt;

&lt;p&gt;Need a magnetic CTA?&lt;/p&gt;

&lt;p&gt;There's an interaction for that.&lt;/p&gt;

&lt;p&gt;Want a liquid visual?&lt;/p&gt;

&lt;p&gt;There's an interaction for that.&lt;/p&gt;

&lt;p&gt;Want a shader-based distortion?&lt;/p&gt;

&lt;p&gt;There's an interaction for that.&lt;/p&gt;

&lt;p&gt;Want to experiment with particles, scroll velocity, typography or WebGL?&lt;/p&gt;

&lt;p&gt;Same idea.&lt;/p&gt;

&lt;p&gt;Pick the interaction.&lt;/p&gt;

&lt;p&gt;Open the source.&lt;/p&gt;

&lt;p&gt;Understand it.&lt;/p&gt;

&lt;p&gt;Change it.&lt;/p&gt;

&lt;p&gt;Use it.&lt;/p&gt;




&lt;h1&gt;
  
  
  Why I called it LIQUID REALITY
&lt;/h1&gt;

&lt;p&gt;The name came from the feeling I was trying to create.&lt;/p&gt;

&lt;p&gt;Interfaces are normally rigid.&lt;/p&gt;

&lt;p&gt;Boxes.&lt;/p&gt;

&lt;p&gt;Cards.&lt;/p&gt;

&lt;p&gt;Buttons.&lt;/p&gt;

&lt;p&gt;Images.&lt;/p&gt;

&lt;p&gt;Text.&lt;/p&gt;

&lt;p&gt;Grids.&lt;/p&gt;

&lt;p&gt;LIQUID REALITY asks:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What if these things behaved less like pixels on a screen and more like physical materials?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;What if an image could ripple?&lt;/p&gt;

&lt;p&gt;What if typography could bend?&lt;/p&gt;

&lt;p&gt;What if a button could feel magnetic?&lt;/p&gt;

&lt;p&gt;What if a cursor could affect an entire visual field?&lt;/p&gt;

&lt;p&gt;What if scrolling changed the physical behaviour of the page?&lt;/p&gt;

&lt;p&gt;The goal isn't to make websites complicated.&lt;/p&gt;

&lt;p&gt;It's to make them &lt;strong&gt;responsive in unexpected ways&lt;/strong&gt;.&lt;/p&gt;




&lt;h1&gt;
  
  
  The result
&lt;/h1&gt;

&lt;p&gt;I packaged the experiments into a downloadable source product for developers.&lt;/p&gt;

&lt;p&gt;The focus is not on giving you another dependency-heavy animation package.&lt;/p&gt;

&lt;p&gt;It's on giving you reusable interaction implementations that you can inspect, modify and integrate into real projects.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;React. TypeScript. GSAP. WebGL. GLSL. Canvas.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Use one effect.&lt;/p&gt;

&lt;p&gt;Combine several.&lt;/p&gt;

&lt;p&gt;Change the parameters.&lt;/p&gt;

&lt;p&gt;Replace the assets.&lt;/p&gt;

&lt;p&gt;Rewrite the shader.&lt;/p&gt;

&lt;p&gt;Take the idea somewhere completely different.&lt;/p&gt;

&lt;p&gt;That's the whole point.&lt;/p&gt;




&lt;h1&gt;
  
  
  If I were building a website today
&lt;/h1&gt;

&lt;p&gt;I wouldn't start by asking:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;"What animation should this section have?"&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;I'd ask:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;"What should this section respond to?"&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Cursor?&lt;/p&gt;

&lt;p&gt;Scroll?&lt;/p&gt;

&lt;p&gt;Velocity?&lt;/p&gt;

&lt;p&gt;Touch?&lt;/p&gt;

&lt;p&gt;Proximity?&lt;/p&gt;

&lt;p&gt;Drag?&lt;/p&gt;

&lt;p&gt;Time?&lt;/p&gt;

&lt;p&gt;Once you define the input, the animation becomes much more interesting.&lt;/p&gt;

&lt;p&gt;That's the shift I'm interested in.&lt;/p&gt;

&lt;p&gt;From:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Animation → Interaction&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;From:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Timeline → Response&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;From:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Element moves → Element reacts&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;And ultimately:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;A website shouldn't just show motion. It should feel like it is listening.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h1&gt;
  
  
  LIQUID REALITY
&lt;/h1&gt;

&lt;p&gt;I built LIQUID REALITY as a collection of these experiments — interactive effects designed for developers who want their websites to feel less static and more alive.&lt;/p&gt;

&lt;p&gt;You can explore the live demo here:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://liquid-reality.vercel.app/" rel="noopener noreferrer"&gt;https://liquid-reality.vercel.app/&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;And if you want the source and want to experiment with the interactions yourself:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;LIQUID REALITY is available on Gumroad.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://boukataya.gumroad.com/l/ygshkz" rel="noopener noreferrer"&gt;https://boukataya.gumroad.com/l/ygshkz&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Build something interesting with it.&lt;/p&gt;

&lt;p&gt;Then break it and make it better.&lt;/p&gt;

</description>
      <category>programming</category>
      <category>javascript</category>
      <category>opensource</category>
      <category>react</category>
    </item>
    <item>
      <title>How I Built 50 Web Interactions Without Turning My React App Into a Performance Nightmare</title>
      <dc:creator>Mohamed Sadok</dc:creator>
      <pubDate>Fri, 02 Oct 2026 13:51:48 +0000</pubDate>
      <link>https://dev.to/mohamed_sadok_42edff3a1b3/how-i-built-50-web-interactions-without-turning-my-react-app-into-a-performance-nightmare-36b</link>
      <guid>https://dev.to/mohamed_sadok_42edff3a1b3/how-i-built-50-web-interactions-without-turning-my-react-app-into-a-performance-nightmare-36b</guid>
      <description>&lt;h1&gt;
  
  
  How I Built 50 Web Interactions Without Turning My React App Into a Performance Nightmare
&lt;/h1&gt;

&lt;p&gt;I kept rebuilding the same small interaction ideas for different projects.&lt;/p&gt;

&lt;p&gt;A magnetic button.&lt;/p&gt;

&lt;p&gt;An image that reacts to the cursor.&lt;/p&gt;

&lt;p&gt;A menu transition.&lt;/p&gt;

&lt;p&gt;A scroll-controlled composition.&lt;/p&gt;

&lt;p&gt;A little physics experiment for typography.&lt;/p&gt;

&lt;p&gt;The individual effects were fun. The problem was that each project ended up with its own component, its own assumptions, and its own implementation details.&lt;/p&gt;

&lt;p&gt;Eventually I realized I wasn't building new interactions.&lt;/p&gt;

&lt;p&gt;I was rebuilding the same ones.&lt;/p&gt;

&lt;p&gt;So I decided to build a proper system for them.&lt;/p&gt;

&lt;p&gt;That became &lt;strong&gt;MOTIONFORGE&lt;/strong&gt;: 50 interaction experiments built with React + TypeScript, organized across ten categories.&lt;/p&gt;

&lt;p&gt;You can play with the live collection here:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://motionforge-premium.vercel.app/" rel="noopener noreferrer"&gt;MOTIONFORGE — Live Demo&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The interesting part wasn't creating 50 animations.&lt;/p&gt;

&lt;p&gt;It was figuring out how to make them reusable without making them feel identical.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Goal
&lt;/h2&gt;

&lt;p&gt;I started with a simple rule:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Every interaction should have a reason to exist.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;I didn't want a collection of random "cool effects."&lt;/p&gt;

&lt;p&gt;Each experiment needed to answer:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;What is the user doing?&lt;/li&gt;
&lt;li&gt;How does the interface respond?&lt;/li&gt;
&lt;li&gt;What makes the response feel intentional?&lt;/li&gt;
&lt;li&gt;Which parameters actually matter?&lt;/li&gt;
&lt;li&gt;What happens on touch?&lt;/li&gt;
&lt;li&gt;What happens with reduced motion?&lt;/li&gt;
&lt;li&gt;Can this realistically be integrated into another project?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The final collection contains:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Hero effects&lt;/li&gt;
&lt;li&gt;Text reveals&lt;/li&gt;
&lt;li&gt;Magnetic buttons&lt;/li&gt;
&lt;li&gt;Cursor effects&lt;/li&gt;
&lt;li&gt;Image distortions&lt;/li&gt;
&lt;li&gt;Scroll animations&lt;/li&gt;
&lt;li&gt;Page transitions&lt;/li&gt;
&lt;li&gt;3D interactions&lt;/li&gt;
&lt;li&gt;Liquid effects&lt;/li&gt;
&lt;li&gt;Navigation effects&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Five experiments per category.&lt;/p&gt;

&lt;p&gt;50 total.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Architecture
&lt;/h2&gt;

&lt;p&gt;The main abstraction is an &lt;code&gt;InteractionStage&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Its job is to handle the things that shouldn't have to be implemented 50 times:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Configuration&lt;/li&gt;
&lt;li&gt;Pointer state&lt;/li&gt;
&lt;li&gt;Pointer interpolation&lt;/li&gt;
&lt;li&gt;Reduced-motion detection&lt;/li&gt;
&lt;li&gt;CSS variable updates&lt;/li&gt;
&lt;li&gt;Animation lifecycle&lt;/li&gt;
&lt;li&gt;Cleanup&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A simplified usage looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;InteractionStage&lt;/span&gt;
  &lt;span class="na"&gt;interaction&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nf"&gt;getInteraction&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;liquid-image&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
  &lt;span class="na"&gt;config&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;defaultConfig&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;distortion&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;68&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The important part is the separation.&lt;/p&gt;

&lt;p&gt;The stage handles &lt;strong&gt;shared infrastructure&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The interaction handles &lt;strong&gt;the actual visual behavior&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;That means a new experiment doesn't need to reinvent pointer handling, lifecycle management, or accessibility detection.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why I Didn't Store Pointer Position in React State
&lt;/h2&gt;

&lt;p&gt;One thing I wanted to avoid was turning every &lt;code&gt;pointermove&lt;/code&gt; event into a React render.&lt;/p&gt;

&lt;p&gt;For high-frequency interactions, that gets expensive quickly.&lt;/p&gt;

&lt;p&gt;Instead, pointer input updates an internal target position.&lt;/p&gt;

&lt;p&gt;A &lt;code&gt;requestAnimationFrame&lt;/code&gt; loop interpolates toward that target and exposes the result through CSS variables.&lt;/p&gt;

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

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;pointer event
     ↓
target position
     ↓
requestAnimationFrame
     ↓
interpolated position
     ↓
CSS variables
     ↓
visual interaction
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;React handles the component structure.&lt;/p&gt;

&lt;p&gt;The browser handles the high-frequency visual updates.&lt;/p&gt;

&lt;p&gt;That separation made the interactions much easier to reason about.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Explorer Doesn't Run 50 Animations
&lt;/h2&gt;

&lt;p&gt;This was another important decision.&lt;/p&gt;

&lt;p&gt;Imagine opening a page containing 50 interactive demos and actually mounting all 50 animation systems.&lt;/p&gt;

&lt;p&gt;It would work.&lt;/p&gt;

&lt;p&gt;It would also be a terrible idea.&lt;/p&gt;

&lt;p&gt;Instead, the explorer initially renders lightweight previews.&lt;/p&gt;

&lt;p&gt;The real interaction is mounted only when the card becomes relevant:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Hover&lt;/li&gt;
&lt;li&gt;Focus&lt;/li&gt;
&lt;li&gt;Tap&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So the index page can display a large collection without running 50 independent animation systems in the background.&lt;/p&gt;

&lt;p&gt;The same principle applies to Canvas-based interactions.&lt;/p&gt;

&lt;p&gt;If there is nothing left to animate, the loop stops.&lt;/p&gt;

&lt;p&gt;When the component unmounts:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Animation frames are cancelled&lt;/li&gt;
&lt;li&gt;Event listeners are removed&lt;/li&gt;
&lt;li&gt;Observers are disconnected&lt;/li&gt;
&lt;li&gt;Canvas resources are cleaned up&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The browser shouldn't keep working on an interaction nobody is looking at.&lt;/p&gt;




&lt;h2&gt;
  
  
  Choosing the Technology After the Interaction
&lt;/h2&gt;

&lt;p&gt;I also wanted to avoid the classic creative-coding trap:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"I need an animation, therefore I need a big animation library."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Not necessarily.&lt;/p&gt;

&lt;p&gt;Most interactions use browser primitives:&lt;/p&gt;

&lt;h3&gt;
  
  
  CSS
&lt;/h3&gt;

&lt;p&gt;Transforms, transitions, perspective, clipping and compositing.&lt;/p&gt;

&lt;h3&gt;
  
  
  SVG
&lt;/h3&gt;

&lt;p&gt;Useful for certain displacement and distortion effects.&lt;/p&gt;

&lt;h3&gt;
  
  
  Canvas
&lt;/h3&gt;

&lt;p&gt;Used when the interaction actually needs individual particles, trails or ripple simulations.&lt;/p&gt;

&lt;h3&gt;
  
  
  GSAP
&lt;/h3&gt;

&lt;p&gt;Used where a controlled timeline or complex choreography genuinely makes the implementation easier.&lt;/p&gt;

&lt;h3&gt;
  
  
  CSS 3D
&lt;/h3&gt;

&lt;p&gt;Several interactions create a convincing sense of depth without requiring WebGL or Three.js.&lt;/p&gt;

&lt;p&gt;That last one was intentional.&lt;/p&gt;

&lt;p&gt;If CSS perspective communicates the idea, introducing a WebGL runtime is unnecessary complexity.&lt;/p&gt;




&lt;h2&gt;
  
  
  Reduced Motion Wasn't an Afterthought
&lt;/h2&gt;

&lt;p&gt;This turned out to be one of the more interesting design constraints.&lt;/p&gt;

&lt;p&gt;An interaction shouldn't depend on its animation to communicate the content.&lt;/p&gt;

&lt;p&gt;If the movement disappears, the interface still needs to work.&lt;/p&gt;

&lt;p&gt;So reduced motion isn't simply:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="nt"&gt;animation&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="nt"&gt;none&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The component needs a meaningful static state.&lt;/p&gt;

&lt;p&gt;For example:&lt;/p&gt;

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

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;WORD → letters separate → letters rearrange → WORD
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Reduced motion:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;WORD
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The information survives.&lt;/p&gt;

&lt;p&gt;Only the unnecessary movement disappears.&lt;/p&gt;

&lt;p&gt;That approach also made the components easier to integrate into real products.&lt;/p&gt;




&lt;h2&gt;
  
  
  Hover Isn't a Mobile Interaction
&lt;/h2&gt;

&lt;p&gt;Cursor experiments created another problem.&lt;/p&gt;

&lt;p&gt;Desktop gives us a pointer.&lt;/p&gt;

&lt;p&gt;Touch doesn't.&lt;/p&gt;

&lt;p&gt;A hover interaction therefore can't simply be copied to mobile and called responsive.&lt;/p&gt;

&lt;p&gt;Depending on the interaction, I used:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Tap states&lt;/li&gt;
&lt;li&gt;Press states&lt;/li&gt;
&lt;li&gt;Static compositions&lt;/li&gt;
&lt;li&gt;Simplified transitions&lt;/li&gt;
&lt;li&gt;Reduced interaction intensity&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Sometimes the correct mobile implementation is not a smaller version of the desktop effect.&lt;/p&gt;

&lt;p&gt;It's a different interaction.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Difficult Part Was Timing
&lt;/h2&gt;

&lt;p&gt;Making something move is easy.&lt;/p&gt;

&lt;p&gt;Making it feel like it has &lt;strong&gt;weight&lt;/strong&gt; is harder.&lt;/p&gt;

&lt;p&gt;This became especially obvious with magnetic interactions.&lt;/p&gt;

&lt;p&gt;Move an element toward the cursor.&lt;/p&gt;

&lt;p&gt;Easy.&lt;/p&gt;

&lt;p&gt;Make it return naturally.&lt;/p&gt;

&lt;p&gt;Much harder.&lt;/p&gt;

&lt;p&gt;Return too quickly:&lt;/p&gt;

&lt;p&gt;It feels mechanical.&lt;/p&gt;

&lt;p&gt;Return too slowly:&lt;/p&gt;

&lt;p&gt;It feels broken.&lt;/p&gt;

&lt;p&gt;Add too much interpolation:&lt;/p&gt;

&lt;p&gt;It feels disconnected from the user's input.&lt;/p&gt;

&lt;p&gt;Remove the interpolation:&lt;/p&gt;

&lt;p&gt;It feels like a DOM element being dragged around.&lt;/p&gt;

&lt;p&gt;The interesting part of these interactions often lives in the return phase.&lt;/p&gt;

&lt;p&gt;The user doesn't consciously think about the easing curve.&lt;/p&gt;

&lt;p&gt;But they definitely notice when something feels wrong.&lt;/p&gt;




&lt;h2&gt;
  
  
  Reusable Doesn't Mean 20 Configuration Options
&lt;/h2&gt;

&lt;p&gt;Another problem I ran into was configuration.&lt;/p&gt;

&lt;p&gt;It's tempting to make everything configurable.&lt;/p&gt;

&lt;p&gt;Strength.&lt;/p&gt;

&lt;p&gt;Speed.&lt;/p&gt;

&lt;p&gt;Radius.&lt;/p&gt;

&lt;p&gt;Delay.&lt;/p&gt;

&lt;p&gt;Easing.&lt;/p&gt;

&lt;p&gt;Scale.&lt;/p&gt;

&lt;p&gt;Resistance.&lt;/p&gt;

&lt;p&gt;Decay.&lt;/p&gt;

&lt;p&gt;Noise.&lt;/p&gt;

&lt;p&gt;Randomness.&lt;/p&gt;

&lt;p&gt;Before long, you have 20 sliders and no idea what half of them actually do.&lt;/p&gt;

&lt;p&gt;I tried to expose parameters that map directly to the behavior.&lt;/p&gt;

&lt;p&gt;For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;strength → how strongly the element reacts

radius → how far the interaction reaches

distortion → how much the surface changes

return → how quickly it settles
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A configuration option should answer a question.&lt;/p&gt;

&lt;p&gt;If I can't explain what a parameter changes, it probably shouldn't be exposed.&lt;/p&gt;




&lt;h2&gt;
  
  
  Performance Is Part of the Design
&lt;/h2&gt;

&lt;p&gt;"60 FPS" is a useful target.&lt;/p&gt;

&lt;p&gt;But it doesn't mean much if the demo is running on an empty page.&lt;/p&gt;

&lt;p&gt;Real websites have:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Images&lt;/li&gt;
&lt;li&gt;Fonts&lt;/li&gt;
&lt;li&gt;Layout work&lt;/li&gt;
&lt;li&gt;Other JavaScript&lt;/li&gt;
&lt;li&gt;Analytics&lt;/li&gt;
&lt;li&gt;Third-party scripts&lt;/li&gt;
&lt;li&gt;Multiple components&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So I tried to keep each interaction reasonably isolated.&lt;/p&gt;

&lt;p&gt;Some of the decisions were simple:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Don't render through React unnecessarily.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Stop Canvas loops when there's nothing to draw.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Prefer transforms and opacity when appropriate.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Avoid expensive rendering when a browser primitive can do the job.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Clean everything up on unmount.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The goal isn't to make every interaction magically "60 FPS."&lt;/p&gt;

&lt;p&gt;The goal is to avoid wasting work.&lt;/p&gt;




&lt;h2&gt;
  
  
  What I Learned Building 50 of Them
&lt;/h2&gt;

&lt;p&gt;After building enough of these, a few patterns became obvious.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Start with the input
&lt;/h3&gt;

&lt;p&gt;Before thinking about the animation, decide what the user is doing.&lt;/p&gt;

&lt;p&gt;Hover?&lt;/p&gt;

&lt;p&gt;Move?&lt;/p&gt;

&lt;p&gt;Click?&lt;/p&gt;

&lt;p&gt;Scroll?&lt;/p&gt;

&lt;p&gt;Drag?&lt;/p&gt;

&lt;p&gt;Touch?&lt;/p&gt;

&lt;p&gt;The input determines the interaction.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Motion needs a rule
&lt;/h3&gt;

&lt;p&gt;If you can't describe why something moves, it's probably decoration.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. The return matters
&lt;/h3&gt;

&lt;p&gt;A believable interaction isn't just about the initial response.&lt;/p&gt;

&lt;p&gt;How the element settles back into its normal state often determines whether it feels physical.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. The simplest technology usually wins
&lt;/h3&gt;

&lt;p&gt;CSS can do a lot.&lt;/p&gt;

&lt;p&gt;SVG can do a lot.&lt;/p&gt;

&lt;p&gt;Canvas can do a lot.&lt;/p&gt;

&lt;p&gt;You don't automatically need WebGL because something looks "3D."&lt;/p&gt;

&lt;h3&gt;
  
  
  5. Accessibility changes the design
&lt;/h3&gt;

&lt;p&gt;Reduced motion and touch aren't annoying edge cases to solve at the end.&lt;/p&gt;

&lt;p&gt;They force you to understand what the interaction is actually communicating.&lt;/p&gt;




&lt;h2&gt;
  
  
  From 50 Experiments to a Reusable System
&lt;/h2&gt;

&lt;p&gt;The biggest change happened when I stopped asking:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"How do I build this effect?"&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;and started asking:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"How do I make this effect easy to reuse?"&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That changed the architecture.&lt;/p&gt;

&lt;p&gt;The shared layer handles:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Input&lt;/li&gt;
&lt;li&gt;Configuration&lt;/li&gt;
&lt;li&gt;Lifecycle&lt;/li&gt;
&lt;li&gt;Accessibility&lt;/li&gt;
&lt;li&gt;Cleanup&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The individual experiments handle:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Visual behavior&lt;/li&gt;
&lt;li&gt;Interaction rules&lt;/li&gt;
&lt;li&gt;Material&lt;/li&gt;
&lt;li&gt;Personality&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That separation makes experimentation much cheaper.&lt;/p&gt;

&lt;p&gt;And that's really what MOTIONFORGE became.&lt;/p&gt;

&lt;p&gt;Not a collection of animations.&lt;/p&gt;

&lt;p&gt;A system for experimenting with interaction.&lt;/p&gt;




&lt;h2&gt;
  
  
  Try It
&lt;/h2&gt;

&lt;p&gt;All 50 experiments are available in the live explorer.&lt;/p&gt;

&lt;p&gt;You can interact with them, browse the categories, and see how the ideas behave before deciding whether something is useful for your own project.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://motionforge-premium.vercel.app/" rel="noopener noreferrer"&gt;Explore MOTIONFORGE&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;If you want the complete collection and source:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://boukataya.gumroad.com/l/hjnfj" rel="noopener noreferrer"&gt;MOTIONFORGE on Gumroad&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Final Thought
&lt;/h2&gt;

&lt;p&gt;The hardest part wasn't making 50 things move.&lt;/p&gt;

&lt;p&gt;It was making 50 different interactions share infrastructure without making them feel like the same interaction repeated 50 times.&lt;/p&gt;

&lt;p&gt;The solution was to keep the boring parts consistent:&lt;/p&gt;

&lt;p&gt;Input.&lt;/p&gt;

&lt;p&gt;Lifecycle.&lt;/p&gt;

&lt;p&gt;Configuration.&lt;/p&gt;

&lt;p&gt;Accessibility.&lt;/p&gt;

&lt;p&gt;Cleanup.&lt;/p&gt;

&lt;p&gt;Then leave the creative layer free to experiment.&lt;/p&gt;

&lt;p&gt;That's probably the biggest lesson I took from the project:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Don't build a library of effects. Build a system that makes experimentation cheap.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;If you're building interaction-heavy interfaces, I'd be interested to know:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What's the hardest part for you to make reusable — input handling, animation, accessibility, performance, or integration?&lt;/strong&gt;&lt;/p&gt;

</description>
      <category>performance</category>
      <category>react</category>
      <category>showdev</category>
      <category>ui</category>
    </item>
    <item>
      <title>I Wanted a Website That Felt Alive. So I Built MOTIONFORGE.</title>
      <dc:creator>Mohamed Sadok</dc:creator>
      <pubDate>Thu, 01 Oct 2026 09:16:47 +0000</pubDate>
      <link>https://dev.to/mohamed_sadok_42edff3a1b3/i-wanted-a-website-that-felt-alive-so-i-built-motionforge-15d2</link>
      <guid>https://dev.to/mohamed_sadok_42edff3a1b3/i-wanted-a-website-that-felt-alive-so-i-built-motionforge-15d2</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fm5e45k28co398z6qr1n7.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fm5e45k28co398z6qr1n7.jpg" alt=" " width="800" height="800"&gt;&lt;/a&gt;&lt;br&gt;
Fifteen experiments in making the web less ordinary, without forgetting the people using it.&lt;br&gt;
There is a moment on a good interactive website when you stop scrolling on autopilot.&lt;/p&gt;

&lt;p&gt;Not because something shouted at you. Not because a modal interrupted you.&lt;/p&gt;

&lt;p&gt;Something simply responded in a way you did not expect.&lt;/p&gt;

&lt;p&gt;A word leaned toward your hand. An image opened into another world. A product came apart, and suddenly its construction made sense.&lt;/p&gt;

&lt;p&gt;For a second, the website stopped feeling like a stack of rectangles.&lt;/p&gt;

&lt;p&gt;It felt like a place.&lt;/p&gt;

&lt;p&gt;That moment is what I wanted to explore with MOTIONFORGE.&lt;/p&gt;

&lt;p&gt;The Problem Wasn’t a Lack of Animation&lt;br&gt;
We already have animation everywhere.&lt;/p&gt;

&lt;p&gt;Headings arrive. Cards lift. Buttons grow. Sections drift into view. Somewhere, a line of text is moving endlessly across the screen.&lt;/p&gt;

&lt;p&gt;None of that is inherently bad. These techniques solve real problems.&lt;/p&gt;

&lt;p&gt;But useful motion and memorable interaction are not always the same thing.&lt;/p&gt;

&lt;p&gt;The question I kept coming back to was more interesting:&lt;/p&gt;

&lt;p&gt;What would a motion library look like if the goal wasn’t to decorate a website, but to give it a physical character?&lt;/p&gt;

&lt;p&gt;I didn’t want another drawer full of interchangeable entrance animations.&lt;/p&gt;

&lt;p&gt;I wanted systems with behavior.&lt;/p&gt;

&lt;p&gt;Systems you could push, pull, interrupt, and reshape.&lt;/p&gt;

&lt;p&gt;Start With a Feeling. Then Choose the Technology.&lt;br&gt;
The first temptation in creative development is to reach for the biggest tool available.&lt;/p&gt;

&lt;p&gt;WebGL can look extraordinary. Three.js can build entire worlds. A shader can make a flat image appear to have material properties.&lt;/p&gt;

&lt;p&gt;But technology is not the idea.&lt;/p&gt;

&lt;p&gt;For MOTIONFORGE, the more useful question became:&lt;/p&gt;

&lt;p&gt;What is the simplest mechanism that can make this interaction feel convincing?&lt;/p&gt;

&lt;p&gt;Magnetic typography doesn’t need a 3D scene.&lt;/p&gt;

&lt;p&gt;It needs individual glyphs with positions, velocities, a spatial force field, and springs that know how to settle.&lt;/p&gt;

&lt;p&gt;A liquid button doesn’t need a game engine.&lt;/p&gt;

&lt;p&gt;It needs a deformable boundary, SVG turbulence, real displacement, and a stable native button underneath.&lt;/p&gt;

&lt;p&gt;A product explosion does need three dimensions.&lt;/p&gt;

&lt;p&gt;Without actual depth, camera perspective, and component separation, the illusion falls apart as soon as you look closely.&lt;/p&gt;

&lt;p&gt;Choosing the technology became part of the art direction.&lt;/p&gt;

&lt;p&gt;Press enter or click to view image in full size&lt;br&gt;
Press enter or click to view image in full size&lt;br&gt;
Press enter or click to view image in full size&lt;br&gt;
Press enter or click to view image in full size&lt;br&gt;
Press enter or click to view image in full size&lt;br&gt;
Three Interactions That Changed the Shape of the Project&lt;br&gt;
Liquid Reality Reveal&lt;br&gt;
The idea started with a simple question:&lt;/p&gt;

&lt;p&gt;What if an image had another reality underneath it?&lt;/p&gt;

&lt;p&gt;The obvious implementation would be a circular mask following the cursor.&lt;/p&gt;

&lt;p&gt;It would work.&lt;/p&gt;

&lt;p&gt;It would also look exactly like a circular mask following the cursor.&lt;/p&gt;

&lt;p&gt;The more interesting version uses turbulence and radial displacement. The boundary refracts. Movement creates a wake. Pointer velocity influences the distortion. An alternate image isn’t simply placed on top; it is sampled through a responsive lens.&lt;/p&gt;

&lt;p&gt;The result is less about revealing content and more about making the surface feel uncertain.&lt;/p&gt;

&lt;p&gt;Magnetic Typography&lt;br&gt;
Typography normally behaves itself.&lt;/p&gt;

&lt;p&gt;It sits on a baseline, follows a grid, and waits for you to read it.&lt;/p&gt;

&lt;p&gt;In this experiment, the word becomes a collection of small physical bodies.&lt;/p&gt;

&lt;p&gt;Each glyph reacts according to where your pointer is. Each one carries velocity. When you move away, it doesn’t teleport back to its original position.&lt;/p&gt;

&lt;p&gt;It resolves the force that moved it.&lt;/p&gt;

&lt;p&gt;That distinction is relatively small in code and surprisingly large in feel.&lt;/p&gt;

&lt;p&gt;The text stops being decoration.&lt;/p&gt;

&lt;p&gt;It becomes an object.&lt;/p&gt;

&lt;p&gt;Scroll-Scrubbed 3D Product Explosion&lt;br&gt;
This is the practical side of the same idea.&lt;/p&gt;

&lt;p&gt;There is a difference between telling someone that a product is carefully engineered and allowing them to see its construction separate in space.&lt;/p&gt;

&lt;p&gt;The example uses a procedural headphone assembly.&lt;/p&gt;

&lt;p&gt;As scroll progression changes, individual components move from stable origins along defined explosion vectors.&lt;/p&gt;

&lt;p&gt;The scene isn’t a sequence of pre-rendered frames.&lt;/p&gt;

&lt;p&gt;It is a construction that can be explored.&lt;/p&gt;

&lt;p&gt;And that makes the motion useful.&lt;/p&gt;

&lt;p&gt;It explains something.&lt;/p&gt;

&lt;p&gt;Fifteen Different Ways to Respond&lt;br&gt;
Those experiments eventually became a collection of fifteen systems:&lt;/p&gt;

&lt;p&gt;Liquid image refraction&lt;br&gt;
Magnetic typography&lt;br&gt;
Melting text&lt;br&gt;
Cinematic image disintegration&lt;br&gt;
Infinite 3D product carousel&lt;br&gt;
Dimensional image trail&lt;br&gt;
Scroll-scrubbed product explosion&lt;br&gt;
Liquid magnetic button&lt;br&gt;
Portal image transition&lt;br&gt;
Holographic glass cards&lt;br&gt;
Scroll tunnel&lt;br&gt;
Procedural aurora field&lt;br&gt;
Physics-based navigation&lt;br&gt;
Controlled signal glitch&lt;br&gt;
Geometry morphing&lt;br&gt;
The variety matters, but it isn’t the point.&lt;/p&gt;

&lt;p&gt;The point is that every effect has a different relationship with input.&lt;/p&gt;

&lt;p&gt;Some follow velocity.&lt;/p&gt;

&lt;p&gt;Some respond to proximity.&lt;/p&gt;

&lt;p&gt;Some use scroll as a narrative timeline.&lt;/p&gt;

&lt;p&gt;Some create atmosphere without demanding attention.&lt;/p&gt;

&lt;p&gt;Become a Medium member&lt;br&gt;
Some are deliberately playful.&lt;/p&gt;

&lt;p&gt;Others are designed to communicate how something works.&lt;/p&gt;

&lt;p&gt;A good collection shouldn’t give every website the same signature.&lt;/p&gt;

&lt;p&gt;It should give developers enough room to find their own.&lt;/p&gt;

&lt;p&gt;A Beautiful Demo Is the Easy Part&lt;br&gt;
The less photogenic work starts after the first successful interaction.&lt;/p&gt;

&lt;p&gt;What happens when the viewport changes?&lt;/p&gt;

&lt;p&gt;What happens when the user is touching a screen instead of holding a mouse?&lt;/p&gt;

&lt;p&gt;What happens when a component mounts twice?&lt;/p&gt;

&lt;p&gt;What happens when an image finishes loading after the component has already been destroyed?&lt;/p&gt;

&lt;p&gt;What happens when the browser cannot create another WebGL context?&lt;/p&gt;

&lt;p&gt;And what happens when the user has explicitly asked the operating system for less motion?&lt;/p&gt;

&lt;p&gt;Those aren’t cleanup questions.&lt;/p&gt;

&lt;p&gt;They’re part of the creative work.&lt;/p&gt;

&lt;p&gt;MOTIONFORGE uses a shared lifecycle so effects can be configured, paused, resumed, and destroyed consistently.&lt;/p&gt;

&lt;p&gt;The gallery initializes effects near the viewport and releases scenes when they leave it.&lt;/p&gt;

&lt;p&gt;Pixel ratio is bounded.&lt;/p&gt;

&lt;p&gt;Mobile rendering can be reduced.&lt;/p&gt;

&lt;p&gt;Late texture requests are guarded.&lt;/p&gt;

&lt;p&gt;Scroll effects own their triggers instead of blindly deleting someone else’s.&lt;/p&gt;

&lt;p&gt;The goal is smooth performance on suitable modern hardware.&lt;/p&gt;

&lt;p&gt;That is deliberately different from promising a universal frame rate.&lt;/p&gt;

&lt;p&gt;The final implementation still needs to be profiled against the browsers and devices that actually matter for a project.&lt;/p&gt;

&lt;p&gt;I’d rather make that boundary explicit than turn an aspiration into a marketing statistic.&lt;/p&gt;

&lt;p&gt;Reduced Motion Isn’t a Second-Class Experience&lt;br&gt;
One of the most important design decisions was deciding what remains when movement is removed.&lt;/p&gt;

&lt;p&gt;The answer cannot be:&lt;/p&gt;

&lt;p&gt;Nothing.&lt;/p&gt;

&lt;p&gt;Text should remain readable.&lt;/p&gt;

&lt;p&gt;Images should remain available.&lt;/p&gt;

&lt;p&gt;Navigation should still work.&lt;/p&gt;

&lt;p&gt;A button should still be a button.&lt;/p&gt;

&lt;p&gt;Under reduced motion, decorative behavior can stop or become a static composition. The underlying content and functionality remain intact.&lt;/p&gt;

&lt;p&gt;This turned out to be more than an accessibility requirement.&lt;/p&gt;

&lt;p&gt;It became a useful creative constraint.&lt;/p&gt;

&lt;p&gt;If an interaction only makes sense while it is moving, perhaps the underlying design needs more thought.&lt;/p&gt;

&lt;p&gt;Let People Explore Before They Read the Source&lt;br&gt;
I wanted the public experience to feel more like an exhibition than a documentation dashboard.&lt;/p&gt;

&lt;p&gt;The demo opens with a large MOTIONFORGE wordmark and a liquid-metal composition.&lt;/p&gt;

&lt;p&gt;From there, the collection becomes a series of live experiments.&lt;/p&gt;

&lt;p&gt;There is no wall of code competing with the motion.&lt;/p&gt;

&lt;p&gt;No source-download buttons.&lt;/p&gt;

&lt;p&gt;No pricing table interrupting the gallery.&lt;/p&gt;

&lt;p&gt;Visitors can explore an effect, open a larger preview, adjust parameters, and save a preset locally.&lt;/p&gt;

&lt;p&gt;The developer experience is deliberately separate.&lt;/p&gt;

&lt;p&gt;The purchased project can be run locally, where the same collection becomes something different:&lt;/p&gt;

&lt;p&gt;Actual source code.&lt;/p&gt;

&lt;p&gt;Copyable examples.&lt;/p&gt;

&lt;p&gt;Documentation.&lt;/p&gt;

&lt;p&gt;Architecture-aware customization prompts.&lt;/p&gt;

&lt;p&gt;Two contexts.&lt;/p&gt;

&lt;p&gt;Two different jobs.&lt;/p&gt;

&lt;p&gt;The public demo helps you decide whether the motion belongs in your idea.&lt;/p&gt;

&lt;p&gt;The local edition helps you make it belong in your codebase.&lt;/p&gt;

&lt;p&gt;Reusability Shouldn’t Mean Removing the Personality&lt;br&gt;
There is a familiar tradeoff in creative tools:&lt;/p&gt;

&lt;p&gt;The more reusable something becomes, the more generic it can feel.&lt;/p&gt;

&lt;p&gt;I tried to avoid that by making the behavior configurable without flattening its character.&lt;/p&gt;

&lt;p&gt;Intensity isn’t simply a larger scale.&lt;/p&gt;

&lt;p&gt;Timing isn’t just a slower fade.&lt;/p&gt;

&lt;p&gt;A spring can become tighter or more languid.&lt;/p&gt;

&lt;p&gt;A liquid field can become quieter or more viscous.&lt;/p&gt;

&lt;p&gt;Geometry can change its phase and material while remaining part of the same underlying system.&lt;/p&gt;

&lt;p&gt;The API is intentionally small.&lt;/p&gt;

&lt;p&gt;Every effect has a target, a clear configuration surface, and a lifecycle that can be reasoned about.&lt;/p&gt;

&lt;p&gt;That predictability is what makes the unusual behavior usable.&lt;/p&gt;

&lt;p&gt;The Web Doesn’t Need to Be Louder&lt;br&gt;
It needs to be more intentional.&lt;/p&gt;

&lt;p&gt;Not every page should melt.&lt;/p&gt;

&lt;p&gt;Not every navigation bar needs inertia.&lt;/p&gt;

&lt;p&gt;Not every product deserves an exploded view.&lt;/p&gt;

&lt;p&gt;But when an idea calls for something expressive, the interaction should feel designed rather than borrowed.&lt;/p&gt;

&lt;p&gt;That’s the ambition behind MOTIONFORGE:&lt;/p&gt;

&lt;p&gt;Production-ready animations for websites that refuse to look ordinary.&lt;/p&gt;

&lt;p&gt;Not motion for the sake of filling a timeline.&lt;/p&gt;

&lt;p&gt;Motion that gives a website a little gravity.&lt;/p&gt;

&lt;p&gt;A little materiality.&lt;/p&gt;

&lt;p&gt;A reason to pause.&lt;/p&gt;

&lt;p&gt;Explore the live playground&lt;br&gt;
&lt;a href="https://motionforge-2.vercel.app/" rel="noopener noreferrer"&gt;https://motionforge-2.vercel.app/&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Move slowly.&lt;/p&gt;

&lt;p&gt;Then quickly.&lt;/p&gt;

&lt;p&gt;Pull a word apart.&lt;/p&gt;

&lt;p&gt;Open a portal.&lt;/p&gt;

&lt;p&gt;Take a product apart and put it back together.&lt;/p&gt;

&lt;p&gt;The interesting part isn’t watching an animation.&lt;/p&gt;

&lt;p&gt;It’s discovering what happens when the animation responds to you.&lt;/p&gt;

&lt;p&gt;Get MOTIONFORGE&lt;br&gt;
&lt;a href="https://boukataya.gumroad.com/l/ybyvxk" rel="noopener noreferrer"&gt;https://boukataya.gumroad.com/l/ybyvxk&lt;/a&gt;&lt;/p&gt;

</description>
      <category>design</category>
      <category>development</category>
      <category>webdev</category>
      <category>programming</category>
    </item>
    <item>
      <title>Your Website’s Animations Are Boring — Here’s the Engineering That Fixes That</title>
      <dc:creator>Mohamed Sadok</dc:creator>
      <pubDate>Wed, 30 Sep 2026 15:32:46 +0000</pubDate>
      <link>https://dev.to/mohamed_sadok_42edff3a1b3/your-websites-animations-are-boring-heres-the-engineering-that-fixes-that-3jdo</link>
      <guid>https://dev.to/mohamed_sadok_42edff3a1b3/your-websites-animations-are-boring-heres-the-engineering-that-fixes-that-3jdo</guid>
      <description>&lt;p&gt;I Built 15 WebGL Animations. The Hard Part Wasn’t the WebGL.&lt;br&gt;
What I learned about render loops, WebGL context limits, real physics, performance, and reduced motion while turning a collection of experiments into a production-ready animation library.&lt;/p&gt;

&lt;p&gt;There is a particular kind of link a client sends you.&lt;/p&gt;

&lt;p&gt;No brief.&lt;br&gt;
No Figma file.&lt;br&gt;
Just a URL and four words:&lt;/p&gt;

&lt;p&gt;“Can we do that?”&lt;/p&gt;

&lt;p&gt;On the other end is a site where the cursor drags liquid across a photograph, where a headline’s letters lean away from your pointer like iron filings, where scrolling tears a product apart into its components.&lt;/p&gt;

&lt;p&gt;It looks like magic.&lt;/p&gt;

&lt;p&gt;It is not magic.&lt;/p&gt;

&lt;p&gt;It is a signed-distance field, a spring integrator, and somebody’s very long afternoon.&lt;/p&gt;

&lt;p&gt;I spent that afternoon fifteen times.&lt;/p&gt;

&lt;p&gt;Then I threw the afternoons away and kept the system.&lt;/p&gt;

&lt;p&gt;This is what the system looks like — and the four engineering decisions that turned “a folder of cool demos” into something you can actually hand to a client.&lt;/p&gt;

&lt;p&gt;The Rule That Made This Possible: Lowest-Complexity Technology Wins&lt;br&gt;
The instinct, when you know WebGL, is to reach for WebGL.&lt;/p&gt;

&lt;p&gt;It is almost always wrong.&lt;/p&gt;

&lt;p&gt;Take the image trail effect: copies of a photo stacked behind your cursor as it moves.&lt;/p&gt;

&lt;p&gt;The lazy version is fifteen DOM nodes with decreasing opacity.&lt;/p&gt;

&lt;p&gt;It looks like a PowerPoint transition.&lt;/p&gt;

&lt;p&gt;The over-engineered version is a render-target ping-pong with a velocity buffer.&lt;/p&gt;

&lt;p&gt;The right version is Canvas 2D with actual perspective maths.&lt;/p&gt;

&lt;p&gt;Each trail instance has a real Z depth. Its corners get rotated in 3D, divided by the focal plane, and the resulting affine basis is written straight into setTransform().&lt;/p&gt;

&lt;p&gt;Depth drives scale, draw order, and atmospheric tint.&lt;/p&gt;

&lt;p&gt;The result is about 90 lines, runs at 60fps on integrated graphics, and — crucially — doesn’t consume a WebGL context.&lt;/p&gt;

&lt;p&gt;Because a WebGL context is one of the scarcest resources on the page.&lt;/p&gt;

&lt;p&gt;The same logic killed three shader passes.&lt;/p&gt;

&lt;p&gt;The holographic cards use thin-film interference maths in CSS instead of a refraction shader:&lt;/p&gt;

&lt;p&gt;2 · n · d · cos(θ)&lt;/p&gt;

&lt;p&gt;mapped through the visible spectrum into gradient stops.&lt;/p&gt;

&lt;p&gt;The liquid button uses the blur() → contrast() metaball trick on a small canvas because that is real viscous merging.&lt;/p&gt;

&lt;p&gt;WebGL appears exactly where WebGL is the only honest answer:&lt;/p&gt;

&lt;p&gt;Displacement fields. Dissolves. Portals. Tunnels. Aurora.&lt;/p&gt;

&lt;p&gt;Decision #1: There Is One Render Loop, and It Is Not Yours&lt;br&gt;
Fifteen animations means fifteen requestAnimationFrame loops if you build them the obvious way.&lt;/p&gt;

&lt;p&gt;Fifteen loops means fifteen independent clock reads.&lt;/p&gt;

&lt;p&gt;Fifteen delta-time calculations.&lt;/p&gt;

&lt;p&gt;And, more importantly, fifteen things continuing to run after the user has scrolled past them.&lt;/p&gt;

&lt;p&gt;So the library owns a single ticker.&lt;/p&gt;

&lt;p&gt;Effects subscribe to it.&lt;/p&gt;

&lt;p&gt;The loop cancels itself when there are no subscribers.&lt;/p&gt;

&lt;p&gt;function loop(now: number) {&lt;br&gt;
  const dt = Math.min((now - last) / 1000, MAX_DT);&lt;br&gt;
  last = now;&lt;br&gt;
  for (const fn of Array.from(subscribers)) {&lt;br&gt;
    fn(dt, elapsed, frame);&lt;br&gt;
  }&lt;br&gt;
  if (subscribers.size &amp;gt; 0) {&lt;br&gt;
    rafId = requestAnimationFrame(loop);&lt;br&gt;
  } else {&lt;br&gt;
    running = false;&lt;br&gt;
  }&lt;br&gt;
}&lt;br&gt;
Two details matter more than they look.&lt;/p&gt;

&lt;p&gt;Delta-time needs a ceiling&lt;br&gt;
A backgrounded browser tab can return with a four-second dt.&lt;/p&gt;

&lt;p&gt;A stiff spring integrated over four seconds doesn’t politely ease back into place.&lt;/p&gt;

&lt;p&gt;It launches itself into NaN.&lt;/p&gt;

&lt;p&gt;So the delta is clamped.&lt;/p&gt;

&lt;p&gt;Subscribers need to be copied&lt;br&gt;
An effect is allowed to unsubscribe during its own tick.&lt;/p&gt;

&lt;p&gt;That’s exactly what happens when an effect tears itself down mid-frame.&lt;/p&gt;

&lt;p&gt;The pointer gets the same treatment.&lt;/p&gt;

&lt;p&gt;One passive pointermove listener publishes:&lt;/p&gt;

&lt;p&gt;position&lt;br&gt;
smoothed velocity&lt;br&gt;
normalized energy&lt;br&gt;
It is reference-counted, so fifteen magnetic effects still cost one listener.&lt;/p&gt;

&lt;p&gt;The sampler switches itself off when nothing is reading it.&lt;/p&gt;

&lt;p&gt;Decision #2: Budget Your WebGL Contexts Like Memory&lt;br&gt;
Here is the bug that ends careers.&lt;/p&gt;

&lt;p&gt;Browsers can silently drop older WebGL contexts when too many are alive.&lt;/p&gt;

&lt;p&gt;You don’t necessarily get a useful error.&lt;/p&gt;

&lt;p&gt;Your hero canvas simply goes black.&lt;/p&gt;

&lt;p&gt;Usually on the pages with the most content, on the machines with the most going on, weeks after launch.&lt;/p&gt;

&lt;p&gt;A library with fifteen effects — several of them WebGL — can hit that ceiling surprisingly quickly.&lt;/p&gt;

&lt;p&gt;So the engine treats contexts as a scarce resource with an explicit budget.&lt;/p&gt;

&lt;p&gt;The rules are simple:&lt;/p&gt;

&lt;p&gt;Four surfaces may be live at once.&lt;br&gt;
Each surface reports a visibility score.&lt;br&gt;
When a fifth requests a context, the least-visible surface is suspended.&lt;br&gt;
Suspended contexts are properly destroyed with WEBGL_lose_context.&lt;br&gt;
When the surface becomes visible again, it re-initializes.&lt;br&gt;
Effects are initialized lazily when they approach the viewport.&lt;br&gt;
releaseOnExit tears them down when they leave it.&lt;br&gt;
This means a page can mount all fifteen effects while remaining inside the context budget.&lt;/p&gt;

&lt;p&gt;The demo even ships with a HUD showing the live count so you can watch the system work.&lt;/p&gt;

&lt;p&gt;Live demo: &lt;a href="https://motionforge-lyart.vercel.app/" rel="noopener noreferrer"&gt;https://motionforge-lyart.vercel.app/&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Decision #3: Physics Is Not a Metaphor&lt;br&gt;
Two effects in the library are physical simulations.&lt;/p&gt;

&lt;p&gt;Both fall apart if you fake them.&lt;/p&gt;

&lt;p&gt;The navigation is a Verlet rope.&lt;/p&gt;

&lt;p&gt;Each item is a body with a position and previous position. Velocity is implicit.&lt;/p&gt;

&lt;p&gt;Bodies are pinned to their rest anchors by constraints and coupled to their neighbours through distance constraints.&lt;/p&gt;

&lt;p&gt;The constraints are relaxed over five iterations per step.&lt;/p&gt;

&lt;p&gt;The cursor applies a repulsion field with a squared falloff.&lt;/p&gt;

&lt;p&gt;Dragging pins a body to the pointer.&lt;/p&gt;

&lt;p&gt;Releasing it writes the pointer’s measured velocity into px and py.&lt;/p&gt;

&lt;p&gt;That’s how you throw it.&lt;/p&gt;

&lt;p&gt;Nothing is tweened.&lt;/p&gt;

&lt;p&gt;There is no:&lt;/p&gt;

&lt;p&gt;ease: "elastic"&lt;br&gt;
anywhere near it.&lt;/p&gt;

&lt;p&gt;And you can feel the difference.&lt;/p&gt;

&lt;p&gt;Elastic easing is a curve pretending to have mass.&lt;/p&gt;

&lt;p&gt;The core is simply:&lt;/p&gt;

&lt;p&gt;integrate(points, dt, gravity, drag);&lt;br&gt;
satisfy(constraints, 5);&lt;br&gt;
The typography works the same way.&lt;/p&gt;

&lt;p&gt;Every glyph owns a critically damped spring.&lt;/p&gt;

&lt;p&gt;Inside the influence radius, cursor attraction falls off with distance squared, while the glyph inherits a slice of pointer velocity as skew.&lt;/p&gt;

&lt;p&gt;Because the springs are per-glyph and slightly detuned, a fast swipe sends a wave through the word instead of moving the entire word as one block.&lt;/p&gt;

&lt;p&gt;That wave only exists because the numbers are real.&lt;/p&gt;

&lt;p&gt;Write on Medium&lt;br&gt;
The same principle applies to the shaders.&lt;/p&gt;

&lt;p&gt;The liquid reveal isn’t a blurred circle mask.&lt;/p&gt;

&lt;p&gt;It uses nine spring-chained metaballs merged with a polynomial smooth-min. Its boundary normal is sampled again to create the refractive rim.&lt;/p&gt;

&lt;p&gt;The dissolve isn’t a simple wipe either.&lt;/p&gt;

&lt;p&gt;An FBM field determines the order in which pixels disappear. Surviving pixels near the burn front are displaced, stretched through a second turbulence pass, desaturated, and surrounded by hashed ember cells that continue moving after the original pixel is gone.&lt;/p&gt;

&lt;p&gt;Decision #4: Reduced Motion Is a State, Not a Checkbox&lt;br&gt;
prefers-reduced-motion is where many motion libraries get quietly dishonest.&lt;/p&gt;

&lt;p&gt;They disable an opacity transition and call it accessible.&lt;/p&gt;

&lt;p&gt;That isn’t enough.&lt;/p&gt;

&lt;p&gt;Every effect in this library ships with a documented fallback that preserves the content and the API.&lt;/p&gt;

&lt;p&gt;For example:&lt;/p&gt;

&lt;p&gt;The liquid reveal displays the photograph in full colour with no shader or canvas.&lt;br&gt;
The 3D carousel becomes a scroll-snap strip using the same panels.&lt;br&gt;
The scroll tunnel becomes seven concentric CSS frames.&lt;br&gt;
The physics navigation becomes a static row using the same real &lt;a href=""&gt; elements.&lt;br&gt;
The morph renders its first shape with the geometry intact.&lt;br&gt;
The glitch effect becomes a single 60ms flash on hover instead of looping automatically.&lt;br&gt;
The media query is also observed live.&lt;/a&gt;&lt;/p&gt;
&lt;a href=""&gt;

&lt;p&gt;Changing the operating-system preference reinitializes the page without requiring a reload.&lt;/p&gt;

&lt;p&gt;The demo includes a switch that forces the same code path.&lt;/p&gt;

&lt;p&gt;Because if you cannot see your fallbacks, you don’t have fallbacks.&lt;/p&gt;

&lt;p&gt;You have intentions.&lt;/p&gt;

&lt;p&gt;What the Fifteen Actually Are&lt;br&gt;
Each effect ships as its own folder containing an index.ts and a detailed README covering:&lt;/p&gt;

&lt;p&gt;installation&lt;br&gt;
configuration&lt;br&gt;
advanced configuration&lt;br&gt;
React usage&lt;br&gt;
vanilla usage&lt;br&gt;
mobile behaviour&lt;br&gt;
accessibility&lt;br&gt;
performance&lt;br&gt;
troubleshooting&lt;br&gt;
Every effect starts with the same base configuration:&lt;/p&gt;

&lt;p&gt;target&lt;br&gt;
intensity&lt;br&gt;
duration&lt;br&gt;
mobileMode&lt;br&gt;
respectReducedMotion&lt;br&gt;
lazy&lt;br&gt;
releaseOnExit&lt;br&gt;
And every effect follows the same lifecycle contract:&lt;/p&gt;

&lt;p&gt;const fx = createPortalTransition({&lt;br&gt;
  target: el,&lt;br&gt;
  images,&lt;br&gt;
  twist: 2.4&lt;br&gt;
});&lt;br&gt;
fx.setOptions({ duration: 2 });&lt;br&gt;
fx.open(clientX, clientY);&lt;br&gt;
fx.destroy();&lt;br&gt;
That last method is probably the least exciting part of the API.&lt;/p&gt;

&lt;p&gt;It’s also one of the most important.&lt;/p&gt;

&lt;p&gt;destroy() removes the nodes, listeners, textures, and contexts.&lt;/p&gt;

&lt;p&gt;In an SPA, that’s the difference between a portfolio site and a memory leak.&lt;/p&gt;

&lt;p&gt;The Part I’d Hand to an AI&lt;br&gt;
Every effect also ships with a customization prompt.&lt;/p&gt;

&lt;p&gt;Not a generic:&lt;/p&gt;

&lt;p&gt;“Make this animation cooler.”&lt;/p&gt;

&lt;p&gt;A real engineering brief.&lt;/p&gt;

&lt;p&gt;The prompt tells a coding agent which architecture must remain intact:&lt;/p&gt;

&lt;p&gt;lifecycle wrapper&lt;br&gt;
shared ticker&lt;br&gt;
WebGL context budget&lt;br&gt;
reduced-motion branch&lt;br&gt;
The agent can change:&lt;/p&gt;

&lt;p&gt;colours&lt;br&gt;
intensity&lt;br&gt;
timing&lt;br&gt;
visual treatment&lt;br&gt;
page integration&lt;br&gt;
without quietly replacing the underlying physics with:&lt;/p&gt;

&lt;p&gt;transition: all 0.3s;&lt;br&gt;
That distinction matters.&lt;/p&gt;

&lt;p&gt;AI is very good at changing what you can see.&lt;/p&gt;

&lt;p&gt;It is much less trustworthy when you ask it to preserve the engineering decisions you cannot see.&lt;/p&gt;

&lt;p&gt;The customization prompt exists to protect those decisions.&lt;/p&gt;

&lt;p&gt;What I’d Do Differently&lt;br&gt;
Measure earlier&lt;br&gt;
The WebGL context budget exists because I found the black-canvas bug the hard way.&lt;/p&gt;

&lt;p&gt;If you’re building something with more than three WebGL contexts, build the budget first.&lt;/p&gt;

&lt;p&gt;Beware filter costs&lt;br&gt;
SVG feTurbulence can look incredible on a headline.&lt;/p&gt;

&lt;p&gt;It can also become catastrophic when applied to a large amount of text every frame.&lt;/p&gt;

&lt;p&gt;Never read layout inside a tick&lt;br&gt;
Every getBoundingClientRect() inside a per-frame callback can trigger expensive layout work.&lt;/p&gt;

&lt;p&gt;Measure on resize.&lt;/p&gt;

&lt;p&gt;Cache the values.&lt;/p&gt;

&lt;p&gt;Interpolate.&lt;/p&gt;

&lt;p&gt;Design teardown with the effect&lt;br&gt;
Don’t bolt cleanup on at the end.&lt;/p&gt;

&lt;p&gt;Lifecycle management should be part of the effect’s architecture from the beginning.&lt;/p&gt;

&lt;p&gt;See It Move&lt;br&gt;
The whole system runs directly in the browser.&lt;/p&gt;

&lt;p&gt;Fifteen live specimens.&lt;/p&gt;

&lt;p&gt;Each one can be opened and reconfigured in real time.&lt;/p&gt;

&lt;p&gt;The HUD reports:&lt;/p&gt;

&lt;p&gt;FPS&lt;br&gt;
live WebGL contexts&lt;br&gt;
ticker subscribers&lt;br&gt;
So the performance claims aren’t just theoretical.&lt;/p&gt;

&lt;p&gt;You can actually watch the system work.&lt;/p&gt;

&lt;/a&gt;&lt;p&gt;&lt;a href=""&gt;→ Live Demo: &lt;/a&gt;&lt;a href="https://motionforge-lyart.vercel.app/" rel="noopener noreferrer"&gt;&lt;/a&gt;&lt;a href="https://motionforge-lyart.vercel.app/" rel="noopener noreferrer"&gt;https://motionforge-lyart.vercel.app/&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If you build websites where motion is part of the argument rather than decoration, I packaged the complete system:&lt;/p&gt;

&lt;p&gt;15 effects + engine + documentation + customization prompts + demo source.&lt;/p&gt;

&lt;p&gt;→ MotionForge on Gumroad: &lt;a href="https://boukataya.gumroad.com/l/cqzizd" rel="noopener noreferrer"&gt;https://boukataya.gumroad.com/l/cqzizd&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Built with TypeScript, WebGL/GLSL, Three.js, GSAP ScrollTrigger, Lenis, and an unreasonable amount of coffee.&lt;/p&gt;

&lt;p&gt;Questions in the responses — I answer all of them.&lt;/p&gt;

</description>
      <category>animation</category>
      <category>webgl</category>
      <category>webdev</category>
      <category>programming</category>
    </item>
    <item>
      <title>I Built a Tool to Find the Tests Developers Have Stopped Trusting</title>
      <dc:creator>Mohamed Sadok</dc:creator>
      <pubDate>Tue, 29 Sep 2026 08:36:54 +0000</pubDate>
      <link>https://dev.to/mohamed_sadok_42edff3a1b3/i-built-a-tool-to-find-the-tests-developers-have-stopped-trusting-3j3c</link>
      <guid>https://dev.to/mohamed_sadok_42edff3a1b3/i-built-a-tool-to-find-the-tests-developers-have-stopped-trusting-3j3c</guid>
      <description>&lt;p&gt;I Built a Tool to Find the Tests Developers Have Stopped Trusting&lt;/p&gt;

&lt;p&gt;A test fails in CI.&lt;/p&gt;

&lt;p&gt;You look at it.&lt;/p&gt;

&lt;p&gt;You run it again.&lt;/p&gt;

&lt;p&gt;It passes.&lt;/p&gt;

&lt;p&gt;You run it again.&lt;/p&gt;

&lt;p&gt;It passes.&lt;/p&gt;

&lt;p&gt;You rerun the pipeline.&lt;/p&gt;

&lt;p&gt;Everything is green.&lt;/p&gt;

&lt;p&gt;So you merge the pull request.&lt;/p&gt;

&lt;p&gt;A few hours later, the same test fails again.&lt;/p&gt;

&lt;p&gt;At some point, the team stops treating that failure as useful information.&lt;/p&gt;

&lt;p&gt;That's the real problem with flaky tests.&lt;/p&gt;

&lt;p&gt;They don't just make CI unreliable.&lt;/p&gt;

&lt;p&gt;They make developers stop trusting CI.&lt;/p&gt;

&lt;h2&gt;
  
  
  The problem isn't writing tests
&lt;/h2&gt;

&lt;p&gt;Modern JavaScript projects already have excellent testing tools.&lt;/p&gt;

&lt;p&gt;Playwright is great for browser testing.&lt;/p&gt;

&lt;p&gt;Vitest and Jest are great for unit and integration testing.&lt;/p&gt;

&lt;p&gt;CI platforms can execute thousands of tests.&lt;/p&gt;

&lt;p&gt;The problem starts after the tests exist.&lt;/p&gt;

&lt;p&gt;A growing project eventually accumulates:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Tests that fail intermittently&lt;/li&gt;
&lt;li&gt;Tests that take too long&lt;/li&gt;
&lt;li&gt;Tests that haven't failed in months&lt;/li&gt;
&lt;li&gt;Tests that fail only in CI&lt;/li&gt;
&lt;li&gt;Tests that fail because of timing&lt;/li&gt;
&lt;li&gt;Tests that produce almost identical errors&lt;/li&gt;
&lt;li&gt;Tests that nobody wants to touch&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The test suite becomes a second system that needs maintenance.&lt;/p&gt;

&lt;p&gt;And most teams don't have a good way to measure its health.&lt;/p&gt;

&lt;h2&gt;
  
  
  So I built TestOps Kit
&lt;/h2&gt;

&lt;p&gt;The idea is simple:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Don't replace the test runner. Analyze what it produces.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;TestOps Kit consumes machine-readable test results and builds a reliability layer around them.&lt;/p&gt;

&lt;p&gt;The workflow looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Playwright / Vitest / Jest
            ↓
       Test results
            ↓
       TestOps Kit
            ↓
 ┌──────────┼───────────┐
 ↓          ↓           ↓
History   Reliability  Performance
 ↓          ↓           ↓
Flaky     Quarantine   Slow tests
tests
            ↓
        Dashboard
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;The existing testing workflow stays intact.&lt;/p&gt;

&lt;p&gt;TestOps simply adds another layer of information.&lt;/p&gt;
&lt;h2&gt;
  
  
  The first feature I wanted was flaky-test detection
&lt;/h2&gt;

&lt;p&gt;A single failed test doesn't necessarily mean a test is flaky.&lt;/p&gt;

&lt;p&gt;You need history.&lt;/p&gt;

&lt;p&gt;For example:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Run 1 → PASS
Run 2 → PASS
Run 3 → FAIL
Run 4 → PASS
Run 5 → FAIL
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;That pattern is much more interesting than a single failure.&lt;/p&gt;

&lt;p&gt;TestOps tracks test behavior across runs and calculates reliability metrics from the available history.&lt;/p&gt;

&lt;p&gt;The goal isn't to say:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"This test is definitely broken."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The goal is to say:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"This test has demonstrated inconsistent behavior and deserves investigation."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That's a much more useful signal.&lt;/p&gt;
&lt;h2&gt;
  
  
  Then came quarantine
&lt;/h2&gt;

&lt;p&gt;Once you identify unreliable tests, the next problem is operational.&lt;/p&gt;

&lt;p&gt;What do you do with them?&lt;/p&gt;

&lt;p&gt;TestOps can generate a quarantine manifest based on a configurable flakiness threshold.&lt;/p&gt;

&lt;p&gt;For example:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Flaky tests

checkout/payment.spec.ts     42%
account/login.spec.ts        31%
orders/create.spec.ts        27%
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Instead of relying on someone's memory, the team now has a concrete list.&lt;/p&gt;

&lt;p&gt;The important part is that quarantine is treated as a temporary reliability workflow, not a way to permanently hide failures.&lt;/p&gt;
&lt;h2&gt;
  
  
  Slow tests are another form of test debt
&lt;/h2&gt;

&lt;p&gt;A test doesn't have to fail to become expensive.&lt;/p&gt;

&lt;p&gt;Imagine a suite containing:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;1,200 tests
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;and a handful of tests account for a significant portion of the runtime.&lt;/p&gt;

&lt;p&gt;Those tests affect every developer.&lt;/p&gt;

&lt;p&gt;Every pull request.&lt;/p&gt;

&lt;p&gt;Every CI run.&lt;/p&gt;

&lt;p&gt;Every deployment.&lt;/p&gt;

&lt;p&gt;So TestOps also tracks duration and highlights slow tests.&lt;/p&gt;

&lt;p&gt;The goal is straightforward:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Find the tests that are costing the team time.&lt;/strong&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  Visual regression belongs in the same conversation
&lt;/h2&gt;

&lt;p&gt;Testing isn't only about pass and fail.&lt;/p&gt;

&lt;p&gt;UI changes can also introduce unexpected regressions.&lt;/p&gt;

&lt;p&gt;TestOps includes deterministic snapshot checking so a CI pipeline can detect changed snapshot content.&lt;/p&gt;

&lt;p&gt;For example:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx testops snapshot snapshots &lt;span class="nt"&gt;--strict&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;If a snapshot changes unexpectedly, the command returns a failure.&lt;/p&gt;

&lt;p&gt;That makes it possible to use the same reliability workflow in CI.&lt;/p&gt;
&lt;h2&gt;
  
  
  I also wanted a boring CLI
&lt;/h2&gt;

&lt;p&gt;Developer tools don't need complicated installation procedures.&lt;/p&gt;

&lt;p&gt;The basic workflow should be something like:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx testops analyze &lt;span class="nt"&gt;--input&lt;/span&gt; test-results.json
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Then:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx testops report
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;And you get a dashboard.&lt;/p&gt;

&lt;p&gt;The dashboard focuses on the questions developers actually care about:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;How many tests do we have?

How many are failing?

Which ones are flaky?

Which ones are slow?

What happened in previous runs?
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;h2&gt;
  
  
  Testing the tester
&lt;/h2&gt;

&lt;p&gt;One of the most important parts of building a developer tool is testing it against something real.&lt;/p&gt;

&lt;p&gt;A synthetic demo can prove that the software works in theory.&lt;/p&gt;

&lt;p&gt;It doesn't prove that developers can use it in their projects.&lt;/p&gt;

&lt;p&gt;So the validation workflow is:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Existing project
      ↓
Existing tests
      ↓
Generate real test report
      ↓
Feed report into TestOps
      ↓
Compare metrics
      ↓
Create intentional flaky test
      ↓
Run repeatedly
      ↓
Verify detection
      ↓
Create slow test
      ↓
Verify performance detection
      ↓
Modify snapshot
      ↓
Verify regression detection
      ↓
Run inside CI
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;This is the test that matters.&lt;/p&gt;

&lt;p&gt;Not whether the landing page looks good.&lt;/p&gt;

&lt;p&gt;Not whether the dashboard has impressive charts.&lt;/p&gt;

&lt;p&gt;Whether it can survive a real repository.&lt;/p&gt;
&lt;h2&gt;
  
  
  What I learned
&lt;/h2&gt;

&lt;p&gt;The interesting part of test infrastructure isn't necessarily generating more tests.&lt;/p&gt;

&lt;p&gt;It's understanding the tests you already have.&lt;/p&gt;

&lt;p&gt;A team can have 5,000 tests and still have a reliability problem.&lt;/p&gt;

&lt;p&gt;More tests don't automatically mean more confidence.&lt;/p&gt;

&lt;p&gt;Sometimes the real question is:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Which tests can we trust?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;That's the problem TestOps Kit is designed around.&lt;/p&gt;
&lt;h2&gt;
  
  
  The goal
&lt;/h2&gt;

&lt;p&gt;The long-term idea is bigger than a CLI.&lt;/p&gt;

&lt;p&gt;A mature version could understand:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Test history&lt;/li&gt;
&lt;li&gt;CI environments&lt;/li&gt;
&lt;li&gt;Failure patterns&lt;/li&gt;
&lt;li&gt;Code changes&lt;/li&gt;
&lt;li&gt;Test ownership&lt;/li&gt;
&lt;li&gt;Dependency relationships&lt;/li&gt;
&lt;li&gt;Visual changes&lt;/li&gt;
&lt;li&gt;Performance regressions&lt;/li&gt;
&lt;li&gt;Failure clusters&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Eventually, the system could answer questions like:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Which tests became unreliable after this deployment?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Or:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Which changed files are responsible for most of the tests we need to run?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Or:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Which tests have consumed the most CI time this month?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;But the first version starts with something much simpler:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Measure test reliability instead of guessing about it.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Because once developers stop trusting their test suite, the test suite has already become technical debt.&lt;/p&gt;

&lt;p&gt;And that's a problem worth measuring.&lt;br&gt;
&lt;/p&gt;
&lt;div class="crayons-card c-embed text-styles text-styles--secondary"&gt;
    &lt;div class="c-embed__content"&gt;
        &lt;div class="c-embed__cover"&gt;
          &lt;a href="https://boukataya.gumroad.com/l/ivsbba" class="c-link align-middle" rel="noopener noreferrer"&gt;
            &lt;img alt="" src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fpublic-files.gumroad.com%2Fbu0po2cp9j7py75bgsz9s4wfkyx9" height="495" class="m-0" width="1005"&gt;
          &lt;/a&gt;
        &lt;/div&gt;
      &lt;div class="c-embed__body"&gt;
        &lt;h2 class="fs-xl lh-tight"&gt;
          &lt;a href="https://boukataya.gumroad.com/l/ivsbba" rel="noopener noreferrer" class="c-link"&gt;
            TestOps Kit — Flaky Test Detection, Test Reliability &amp;amp; CI Toolkit
          &lt;/a&gt;
        &lt;/h2&gt;
          &lt;p class="truncate-at-3"&gt;
            TestOps KitProduction Test Reliability Toolkit for Modern JavaScript TeamsYour test suite is supposed to give you confidence.Instead, you get: Random CI failures Tests that pass locally but fail in CI Slow test suites Repeated failures nobody investigates Visual regressions Hundreds of test results to manually inspect Flaky tests that waste hours every week TestOps Kit turns raw test results into actionable test reliability data.It analyzes your existing test reports, tracks test history, detects flaky tests, identifies slow tests, generates quarantine manifests, checks visual snapshots, and produces a standalone reliability dashboard.No need to replace your existing test framework.What you getFlaky Test DetectionTrack test behavior across multiple runs and identify tests that repeatedly switch between passing and failing.See: Flakiness percentage Failure frequency Run history Test duration Failure information Test Reliability DashboardGet a single view of your test suite: Total tests Pass rate Failed tests Flaky tests Slow tests Test duration Historical runs Failure details Test QuarantineGenerate a quarantine manifest for tests that exceed your configured flakiness threshold.Instead of manually maintaining a list of unreliable tests, let TestOps identify them from actual test history.Slow Test DetectionFind tests that are consuming disproportionate CI time.Use duration history to identify tests that deserve optimization.Visual Regression ChecksCreate deterministic snapshot baselines and detect unexpected changes using content hashing.Useful for: HTML snapshots JSON snapshots Generated assets Test fixtures Visual regression workflows CI ReadyDesigned to fit into existing CI pipelines.Use it with GitHub Actions and your existing test infrastructure.Multiple Report FormatsThe toolkit supports common machine-readable test reports including: Playwright JSON Vitest/Jest-style JSON JUnit XML Test Impact AnalysisIdentify candidate tests related to changed files so you can focus testing where changes actually occurred.Stress TestingRun the same command repeatedly to expose intermittent failures.Example:npx testops stress --command "npm test" --runs 20 Developer CLIEverything is accessible from the command line.npx testops analyze --input test-results.json npx testops report npx testops quarantine npx testops snapshot snapshots --strict npx testops stress --command "npm test" --runs 20 Built for JavaScript developers TypeScript developers SaaS teams Freelancers Agencies QA engineers DevOps engineers CI/CD-heavy projects Teams using Playwright, Vitest or Jest Requirements Node.js 18+ npm Git Existing automated test reports Included TestOps CLI Reliability analyzer Flaky test detector Test history Quarantine generator Slow-test analysis Stress runner Visual snapshot checker Git impact analysis Standalone dashboard GitHub Actions integration Demo fixtures Complete documentation Example configuration MIT license Why TestOps?Test generation is only half the testing problem.The other half is knowing whether the tests you already have can actually be trusted.TestOps focuses on that problem.Know which tests are stable. Know which ones are flaky. Know which ones are slowing CI down.Build with confidence instead of debugging CI noise.
          &lt;/p&gt;
        &lt;div class="color-secondary fs-s flex items-center"&gt;
            &lt;img alt="favicon" class="c-embed__favicon m-0 mr-2 radius-0" src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fpublic-files.gumroad.com%2F7ai3z4dx2bl2bs70di842sywirml" width="400" height="250"&gt;
          boukataya.gumroad.com
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;
&lt;/div&gt;



</description>
      <category>testing</category>
      <category>typescript</category>
      <category>javascript</category>
      <category>opensource</category>
    </item>
    <item>
      <title>How We Built a Production-Ready SaaS Billing Engine (And How You Can Ship Yours Today)</title>
      <dc:creator>Mohamed Sadok</dc:creator>
      <pubDate>Mon, 28 Sep 2026 15:53:44 +0000</pubDate>
      <link>https://dev.to/mohamed_sadok_42edff3a1b3/how-we-built-a-production-ready-saas-billing-engine-and-how-you-can-ship-yours-today-4be3</link>
      <guid>https://dev.to/mohamed_sadok_42edff3a1b3/how-we-built-a-production-ready-saas-billing-engine-and-how-you-can-ship-yours-today-4be3</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F5dz4oows9nti7hokk3q9.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F5dz4oows9nti7hokk3q9.jpg" alt=" " width="800" height="800"&gt;&lt;/a&gt;&lt;br&gt;
The complete blueprint for handling subscriptions, customer billing portals, and edge-case webhooks without losing your sanity.*&lt;/p&gt;



&lt;p&gt;Every developer building a SaaS eventually runs into the same wall: &lt;strong&gt;Monetization Architecture&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Building the core features of an application is exciting. Writing billing logic, managing recurring cycles, handling failed credit card charges, generating PDFs, and wiring up webhook endpoints is usually where momentum slows down.&lt;/p&gt;

&lt;p&gt;In this article, we'll walk through the architecture of a production-ready SaaS billing engine and examine the critical patterns needed to accept recurring payments reliably.&lt;/p&gt;


&lt;h2&gt;
  
  
  1. The Billing State Problem
&lt;/h2&gt;

&lt;p&gt;Handling payments is not just a single API call to &lt;code&gt;charges.create()&lt;/code&gt;. A production billing system must manage state transitions across multiple scenarios:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;What happens when a renewal payment fails at 3:00 AM?&lt;/li&gt;
&lt;li&gt;How do you handle plan upgrades with prorated charges mid-cycle?&lt;/li&gt;
&lt;li&gt;What happens if a user cancels their subscription before the cycle ends?&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;
  
  
  The Core Subscription State Machine
&lt;/h3&gt;

&lt;p&gt;A resilient subscription model requires tracking distinct statuses:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;trialing&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;active&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;past_due&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;canceled&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;incomplete&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[ Checkout ] ──► [ Active ] ──► (Cycle Renewal)
                      │                │
                      ▼                ▼
                 [ Canceled ]    [ Past Due ] ──► [ Inactive ]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  2. Webhooks: The Single Source of Truth
&lt;/h2&gt;

&lt;p&gt;Never rely solely on client-side redirect URLs (e.g., &lt;code&gt;/checkout/success&lt;/code&gt;) to grant access to paid tiers. Network disconnects, closed browser tabs, or client-side crashes can prevent access from being granted.&lt;/p&gt;
&lt;h3&gt;
  
  
  Recommended Webhook Pipeline
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Verify Signature:&lt;/strong&gt; Ensure the payload originated from your payment gateway.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Idempotency Check:&lt;/strong&gt; Store incoming &lt;code&gt;event_id&lt;/code&gt; values to prevent processing duplicate events.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Queue Processing:&lt;/strong&gt; Immediately return a &lt;code&gt;200 OK&lt;/code&gt; response to the payment provider and dispatch the actual processing task to a background worker.
&lt;/li&gt;
&lt;/ol&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Example: Verifying and dispatching webhooks asynchronously&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;handleWebhook&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;$event&lt;/span&gt; &lt;span class="o"&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="nf"&gt;verifySignature&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&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="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="nf"&gt;hasBeenProcessed&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$event&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;response&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;json&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="s1"&gt;'status'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'already_processed'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="nc"&gt;ProcessBillingEventJob&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;dispatch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$event&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;response&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;json&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="s1"&gt;'status'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'queued'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="mi"&gt;200&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;
  
  
  3. Customer Self-Service: The Billing Portal
&lt;/h2&gt;

&lt;p&gt;Modern users expect to manage their own billing:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Updating expired credit cards&lt;/li&gt;
&lt;li&gt;Downloading past VAT / Tax invoices&lt;/li&gt;
&lt;li&gt;Upgrading or downgrading plans&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Using hosted billing portals (like Stripe Customer Portal) eliminates the need to build custom PCI-compliant credit card updating forms on your frontend.&lt;/p&gt;


&lt;h2&gt;
  
  
  4. Get the Starter Kit
&lt;/h2&gt;

&lt;p&gt;Rather than spending 40+ hours wiring up database schemas, webhook listeners, environment configs, and test suites, you can grab the complete &lt;strong&gt;SaaS Billing Engine Starter Kit&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Includes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Complete source code with local DDEV / Docker support&lt;/li&gt;
&lt;li&gt;Production-tested webhook handlers&lt;/li&gt;
&lt;li&gt;Seeders, migrations, and plan definitions&lt;/li&gt;
&lt;li&gt;Detailed documentation&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://boukataya.gumroad.com/l/shouoa" rel="noopener noreferrer"&gt;https://boukataya.gumroad.com/l/shouoa&lt;/a&gt;&lt;/p&gt;


&lt;div class="crayons-card c-embed text-styles text-styles--secondary"&gt;
    &lt;div class="c-embed__content"&gt;
      &lt;div class="c-embed__body flex items-center justify-between"&gt;
        &lt;a href="https://saa-s-billing-starter-kit-demo.vercel.app/" rel="noopener noreferrer" class="c-link fw-bold flex items-center"&gt;
          &lt;span class="mr-2"&gt;saa-s-billing-starter-kit-demo.vercel.app&lt;/span&gt;
          

        &lt;/a&gt;
      &lt;/div&gt;
    &lt;/div&gt;
&lt;/div&gt;


</description>
      <category>laravel</category>
      <category>stripe</category>
      <category>saas</category>
      <category>php</category>
    </item>
    <item>
      <title>How I Built a 1970s Retro Vinyl Store Web App in 1 Single HTML File (No Frameworks, No Build Step)</title>
      <dc:creator>Mohamed Sadok</dc:creator>
      <pubDate>Fri, 25 Sep 2026 10:17:29 +0000</pubDate>
      <link>https://dev.to/mohamed_sadok_42edff3a1b3/how-i-built-a-1970s-retro-vinyl-store-web-app-in-1-single-html-file-no-frameworks-no-build-step-1717</link>
      <guid>https://dev.to/mohamed_sadok_42edff3a1b3/how-i-built-a-1970s-retro-vinyl-store-web-app-in-1-single-html-file-no-frameworks-no-build-step-1717</guid>
      <description>&lt;p&gt;Every modern e-commerce website seems to look the same.&lt;/p&gt;

&lt;p&gt;Clean white backgrounds.&lt;br&gt;
Minimal cards.&lt;br&gt;
Rounded buttons.&lt;br&gt;
The same fonts.&lt;br&gt;
The same SaaS-style layouts.&lt;/p&gt;

&lt;p&gt;It works, but sometimes it feels completely soulless.&lt;/p&gt;

&lt;p&gt;So I decided to build something different.&lt;/p&gt;

&lt;p&gt;I wanted to recreate the feeling of walking into a small record store in the late 1970s — old vinyl sleeves, warm paper textures, bold typography, imperfect print effects, and even the subtle sound of vinyl crackling in the background.&lt;/p&gt;

&lt;p&gt;The result is GrooveHaus, a retro vinyl store web app built entirely inside one HTML file.&lt;/p&gt;

&lt;p&gt;No framework.&lt;br&gt;
No build process.&lt;br&gt;
No node_modules.&lt;br&gt;
No complicated setup.&lt;/p&gt;

&lt;p&gt;Just HTML, CSS, JavaScript, and a lot of experimentation.&lt;/p&gt;

&lt;p&gt;🎧 Live Demo &amp;amp; Template&lt;/p&gt;

&lt;p&gt;Live Demo:&lt;br&gt;
&lt;a href="https://groove-haus.vercel.app/" rel="noopener noreferrer"&gt;https://groove-haus.vercel.app/&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Template:&lt;br&gt;
&lt;a href="https://boukataya.gumroad.com/l/ytowj" rel="noopener noreferrer"&gt;https://boukataya.gumroad.com/l/ytowj&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The Idea Behind GrooveHaus&lt;/p&gt;

&lt;p&gt;The goal wasn't simply to make an "old-looking" website.&lt;/p&gt;

&lt;p&gt;I wanted the interface itself to feel like a physical object.&lt;/p&gt;

&lt;p&gt;Think:&lt;/p&gt;

&lt;p&gt;Vintage record sleeves&lt;br&gt;
Printed paper textures&lt;br&gt;
1970s-inspired typography&lt;br&gt;
Burnt orange, mustard, cream and burgundy colors&lt;br&gt;
Grain and imperfect print effects&lt;br&gt;
Records that physically slide out of their sleeves&lt;br&gt;
Analog vinyl crackle&lt;br&gt;
Simple page transitions&lt;br&gt;
A store experience that feels more like browsing a record shop than using a typical e-commerce dashboard&lt;/p&gt;

&lt;p&gt;The interesting part was trying to create those sensations using only browser-native technologies.&lt;/p&gt;

&lt;p&gt;The Constraint: One HTML File&lt;/p&gt;

&lt;p&gt;I deliberately gave myself a strict constraint:&lt;/p&gt;

&lt;p&gt;Everything had to live inside a single .html file.&lt;/p&gt;

&lt;p&gt;That means no React.&lt;/p&gt;

&lt;p&gt;No Vue.&lt;/p&gt;

&lt;p&gt;No build system.&lt;/p&gt;

&lt;p&gt;No npm installation.&lt;/p&gt;

&lt;p&gt;No bundler.&lt;/p&gt;

&lt;p&gt;No component library.&lt;/p&gt;

&lt;p&gt;The page contains the markup, styling and JavaScript needed to run the application.&lt;/p&gt;

&lt;p&gt;External resources are limited to things like fonts and CDN-hosted libraries.&lt;/p&gt;

&lt;p&gt;This sounds restrictive, but it actually made the project more interesting.&lt;/p&gt;

&lt;p&gt;Instead of asking:&lt;/p&gt;

&lt;p&gt;"Which framework should I use?"&lt;/p&gt;

&lt;p&gt;I could focus on:&lt;/p&gt;

&lt;p&gt;"How much can I build with the browser itself?"&lt;/p&gt;

&lt;p&gt;The Stack&lt;/p&gt;

&lt;p&gt;The technology stack is intentionally simple:&lt;/p&gt;

&lt;p&gt;HTML5 handles the structure and application views.&lt;/p&gt;

&lt;p&gt;Tailwind CSS via CDN provides the utility classes and makes it easy to create the visual system.&lt;/p&gt;

&lt;p&gt;Vanilla JavaScript handles routing, application state, rendering, filtering and interactions.&lt;/p&gt;

&lt;p&gt;Web Audio API generates the vinyl crackle effect directly in the browser.&lt;/p&gt;

&lt;p&gt;Google Fonts and Font Awesome provide typography and icons.&lt;/p&gt;

&lt;p&gt;There is no backend because the goal was to create a self-contained frontend template.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Making the Browser Sound Like Vinyl&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;One of my favorite parts of the project is the analog noise effect.&lt;/p&gt;

&lt;p&gt;Instead of shipping an audio file containing vinyl crackle, GrooveHaus generates the sound dynamically using the Web Audio API.&lt;/p&gt;

&lt;p&gt;The browser creates an audio buffer containing low-level random noise combined with occasional louder pops.&lt;/p&gt;

&lt;p&gt;The result is a subtle background texture that resembles an old record playing.&lt;/p&gt;

&lt;p&gt;The important part is keeping it subtle.&lt;/p&gt;

&lt;p&gt;The effect isn't supposed to become annoying background music. It's more like an atmospheric layer that you notice when you stop and listen.&lt;/p&gt;

&lt;p&gt;Users can also turn the effect on or off.&lt;/p&gt;

&lt;p&gt;This was a good reminder that the Web Audio API can do much more than simply play MP3 files.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;A Tiny SPA Without a Framework&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The application behaves like a small single-page application.&lt;/p&gt;

&lt;p&gt;There are different views for things like:&lt;/p&gt;

&lt;p&gt;Storefront&lt;br&gt;
Catalog&lt;br&gt;
Product details&lt;br&gt;
Artists&lt;br&gt;
Journal&lt;br&gt;
Cart&lt;br&gt;
Settings&lt;/p&gt;

&lt;p&gt;Instead of installing a routing library, I used the browser's native URL hash.&lt;/p&gt;

&lt;p&gt;For example, different hash routes determine which application view should be displayed.&lt;/p&gt;

&lt;p&gt;When the URL changes, JavaScript hides the previous view, displays the appropriate one and renders the required content.&lt;/p&gt;

&lt;p&gt;It's obviously not a replacement for a full routing solution in a large production application.&lt;/p&gt;

&lt;p&gt;But for a self-contained template?&lt;/p&gt;

&lt;p&gt;It works surprisingly well.&lt;/p&gt;

&lt;p&gt;And there is something satisfying about realizing that you don't always need another dependency to solve a relatively small problem.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Making Vinyl Records Feel Physical&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;One of the visual details I wanted was the feeling of pulling a record out of its sleeve.&lt;/p&gt;

&lt;p&gt;When hovering over a product card, the vinyl disc slides out from behind the album artwork while rotating.&lt;/p&gt;

&lt;p&gt;It's a small interaction, but it changes how the product card feels.&lt;/p&gt;

&lt;p&gt;Instead of:&lt;/p&gt;

&lt;p&gt;Image → title → price → button&lt;/p&gt;

&lt;p&gt;you get something closer to interacting with a physical record.&lt;/p&gt;

&lt;p&gt;The effect is entirely CSS-based, using transforms and transitions.&lt;/p&gt;

&lt;p&gt;No animation library required.&lt;/p&gt;

&lt;p&gt;This is one of those cases where a few lines of CSS can create much more personality than another UI component ever could.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Recreating Printed Paper&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Modern websites are usually extremely clean.&lt;/p&gt;

&lt;p&gt;GrooveHaus deliberately isn't.&lt;/p&gt;

&lt;p&gt;The background uses warm paper tones combined with subtle radial patterns to imitate the texture of printed material.&lt;/p&gt;

&lt;p&gt;The color palette is built around:&lt;/p&gt;

&lt;p&gt;Cream&lt;br&gt;
Aged paper&lt;br&gt;
Dark brown&lt;br&gt;
Burnt orange&lt;br&gt;
Mustard&lt;br&gt;
Burgundy&lt;/p&gt;

&lt;p&gt;Typography also plays a major role.&lt;/p&gt;

&lt;p&gt;Instead of using one generic UI font everywhere, the design combines display and serif typefaces to create something closer to vintage posters, record sleeves and printed magazines.&lt;/p&gt;

&lt;p&gt;The goal isn't perfect realism.&lt;/p&gt;

&lt;p&gt;It's controlled imperfection.&lt;/p&gt;

&lt;p&gt;Why I Didn't Use React&lt;/p&gt;

&lt;p&gt;This project probably could have been built faster with React.&lt;/p&gt;

&lt;p&gt;But that wasn't the point.&lt;/p&gt;

&lt;p&gt;Sometimes constraints are useful.&lt;/p&gt;

&lt;p&gt;Removing the framework forced me to think about what the browser already provides:&lt;/p&gt;

&lt;p&gt;DOM APIs&lt;br&gt;
URL hashes&lt;br&gt;
CSS transitions&lt;br&gt;
Web Audio&lt;br&gt;
browser storage&lt;br&gt;
native events&lt;br&gt;
modern JavaScript&lt;/p&gt;

&lt;p&gt;It also makes the final result extremely portable.&lt;/p&gt;

&lt;p&gt;There is something nice about downloading one HTML file and knowing that most of the application is sitting right there.&lt;/p&gt;

&lt;p&gt;Where Single-File Apps Actually Make Sense&lt;/p&gt;

&lt;p&gt;I'm not suggesting everyone should abandon frameworks.&lt;/p&gt;

&lt;p&gt;For a large SaaS platform, complex dashboard or production application with dozens of developers, a single HTML file would quickly become painful.&lt;/p&gt;

&lt;p&gt;But there are some cases where this architecture makes a lot of sense.&lt;/p&gt;

&lt;p&gt;For example:&lt;/p&gt;

&lt;p&gt;Templates&lt;/p&gt;

&lt;p&gt;A customer can download one file and immediately start customizing it.&lt;/p&gt;

&lt;p&gt;Prototypes&lt;/p&gt;

&lt;p&gt;You can experiment with an idea without spending time setting up infrastructure.&lt;/p&gt;

&lt;p&gt;Landing pages&lt;/p&gt;

&lt;p&gt;For a relatively simple marketing experience, a build pipeline may be unnecessary.&lt;/p&gt;

&lt;p&gt;Digital products&lt;/p&gt;

&lt;p&gt;A self-contained template is easy to distribute through marketplaces and digital stores.&lt;/p&gt;

&lt;p&gt;Experiments&lt;/p&gt;

&lt;p&gt;Sometimes you simply want to see how far native browser APIs can take you.&lt;/p&gt;

&lt;p&gt;The Bigger Lesson&lt;/p&gt;

&lt;p&gt;The most interesting part of building GrooveHaus wasn't actually the vinyl store.&lt;/p&gt;

&lt;p&gt;It was realizing how much you can do before reaching for another dependency.&lt;/p&gt;

&lt;p&gt;A browser already gives you a lot:&lt;/p&gt;

&lt;p&gt;HTML for structure.&lt;/p&gt;

&lt;p&gt;CSS for animation and visual effects.&lt;/p&gt;

&lt;p&gt;JavaScript for application logic.&lt;/p&gt;

&lt;p&gt;Web Audio for sound.&lt;/p&gt;

&lt;p&gt;The URL for simple routing.&lt;/p&gt;

&lt;p&gt;And modern browser APIs for everything in between.&lt;/p&gt;

&lt;p&gt;Frameworks are incredibly useful.&lt;/p&gt;

&lt;p&gt;But they aren't mandatory for every project.&lt;/p&gt;

&lt;p&gt;Sometimes putting yourself inside a few artificial constraints leads to more creative solutions.&lt;/p&gt;

&lt;p&gt;Final Thoughts&lt;/p&gt;

&lt;p&gt;GrooveHaus started as a visual experiment:&lt;/p&gt;

&lt;p&gt;"Can I make a website feel like a physical record store?"&lt;/p&gt;

&lt;p&gt;It ended up becoming an experiment in something else:&lt;/p&gt;

&lt;p&gt;"How much can I build with nothing more than a browser and a single HTML file?"&lt;/p&gt;

&lt;p&gt;The answer is: quite a lot.&lt;/p&gt;

&lt;p&gt;And honestly, building something that doesn't look like another SaaS dashboard was probably the most refreshing part.&lt;/p&gt;

&lt;p&gt;Live Demo:&lt;br&gt;
&lt;a href="https://groove-haus.vercel.app/" rel="noopener noreferrer"&gt;https://groove-haus.vercel.app/&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Template:&lt;br&gt;
&lt;a href="https://boukataya.gumroad.com/l/ytowj" rel="noopener noreferrer"&gt;https://boukataya.gumroad.com/l/ytowj&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;What retro web design trend would you bring back?&lt;/p&gt;

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

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fssjaax52nhc4yjg2rlmd.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fssjaax52nhc4yjg2rlmd.jpg" alt=" " width="800" height="429"&gt;&lt;/a&gt;&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fcb6tzjrtskw4x43ngt3o.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fcb6tzjrtskw4x43ngt3o.jpg" alt=" " width="799" height="429"&gt;&lt;/a&gt;&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F4uv53eygwtde3qmk220k.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F4uv53eygwtde3qmk220k.jpg" alt=" " width="800" height="430"&gt;&lt;/a&gt;&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fpac26r1dnri0vby1ii3z.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fpac26r1dnri0vby1ii3z.jpg" alt=" " width="800" height="430"&gt;&lt;/a&gt;&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fx569qerxsuy7mdal9w6v.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fx569qerxsuy7mdal9w6v.jpg" alt=" " width="798" height="430"&gt;&lt;/a&gt;&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fvy8812otlbyppwd91qzl.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fvy8812otlbyppwd91qzl.jpg" alt=" " width="800" height="430"&gt;&lt;/a&gt;&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F93elshgyt37fryizftew.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F93elshgyt37fryizftew.jpg" alt=" " width="800" height="429"&gt;&lt;/a&gt;&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>javascript</category>
      <category>html</category>
      <category>node</category>
    </item>
  </channel>
</rss>
