<?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: Maya Shavin 🌷☕️🏡</title>
    <description>The latest articles on DEV Community by Maya Shavin 🌷☕️🏡 (@mayashavin).</description>
    <link>https://dev.to/mayashavin</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%2F146930%2Fd38cb498-9e87-434a-8510-0430a2c28600.jpeg</url>
      <title>DEV Community: Maya Shavin 🌷☕️🏡</title>
      <link>https://dev.to/mayashavin</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/mayashavin"/>
    <language>en</language>
    <item>
      <title>How To Create An Artistic And Personal Intro Video With ChatGPT And Nano Banana</title>
      <dc:creator>Maya Shavin 🌷☕️🏡</dc:creator>
      <pubDate>Thu, 11 Jun 2026 09:27:00 +0000</pubDate>
      <link>https://dev.to/mayashavin/how-to-create-an-artistic-and-personal-intro-video-with-chatgpt-and-nano-banana-5ehc</link>
      <guid>https://dev.to/mayashavin/how-to-create-an-artistic-and-personal-intro-video-with-chatgpt-and-nano-banana-5ehc</guid>
      <description>&lt;p&gt;&lt;em&gt;In this post, we will walkthrough the workflow, prompts and strategies needed to create a personalized AI intro video. We will also compare different execution approaches, and explore why a sketch-first workflow can lead to significantly better results compared to a standard, one-shot text-to-video generation.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  TL;DR
&lt;/h2&gt;

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

&lt;ul&gt;
&lt;li&gt;ChatGPT/Gemini Pro: For my personal identity sketch, storyboard and video sequence script.&lt;/li&gt;
&lt;li&gt;Nano Banana: Core video generation model.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Total time:&lt;/strong&gt; 1-2 hours&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Key Takeaways:&lt;/strong&gt; &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Creating a &lt;strong&gt;visual reference&lt;/strong&gt; first yields a significantly better result than relying solely on text prompts.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Always&lt;/strong&gt; add your photo, otherwise AI tends to hallucinate your likeness.&lt;/li&gt;
&lt;li&gt;Audio is not there yet. It's still more efficient to add music and voiceover &lt;strong&gt;manually&lt;/strong&gt; during post-production.&lt;/li&gt;
&lt;/ul&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%2Fres.cloudinary.com%2Fmayashavin%2Fimage%2Fupload%2Ff_auto%2Cq_auto%2Fv1780553726%2Farticles%2Fsket2flow" 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%2Fres.cloudinary.com%2Fmayashavin%2Fimage%2Fupload%2Ff_auto%2Cq_auto%2Fv1780553726%2Farticles%2Fsket2flow" alt="High-level overview diagram of the Sketch-First Workflow" width="1774" height="887"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The Challenge
&lt;/h2&gt;

&lt;p&gt;I have always wanted to a short intro video that feels personal, reflects my work and artistic side, while looking professional enough to reuse across different content channels, whether for my podcast, conference talks, or YouTube videos.&lt;/p&gt;

&lt;p&gt;While I have a background in illustration, creating a 10-second animated intro is a entirely different skill set. Storyboarding, animating transitions, and video editing are a whole different world. So, the idea stayed on my wishlist, until AI came.&lt;/p&gt;

&lt;p&gt;But does this mean I can simply ask an AI to create an intro video and magically get exactly what I want in a single click, like many LinkedIn posts claim? &lt;/p&gt;

&lt;p&gt;Not really. From beautiful demo videos to something that feels personal and represents you, it is a massive challenge.&lt;/p&gt;

&lt;h2&gt;
  
  
  Initial Approach: Why One-Shot Prompting Failed
&lt;/h2&gt;

&lt;p&gt;My first instinct was to take the most obvious way possible:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Write a descriptive prompt outlining the video I wanted, with the help of LLMs.&lt;/li&gt;
&lt;li&gt;Feed it to Nano Banana together with my profile photo.&lt;/li&gt;
&lt;li&gt;Get the video and review it.&lt;/li&gt;
&lt;/ul&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%2Fres.cloudinary.com%2Fmayashavin%2Fimage%2Fupload%2Ff_auto%2Cq_auto%2Cc_thumb%2Fv1780553726%2Farticles%2Fprompt2video" 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%2Fres.cloudinary.com%2Fmayashavin%2Fimage%2Fupload%2Ff_auto%2Cq_auto%2Cc_thumb%2Fv1780553726%2Farticles%2Fprompt2video" alt="Diagram showing the traditional failed one-shot prompting approach" width="1774" height="887"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Straightforward, right? &lt;/p&gt;

&lt;p&gt;Technically, the result was impressive. The animation were smooth and the rendering looked polished. The generated character was somewhat similar to my photo.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Except it didn't feel like me&lt;/strong&gt;. The character looked like a random person rather than Maya. There was little personality, too many visual elements competing for attention, and no clear focus on what I wanted to convey.&lt;/p&gt;

&lt;p&gt;&lt;/p&gt;

&lt;p&gt;At first, I assumed I needed a few more iterations to fine-tune the prompt. However, &lt;strong&gt;iteration is not free&lt;/strong&gt;. Video generation has a daily limits, and I realized I couldn't afford this costly and time-consuming process of trial and error.&lt;/p&gt;

&lt;p&gt;The primary issue, I started to suspect, wasn't about Nano Banana itself. It was more about the lack of context and visual reference for the model to understand the story I wanted to tell.&lt;/p&gt;

&lt;p&gt;That's when I decided to try things differently.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 1: Create A Visual Version Of Yourself First
&lt;/h2&gt;

&lt;p&gt;My number one rule of thumb:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;If you want to explain something, sketch it out&lt;/em&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Instead of jumping directly to video generation, I broke the process into smaller steps, starting with a static visual sketch as the primary reference for the rest of the generation workflow.&lt;/p&gt;

&lt;p&gt;Since I use ChatGPT frequently, it already has a sufficient understanding of my public profile, work, and interests. Rather than describing all of that from scratch, I focused my prompt on the following three key aspects:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Goal&lt;/strong&gt;: What should the image achieve?
&amp;gt; &lt;em&gt;Create a visual sketch that introduces me to the audience.&lt;/em&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Styles&lt;/strong&gt;: How should the image look like? What artistic style should it follow?
&amp;gt; &lt;em&gt;Speed paint/watercolor annotations, color sketching, sketch on white paper, technical doodles.&lt;/em&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Rules&lt;/strong&gt;: What constraints should the image follow?
&amp;gt; &lt;em&gt;Number of points, composition between elements, how to integrate different elements in the generated layout.&lt;/em&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;With those requirements, I came up with the following prompt:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;Based on what you know publicly about me and my attached profile image, generate an image as a visual layout to introduce about me to the audience.
Styles: 
&lt;span class="p"&gt;*&lt;/span&gt; Speed paint/ watercolor annotations
&lt;span class="p"&gt;*&lt;/span&gt; Color sketching
&lt;span class="p"&gt;*&lt;/span&gt; Sketch on white paper
&lt;span class="p"&gt;*&lt;/span&gt; Technical doodles 
Rules: 
&lt;span class="p"&gt;*&lt;/span&gt; Short, concise, memorable, maximum 4 concise bullet points
&lt;span class="p"&gt;*&lt;/span&gt; Preserve my actual likeness
&lt;span class="p"&gt;*&lt;/span&gt; Convert the photo into hand-drawn sketch/speed paint watercolor style, integrated proportionally balanced into the artwork.
&lt;span class="p"&gt;*&lt;/span&gt; Proper conference slide composition   
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The result wasn't perfect, but it was surprisingly good. While the generated image contains too much texts, inconsistent bullet icons, and a bigger head-shot than expected, the model captured the right aesthetic elements and theme very well. It accurately reflected my core focus areas, and the overall design style felt pretty close to what I had in mind.&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%2Fres.cloudinary.com%2Fmayashavin%2Fimage%2Fupload%2Ff_auto%2Cq_auto%2Fv1780553726%2Farticles%2Fbio" 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%2Fres.cloudinary.com%2Fmayashavin%2Fimage%2Fupload%2Ff_auto%2Cq_auto%2Fv1780553726%2Farticles%2Fbio" alt="MThe successful personalized artistic sketch generated by ChatGPT" width="1672" height="941"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; Having an maintained a public presence likely helped ChatGPT to infer the right details such as my website color palette and key themes. If you don't have a public profile, you can simple replace the line:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;What you know publicly about me&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;with a list of your background details or exact content.&lt;/p&gt;

&lt;p&gt;After a few iterations, I refined the sketch to a version I'm happy with. I finally had a visual anchor and a reference as the foundation for the next step in my workflow. I was no longer generating a random video, but a recognizable personal storyboard.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 2: Turning the Sketch Into a Story
&lt;/h2&gt;

&lt;p&gt;Creating a video is very different from creating a static image. Even within a short 10-second intro, you still need to think about the motion, transitions, and the elements sequence. In many ways, the process is close to planning a movie storyboard.&lt;/p&gt;

&lt;p&gt;I already have a visual representation of myself from step 1. The next step was figuring out how the sketch would come to life. For that, I asked ChatGPT to build out an intro sequence based on the sketch, using a below prompt:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;Generate a 10-second intro sequence script with hand-drawn sketch elements appearing progressively. The output should be optimized for Nano Banana video generation.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Grounding the Storyboard Prompt
&lt;/h3&gt;

&lt;p&gt;Before sending the generated storyboard to Nano Banana, I added a few more grounding rules to it, such as:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Who is this intro for (&lt;code&gt;@me&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;The sketch should be used as a style reference / storyboard&lt;/li&gt;
&lt;li&gt;The final animation should preserve the existing illustration style.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Important tip:&lt;/strong&gt; Nano Banana includes safeguards around generating content of real individuals. Explicitly referencing yourself using terms like &lt;code&gt;@me&lt;/code&gt;, &lt;code&gt;my sketch&lt;/code&gt;, or &lt;code&gt;my profile&lt;/code&gt; in the prompt ensures the content is based on your own uploaded image and helps the model stay aligned with your intent.&lt;/p&gt;

&lt;p&gt;Here is the shortened version of the &lt;a href="https://gist.github.com/mayashavin/c206372829dff9f0f72a77e985cc9bfc#file-video_prompt_example" rel="noopener noreferrer"&gt;final storyboard prompt&lt;/a&gt; I used:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;Timeline:
0:00 – 0:01
Blank textured paper.
“Hi!” is written by hand in black ink.

0:01 – 0:02
The text writes itself naturally:
“I’m Maya Shavin”
Sparkles and doodles animate around the name.

0:02 – 0:04
A watercolor splash appears center-left.
A portrait of Maya is gradually sketched from pencil lines into a fully colored illustration
Hair, face, clothing, and shading appear step-by-step.

0:05 – 0:06.5
Three watercolor badges and the associated text appear one after another.
Each line appears with realistic pen-writing animation.

0:06.5 – 0:08
Technology and podcast-related doodles appear.

0:09 – 0:10
Camera slowly zooms out.
The paper airplane flies across the bottom of the page leaving a dotted sketch trail.
Hold on the complete illustration for the final second.

End frame:
A finished hand-drawn portrait of Maya Shavin surrounded by technology, accessibility, AI, speaking, podcasting, and community-building illustrations.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Bringing It To Life with Nano Banana
&lt;/h2&gt;

&lt;p&gt;At this point, the workflow was finally complete. Instead of asking Nano Banana to generate a video from a single prompt, I fed it with three inputs:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The storyboard/script&lt;/strong&gt; describing the sequence of animations and elements.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The visual reference sketch&lt;/strong&gt; created in the first step.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;My original profile photo&lt;/strong&gt; as the source of truth for my appearance.&lt;/li&gt;
&lt;/ul&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%2Fres.cloudinary.com%2Fmayashavin%2Fimage%2Fupload%2Ff_auto%2Cq_auto%2Fv1780553726%2Farticles%2Fsket2flow" 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%2Fres.cloudinary.com%2Fmayashavin%2Fimage%2Fupload%2Ff_auto%2Cq_auto%2Fv1780553726%2Farticles%2Fsket2flow" alt="Text field component design" width="1774" height="887"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;As it turns out, the profile photo was crucial. While Nano Banana could interpret the illustrative sketch style, it struggled to extract my face accurately from the illustration alone. Without the real photo, the model sometimes hallucinated, drifting from one version of Maya to another. Providing both the sketch and the photo helped increase the consistency for the visual identity.&lt;/p&gt;

&lt;p&gt;But even then, the process wasn't entirely smooth. There were moments where the model rejected the generation because it interpreted the prompt as creating content about other people. Other times, the transition between frames felt awkward, or the generated character looked noticeably different from me. And working against the daily video generation quota added pressure to get it right within fewer iterations.&lt;/p&gt;

&lt;p&gt;Fortunately, thanks to the groundwork laid in the previous steps with the sketch and the storyboard, I didn't need many iterations before reaching the version I was happy with. &lt;/p&gt;

&lt;p&gt;&lt;/p&gt;

&lt;p&gt;The model followed a visual and narrative plan that I had defined, and that's my biggest takeaway from this experiment: &lt;em&gt;the quality of your final video is set long before you ever hit **Enter&lt;/em&gt;* on the video generation chat session*.&lt;/p&gt;

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

&lt;p&gt;This experiment gave me plenty of &lt;em&gt;"whoopsies"&lt;/em&gt; and &lt;em&gt;"aha"&lt;/em&gt; moments, such as:&lt;/p&gt;

&lt;h3&gt;
  
  
  Multi-Step Workflows Beats One-Shot Generation
&lt;/h3&gt;

&lt;p&gt;By breaking the process into smaller steps (&lt;code&gt;sketch → storyboard → video&lt;/code&gt;), the AI model was able to produce higher quality outputs than trying to generate the video directly from a single prompt. Additionally, this multi-step workflow approach forced me to think more clearly about the overall story I wanted to tell, while giving the model more context to work with in every stage. It’s a win-win, where I spend less time on regenerating the video, and more time on refining the parts that mattered.&lt;/p&gt;

&lt;h3&gt;
  
  
  Visual Reference Matters More Than Instructions
&lt;/h3&gt;

&lt;p&gt;I was really surprised by how much a strong visual reference could improve the results. Just like how we often use sketches to communicate ideas to engineers or designers, AI models seem to respond significantly better when having something visual to anchor on. &lt;/p&gt;

&lt;p&gt;&lt;em&gt;"A picture is worth a thousand words"&lt;/em&gt; seems to apply here as well. It does require more tokens, but it was worth the investment.&lt;/p&gt;

&lt;h3&gt;
  
  
  Consistency Is Still A Challenge
&lt;/h3&gt;

&lt;p&gt;Multi-modal AI models have gotten better in maintaining consistency, but there is a long way to go. Many times I found myself adding the same instruction repetitively:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;DO NOT CHANGE anything&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Without this rule, the model tend to occasionally change something already correct, like icons, my face, or the text unexpectedly in each iteration. Moreover, I noticed how the quality can degrade over time, such as broken texts, low resolution, especially towards the end of the day, or after a long session.&lt;/p&gt;

&lt;h3&gt;
  
  
  Different Account Types Can Produce Different Results
&lt;/h3&gt;

&lt;p&gt;One unexpected observation was how the outputs can be noticeably different between my Pro Personal account and Enterprise account, despite using the same model and input. The Enterprise account often produced a more polished and accurate video, while the Personal account usually leaned into more playful and visually engaging animation.&lt;/p&gt;

&lt;p&gt;Since this is a small experiment, I wouldn't jump to any conclusion from it. Still, it was interesting to observe the differences, and wonder what factors could contribute to it.&lt;/p&gt;

&lt;h3&gt;
  
  
  Unexpected Accuracy
&lt;/h3&gt;

&lt;p&gt;It's surprising how accurately ChatGPT could capture the theme in my work, from engineering to speaking, mentoring, and building. The sketch felt less like a generic AI image, and more like a visual summary of my personal brand. And seeing that sketch come to life in the final animation was no doubt my sweetest moment.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I Learnt About Creating Videos With AI
&lt;/h2&gt;

&lt;p&gt;I’m not a professional video creator, nor a prompt expert. That said, these below lessons are more of a personal experience than a standard AI practice:&lt;/p&gt;

&lt;h3&gt;
  
  
  AI Is Not Your Mind Reader
&lt;/h3&gt;

&lt;p&gt;The biggest mistake I made was expecting AI to magically interpret my intention and get everything right in the first try. Instead, I found it much more effective to treat AI as my collaborator, and iterate with it the same way I would with a human designer or editor. The more information I provided, the better the model could align with what I wanted to achieve.&lt;/p&gt;

&lt;h3&gt;
  
  
  Start With A Visual Representation
&lt;/h3&gt;

&lt;p&gt;Nano Banana is a powerful tool, but the final video was only as good as the inputs I provided. By investing time into the sketch, I leveled up the input for the storyboard phase, which in turn reduced ambiguity for the final video rendering phase. It's a chain reaction, where each stage builds upon the success of the one before.&lt;/p&gt;

&lt;p&gt;Moving forward, I would never skip this initial step of creating the static visual sketch. It acts as the foundation for the entire workflow, serving as shared reference for both the storyboard and the video generation process. Without it, the final outcome would likely rely on random generation luck, and that's not a good thing.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;On &lt;a href="https://open.spotify.com/episode/0XEHLR3gJLJAz9Cna7aVDo?si=WzhMybkDTfWgDWRvtKmnzA" rel="noopener noreferrer"&gt;Build With Maya&lt;/a&gt;, I share lessons from software engineering, AI, content creation, and the systems behind building things that scale.&lt;/em&gt;&lt;br&gt;
&lt;em&gt;If you enjoyed this breakdown, you might enjoy the podcast too.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

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

&lt;p&gt;It's amazing to discover how much a well-structured workflow shaped the final output. Tools like ChatGPT, Gemini, or Grok help generate the assets, but the workflow determines what level of the final product can reach.&lt;/p&gt;

&lt;p&gt;That said, my final note for those who are interested in trying AI video generation: start with your story first (both visually and narratively). It might make all the difference.&lt;/p&gt;

&lt;p&gt;👉 &lt;em&gt;Learn about Vue 3 and TypeScript with my new book &lt;a href="https://www.oreilly.com/library/view/learning-vue/9781492098812/" rel="noopener noreferrer"&gt;Learning Vue&lt;/a&gt;!&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;👉 &lt;em&gt;If you'd like to catch up with me sometimes, follow me on &lt;a href="https://x.com/MayaShavin" rel="noopener noreferrer"&gt;X&lt;/a&gt; | &lt;a href="https://www.linkedin.com/in/mayashavin" rel="noopener noreferrer"&gt;LinkedIn&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Like this post or find it helpful? Share it 👇🏼 😉&lt;/p&gt;

</description>
      <category>ai</category>
      <category>chatgpt</category>
      <category>gemini</category>
      <category>productivity</category>
    </item>
    <item>
      <title>What would I do without AI?</title>
      <dc:creator>Maya Shavin 🌷☕️🏡</dc:creator>
      <pubDate>Tue, 27 Jan 2026 07:39:07 +0000</pubDate>
      <link>https://dev.to/mayashavin/what-would-i-do-without-ai-51ik</link>
      <guid>https://dev.to/mayashavin/what-would-i-do-without-ai-51ik</guid>
      <description>&lt;p&gt;&lt;em&gt;That’s probably the most common sentence my colleagues and I say at work these days.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;AI didn’t arrive with a big announcement. It slowly crept into my daily engineering workflow—first as a coding assistant, then as a search tool, and eventually as something much closer to a thinking partner. Not a replacement. Not magic. And definitely not something I trust blindly.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;I’m a lead engineer working in large, complex systems, where context, history, and tradeoffs matter just as much as writing code. In that environment, AI turned out to be most valuable not when it does the work for me, but when it helps me move faster through noise—finding information, understanding unfamiliar code, and turning rough ideas into something concrete.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;This post isn’t about hype, fear, or “AI will replace engineers.” It’s a practical look at how I actually use AI today: where it saves me hours, where it still gets things wrong, and why I see it less as a threat and more as a rescuer.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  AI as Your Trusted Slackbot
&lt;/h2&gt;

&lt;p&gt;I love Slack. Not because I work for Salesforce. I used Slack long before that. I’ve worked with Teams back in my Microsoft days, but Slack is on another level.&lt;/p&gt;

&lt;p&gt;And with recent Slackbot updates, Slack is no longer "communication" app. It’s a real assistant.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://slack.com/features/slackbot" rel="noopener noreferrer"&gt;Slackbot&lt;/a&gt; now searches across Slack, Confluence, Google Drive, Canvas, GUS (Salesforce’s Jira-wannabe), and more. It doesn’t just return links — it consolidates information and generates a structured answer based on what I ask.&lt;/p&gt;

&lt;p&gt;One recent example: my self-performance evaluation.&lt;/p&gt;

&lt;p&gt;I do keep a document where I track contributions and progress, but let’s be honest, no one captures everything. But what got captured, automaticall and silently, was my Slack activity: discussions, design reviews, investigations, decisions.&lt;/p&gt;

&lt;p&gt;So I asked Slackbot to summarize my contributions.&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%2Fres.cloudinary.com%2Fmayashavin%2Fimage%2Fupload%2Ff_auto%2Cq_auto%2Fv1675680195%2Farticles%2Fllm%2FScreenshot_2026-01-26_at_9.19.05" 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%2Fres.cloudinary.com%2Fmayashavin%2Fimage%2Fupload%2Ff_auto%2Cq_auto%2Fv1675680195%2Farticles%2Fllm%2FScreenshot_2026-01-26_at_9.19.05" alt="Screenshot of Slackbot summarizing contributions" width="898" height="302"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;And boom! Out came a detailed list of work, grouped by features and discussions, with a clear impact summary, references, and footnotes. My job after that was pretty straightforward: merge it with my own notes and pass it through another LLM agent to polish it according to the evaluation template.&lt;/p&gt;

&lt;p&gt;Is it perfect? No.&lt;/p&gt;

&lt;p&gt;It once pulled demo slides from a “Maya” who &lt;strong&gt;was… not me&lt;/strong&gt; (&lt;em&gt;turned out it was a made-up Maya as a demo case&lt;/em&gt; 😄). It still requires my review to avoid hallucinations or incorrect context.&lt;/p&gt;

&lt;p&gt;But saving hours of digging through old docs and Slack threads? That alone is a &lt;strong&gt;huge&lt;/strong&gt; win.&lt;/p&gt;

&lt;h2&gt;
  
  
  Code Analyzer &amp;amp; Assistant
&lt;/h2&gt;

&lt;p&gt;Of course, we can’t talk about AI without talking about code.&lt;/p&gt;

&lt;p&gt;We’ve moved far beyond &lt;em&gt;copilot that writes a function&lt;/em&gt;. With tools like &lt;a href="https://cursor.com/" rel="noopener noreferrer"&gt;Cursor&lt;/a&gt;, &lt;a href="https://claude.com/product/claude-code" rel="noopener noreferrer"&gt;Claude Code&lt;/a&gt; and MCP-based agents, AI now helps me understand large codebases.&lt;/p&gt;

&lt;p&gt;Not vibe coding.&lt;/p&gt;

&lt;p&gt;Not replacing engineers.&lt;/p&gt;

&lt;p&gt;But acting as a powerful assistant.&lt;/p&gt;

&lt;p&gt;I can ask questions like:&lt;/p&gt;

&lt;p&gt;&lt;em&gt;“Who calls this function?”&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;“Map the flow from this UI component to the backend.”&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;“Which part of the code triggers this LLM orchestration?”&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Within minutes, it maps relationships across modules in a &lt;strong&gt;massive monolith&lt;/strong&gt; codebase and explains them in plain language—saving hours (or days) of digging through unfamiliar code. This is especially helpful when working in languages or systems that aren’t your home turf (hello, Java).&lt;/p&gt;

&lt;p&gt;I felt this most during a recent hackathon. Within 24 hours, our team reused existing internal UI and server features from multiple teams, layered our logic on top, and aimed to get as close to production-ready as possible. We also used AI as part of the product itself, helping customers reduce onboarding time from months to hours.&lt;/p&gt;

&lt;p&gt;The result?&lt;/p&gt;

&lt;p&gt;New technical knowledge unlocked, a working demo with real data, and a hackathon award.&lt;/p&gt;

&lt;h2&gt;
  
  
  Professional Content Editor for Professional Discussions
&lt;/h2&gt;

&lt;p&gt;One underrated use of AI: leveling up professional communication.&lt;/p&gt;

&lt;p&gt;Writing technical design docs, business justifications, RCA reports, or even a Slack announcement used to be hard, especially as non-native English speakers. Engineers aren’t trained writers or marketers, and it shows.&lt;/p&gt;

&lt;p&gt;With tools like ChatGPT or &lt;a href="https://notebooklm.google/" rel="noopener noreferrer"&gt;Gemini&lt;/a&gt;, I can now brainstorm ideas, structure my thoughts, draft proposals, get them refined, polished, and critiqued.&lt;/p&gt;

&lt;p&gt;The key is asking for criticism. If you don’t, AI will happily agree with everything you write.&lt;/p&gt;

&lt;p&gt;This isn’t limited to design docs. It’s just as useful for documentation, RCAs, or any message that goes beyond your immediate team.&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%2Fres.cloudinary.com%2Fmayashavin%2Fimage%2Fupload%2Ff_auto%2Cq_auto%2Fv1675680195%2Farticles%2Fllm%2Fai_editor" 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%2Fres.cloudinary.com%2Fmayashavin%2Fimage%2Fupload%2Ff_auto%2Cq_auto%2Fv1675680195%2Farticles%2Fllm%2Fai_editor" alt="Screenshot of a relaxing engineer sitting with an AI working" width="1536" height="1024"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;And yes, Slack announcements too. Feed it your intent, and it’ll give you a version that sounds like you, just clearer and without grammar issues.&lt;/p&gt;

&lt;p&gt;I even built an RCA agent that generates detailed bug reports from investigations, Slack threads, and standard templates, ready for review and publishing. It also won a hackathon.&lt;/p&gt;

&lt;h2&gt;
  
  
  Pair Programmer That Turns Ideas Into Tools
&lt;/h2&gt;

&lt;p&gt;One of the biggest shifts for me is how AI helps turn ideas into production tools.&lt;/p&gt;

&lt;p&gt;Call it vibe coding if you want. But I use it to draft internal tools that boost productivity: setting up dev environments, provisioning mobile simulators, automating workflows with Python and Bash. Those things that used to take weeks of trial and error, now take a day or less, with fast feedback loops. From there, I refine and improve the solution myself.&lt;/p&gt;

&lt;p&gt;Since leaning into this, I’ve released several small tools that help my team move faster and make impact sooner. And it doesn't stop there.&lt;/p&gt;

&lt;h2&gt;
  
  
  A Virtual Slack On-Call Engineer
&lt;/h2&gt;

&lt;p&gt;Working across large systems and multiple projects usually means monitoring countless Slack channels—supporting product managers, solution architects, customer support, and other engineers. Many questions are repetitive or already answered in documentation or past discussions, but it’s often faster for people to tag the on-call engineer than to search for them. For us as engineers, constantly switching contexts across channels is expensive and inefficient.&lt;/p&gt;

&lt;p&gt;By integrating AI agents into Slack, I can set up a virtual on-call engineer that monitors specific channels, is grounded in documentation, known issues, and discussion history, and continuously indexes new information. It answers common questions automatically and escalates only complex cases, reducing interruptions while still ensuring timely, accurate responses.&lt;/p&gt;

&lt;p&gt;With this &lt;a href="https://slack.com/blog/transformation/engineering-agent-slack-productivity" rel="noopener noreferrer"&gt;Engineer Agent&lt;/a&gt;, my team and I can focus on high-impact work without being bogged down by repetitive queries.&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%2Fres.cloudinary.com%2Fmayashavin%2Fimage%2Fupload%2Ff_auto%2Cq_auto%2Fv1675680195%2Farticles%2Fllm%2FScreenshot_2026-01-26_at_9.04.43" 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%2Fres.cloudinary.com%2Fmayashavin%2Fimage%2Fupload%2Ff_auto%2Cq_auto%2Fv1675680195%2Farticles%2Fllm%2FScreenshot_2026-01-26_at_9.04.43" alt="Screenshot of Engineering Agent in Slack with Agentforce" width="357" height="144"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;p&gt;Will AI replace my job one day?&lt;/p&gt;

&lt;p&gt;Maybe. Just like my role could be replaced by a younger engineer or by the industry evolving. No one is irreplaceable, especially at work. Even spaghetti code won’t save you forever.&lt;/p&gt;

&lt;p&gt;But should I worry? I don't know. What I do know is this: AI helps me onboard faster, cut through noise, and focus on what actually matters—building better products. When AI is wrong, it’s on me to notice. When it suggests a shortcut, it’s on me to decide if it’s the right one.&lt;/p&gt;

&lt;p&gt;I don’t see AI as a junior engineer or a looming threat. I see it as a companion—reducing friction, speeding things up, and helping me focus on what actually matters.&lt;/p&gt;

&lt;p&gt;As long as I stay in control of the decisions, that’s a tradeoff I’m happy to make.&lt;/p&gt;

&lt;p&gt;👉 &lt;em&gt;If you’d like to continue the conversation, you can find me on &lt;a href="https://x.com/MayaShavin" rel="noopener noreferrer"&gt;X&lt;/a&gt; or &lt;a href="https://www.linkedin.com/in/mayashavin" rel="noopener noreferrer"&gt;LinkedIn&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Found this post helpful? Give it a like or share it with someone who might need it 👇🏼&lt;/p&gt;

</description>
      <category>ai</category>
      <category>aitools</category>
      <category>engineering</category>
      <category>productivity</category>
    </item>
    <item>
      <title>Making a custom input counter component accessible</title>
      <dc:creator>Maya Shavin 🌷☕️🏡</dc:creator>
      <pubDate>Wed, 16 Apr 2025 07:32:43 +0000</pubDate>
      <link>https://dev.to/mayashavin/making-a-custom-input-counter-component-accessible-1e3m</link>
      <guid>https://dev.to/mayashavin/making-a-custom-input-counter-component-accessible-1e3m</guid>
      <description>&lt;p&gt;&lt;em&gt;This blog post is not a tutorial on how to build an input counter component, but rather a discussion on how to structure the component for accessibility and good practice.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The problem of overlapping elements
&lt;/h2&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%2Fres.cloudinary.com%2Fmayashavin%2Fimage%2Fupload%2Fq_auto%2Cf_auto%2Fv1744754170%2Farticles%2FCSS%2FCounter" 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%2Fres.cloudinary.com%2Fmayashavin%2Fimage%2Fupload%2Fq_auto%2Cf_auto%2Fv1744754170%2Farticles%2FCSS%2FCounter" alt="Counter component" width="398" height="210"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;We recently encountered an input counter component that failed the accessibility test performed by &lt;a href="https://www.evinced.com/products/flow-analyzer-for-web" rel="noopener noreferrer"&gt;Evinced Web Flow Analyzer&lt;/a&gt;. The reported error was &lt;em&gt;"There is an overlapping between interactive elements"&lt;/em&gt; involving the increment &lt;code&gt;button&lt;/code&gt; and the &lt;code&gt;input&lt;/code&gt; field. This issue surprised us because, at first glance, the component appeared accessible, with keyboard navigation, voiceover, and focus state working as expected during manual testing.&lt;/p&gt;

&lt;p&gt;To investigate, we examined the component's implementation. Initially, its HTML template seemed well-structured, with the increment &lt;code&gt;button&lt;/code&gt;, &lt;code&gt;input&lt;/code&gt; field, and decrement &lt;code&gt;button&lt;/code&gt; nested as sibling elements within a &lt;code&gt;div&lt;/code&gt; container:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"container"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;button&lt;/span&gt; &lt;span class="na"&gt;aria-label=&lt;/span&gt;&lt;span class="s"&gt;"Increment value"&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"btn incr"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;+&lt;span class="nt"&gt;&amp;lt;/button&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;input&lt;/span&gt; &lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"number"&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"input"&lt;/span&gt; &lt;span class="na"&gt;aria-label=&lt;/span&gt;&lt;span class="s"&gt;"Counter value"&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"counter"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;button&lt;/span&gt; &lt;span class="na"&gt;aria-label=&lt;/span&gt;&lt;span class="s"&gt;"Decrement value"&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"btn decr"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;-&lt;span class="nt"&gt;&amp;lt;/button&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;However, the component's CSS styling revealed a different story. The increment and decrement buttons were absolutely positioned, while the &lt;code&gt;input&lt;/code&gt; field was styled with &lt;code&gt;width: calc(100% - 60px)&lt;/code&gt; and horizontal padding (&lt;code&gt;padding-inline: 30px&lt;/code&gt;), aligning with the button widths:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="nc"&gt;.container&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c"&gt;/**... */&lt;/span&gt;
  &lt;span class="nl"&gt;width&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;150px&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;position&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;relative&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nc"&gt;.btn&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;position&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="nb"&gt;absolute&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;height&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;100%&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;min-width&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;30px&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nc"&gt;.incr&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;left&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1px&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nc"&gt;.decr&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;right&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nc"&gt;.counter&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;border&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="nb"&gt;none&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;padding&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;5px&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="py"&gt;padding-inline&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;30px&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;width&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;calc&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;100%&lt;/span&gt; &lt;span class="n"&gt;-&lt;/span&gt; &lt;span class="m"&gt;60px&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nl"&gt;text-align&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;center&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;While visually effective, this CSS introduced poor positioning practices:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Using &lt;code&gt;position: absolute&lt;/code&gt; removes buttons from the normal document flow, forcing the browser into additional layout computations.&lt;/li&gt;
&lt;li&gt;Button positions aren't relative to the &lt;code&gt;input&lt;/code&gt; field, causing additional complexity in CSS rules when adjusting font sizes, widths, or container dimensions.&lt;/li&gt;
&lt;li&gt;Absolute positioning complicates RTL/LTR language support (such as Arabic or Hebrew), requiring extra CSS adjustments.&lt;/li&gt;
&lt;li&gt;There is no actual necessity for absolute positioning since elements are already correctly structured in HTML.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Additionally, the &lt;code&gt;left: 1px&lt;/code&gt; rule caused a &lt;code&gt;1px&lt;/code&gt; overlap of the increment &lt;code&gt;button&lt;/code&gt; on the &lt;code&gt;input&lt;/code&gt; field, directly triggering the accessibility error.&lt;/p&gt;

&lt;h2&gt;
  
  
  Fixing overlapping and RTL issues using CSS Flexbox
&lt;/h2&gt;

&lt;p&gt;A simple fix would be adjusting &lt;code&gt;left: 1px&lt;/code&gt; to &lt;code&gt;left: 0&lt;/code&gt;, removing the immediate overlap. However, this would only patch the bug without addressing the root issue (&lt;code&gt;position: absolute&lt;/code&gt;).&lt;/p&gt;

&lt;p&gt;A better solution is using CSS Flexbox for the container, eliminating absolute positioning entirely. The updated CSS rules remove unnecessary properties (&lt;code&gt;left&lt;/code&gt;, &lt;code&gt;right&lt;/code&gt;, &lt;code&gt;height: 100%&lt;/code&gt;, &lt;code&gt;padding&lt;/code&gt;, and &lt;code&gt;calc()&lt;/code&gt;):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="nc"&gt;.container&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;border&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1px&lt;/span&gt; &lt;span class="nb"&gt;solid&lt;/span&gt; &lt;span class="n"&gt;lightgray&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;padding&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;display&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;flex&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nc"&gt;.btn&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;min-width&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;30px&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nt"&gt;input&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;border&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="nb"&gt;none&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;padding&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;5px&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;text-align&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;center&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;However, applying Flexbox introduces an overflow issue for the &lt;code&gt;input&lt;/code&gt; field, as form elements inherently have a default &lt;code&gt;width&lt;/code&gt; set by browser stylesheets.&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%2Fres.cloudinary.com%2Fmayashavin%2Fimage%2Fupload%2Fq_auto%2Cf_auto%2Fv1744754170%2Farticles%2FCSS%2FCounter_Overflow" 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%2Fres.cloudinary.com%2Fmayashavin%2Fimage%2Fupload%2Fq_auto%2Cf_auto%2Fv1744754170%2Farticles%2FCSS%2FCounter_Overflow" alt="Counter component with an overflowed input" width="508" height="220"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Resolving input overflow in Flexbox
&lt;/h3&gt;

&lt;p&gt;To address the overflow, set &lt;code&gt;min-width: 0&lt;/code&gt; and &lt;code&gt;flex: 1&lt;/code&gt; on the &lt;code&gt;input&lt;/code&gt; field. This ensures it dynamically shrinks or grows within the available container space:&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;input&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c"&gt;/** ... */&lt;/span&gt;
  &lt;span class="nl"&gt;min-width&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;flex&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1&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;Alternatively, using a &lt;code&gt;fieldset&lt;/code&gt; instead of a &lt;code&gt;div&lt;/code&gt; container could omit the &lt;code&gt;min-width: 0&lt;/code&gt; property. This provides a semantically meaningful solution beneficial for accessibility.&lt;/p&gt;

&lt;h3&gt;
  
  
  Supporting RTL/LTR languages
&lt;/h3&gt;

&lt;p&gt;With Flexbox, the component inherently supports RTL/LTR languages without additional CSS rules. To verify this, use the &lt;code&gt;dir&lt;/code&gt; attribute on the container:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"container"&lt;/span&gt; &lt;span class="na"&gt;dir=&lt;/span&gt;&lt;span class="s"&gt;"rtl"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;button&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"increment"&lt;/span&gt; &lt;span class="na"&gt;aria-label=&lt;/span&gt;&lt;span class="s"&gt;"Increment value"&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"btn incr"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;+&lt;span class="nt"&gt;&amp;lt;/button&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;input&lt;/span&gt; &lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"number"&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"input"&lt;/span&gt; &lt;span class="na"&gt;aria-label=&lt;/span&gt;&lt;span class="s"&gt;"Counter value"&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"counter"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;button&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"decrement"&lt;/span&gt; &lt;span class="na"&gt;aria-label=&lt;/span&gt;&lt;span class="s"&gt;"Decrement value"&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"btn decr"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;-&lt;span class="nt"&gt;&amp;lt;/button&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This approach automatically swaps the increment and decrement buttons appropriately, maintaining a centered input field.&lt;/p&gt;


  





&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;p&gt;In this blog post, we addressed accessibility issues caused by overlapping elements in an input counter component due to absolute positioning. By replacing this with CSS Flexbox and adjusting the CSS style of &lt;code&gt;input&lt;/code&gt; field, we significantly improved the component's accessibility and flexibility, providing RTL/LTR language support out of the box.&lt;/p&gt;

&lt;p&gt;👉 &lt;em&gt;Learn about Vue 3 and TypeScript with my new book &lt;a href="https://www.oreilly.com/library/view/learning-vue/9781492098812/" rel="noopener noreferrer"&gt;Learning Vue&lt;/a&gt;!&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;👉 &lt;em&gt;Follow me on &lt;a href="https://x.com/MayaShavin" rel="noopener noreferrer"&gt;X&lt;/a&gt; | &lt;a href="https://www.linkedin.com/in/mayashavin" rel="noopener noreferrer"&gt;LinkedIn&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Like this post or find it helpful? Share it 👇🏼 😉&lt;/p&gt;

</description>
      <category>css</category>
      <category>javascript</category>
      <category>webdev</category>
      <category>a11y</category>
    </item>
    <item>
      <title>Managing Multi-Step Forms in Vue with XState</title>
      <dc:creator>Maya Shavin 🌷☕️🏡</dc:creator>
      <pubDate>Wed, 12 Feb 2025 08:48:43 +0000</pubDate>
      <link>https://dev.to/mayashavin/managing-multi-step-forms-in-vue-with-xstate-5fhj</link>
      <guid>https://dev.to/mayashavin/managing-multi-step-forms-in-vue-with-xstate-5fhj</guid>
      <description>&lt;p&gt;&lt;em&gt;State management is essential for any application, ensuring a consistent data flow and predictable behavior. Choosing the right approach impacts scalability and maintainability.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;In this article, we'll explore how to refactor a multi-step sign-up form in Vue.js to use XState, a state management library based on finite state machines, making state handling more structured and efficient.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Table of Contents
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Table of Contents&lt;/li&gt;
&lt;li&gt;The challenge of managing state a multi-step sign-up form wizard&lt;/li&gt;
&lt;li&gt;What Are State Machines &amp;amp; XState?&lt;/li&gt;
&lt;li&gt;
Building a sign-up form wizard machine with XState

&lt;ul&gt;
&lt;li&gt;Defining the State Machine&lt;/li&gt;
&lt;li&gt;Integrating XState in the Component&lt;/li&gt;
&lt;li&gt;Adding Asynchronous Submission&lt;/li&gt;
&lt;li&gt;Updating the UI for Submission Status&lt;/li&gt;
&lt;li&gt;Passing Data to submitForm&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;Mapping Component's Data to Context&lt;/li&gt;

&lt;li&gt;Resources&lt;/li&gt;

&lt;li&gt;

Summary

&lt;ul&gt;
&lt;li&gt;What's next?&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;/ul&gt;

&lt;h2&gt;
  
  
  The challenge of managing state a multi-step sign-up form wizard
&lt;/h2&gt;

&lt;p&gt;Let's say we have a sign-up form built as a two-step wizard: one step to collect the user's name and another for their email address. We'll call this component &lt;code&gt;SignupFormWizard&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;template&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;form&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"form-main-view"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;v-if=&lt;/span&gt;&lt;span class="s"&gt;"isNameStep"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;label&lt;/span&gt; &lt;span class="na"&gt;for=&lt;/span&gt;&lt;span class="s"&gt;"name"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Name&lt;span class="nt"&gt;&amp;lt;/label&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;input&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"name"&lt;/span&gt; &lt;span class="na"&gt;placeholder=&lt;/span&gt;&lt;span class="s"&gt;"Name"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;v-else-if=&lt;/span&gt;&lt;span class="s"&gt;"isEmailStep"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;label&lt;/span&gt; &lt;span class="na"&gt;for=&lt;/span&gt;&lt;span class="s"&gt;"email"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Email&lt;span class="nt"&gt;&amp;lt;/label&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;input&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"email"&lt;/span&gt; &lt;span class="na"&gt;placeholder=&lt;/span&gt;&lt;span class="s"&gt;"Email"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;v-else-if=&lt;/span&gt;&lt;span class="s"&gt;"isSubmitStep"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;p&amp;gt;&lt;/span&gt;Submitting...&lt;span class="nt"&gt;&amp;lt;/p&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;div&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;button&lt;/span&gt; &lt;span class="err"&gt;@&lt;/span&gt;&lt;span class="na"&gt;click=&lt;/span&gt;&lt;span class="s"&gt;"prev"&lt;/span&gt; &lt;span class="na"&gt;v-if=&lt;/span&gt;&lt;span class="s"&gt;"isEmailStep"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Prev&lt;span class="nt"&gt;&amp;lt;/button&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;button&lt;/span&gt; &lt;span class="err"&gt;@&lt;/span&gt;&lt;span class="na"&gt;click=&lt;/span&gt;&lt;span class="s"&gt;"next"&lt;/span&gt; &lt;span class="na"&gt;v-if=&lt;/span&gt;&lt;span class="s"&gt;"isNameStep"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Next&lt;span class="nt"&gt;&amp;lt;/button&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;button&lt;/span&gt; &lt;span class="err"&gt;@&lt;/span&gt;&lt;span class="na"&gt;click=&lt;/span&gt;&lt;span class="s"&gt;"submit"&lt;/span&gt; &lt;span class="na"&gt;v-else-if=&lt;/span&gt;&lt;span class="s"&gt;"isEmailStep"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Submit&lt;span class="nt"&gt;&amp;lt;/button&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/form&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/template&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here's how we manage the local state in the &lt;code&gt;script&lt;/code&gt; section:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;script &lt;/span&gt;&lt;span class="na"&gt;setup&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;ref&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;reactive&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;computed&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;vue&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;formData&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;reactive&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;email&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;''&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;step&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;ref&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;isNameStep&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;computed&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;step&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="mi"&gt;1&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;isEmailStep&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;computed&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;step&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="mi"&gt;2&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;isSubmitStep&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;computed&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;step&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="mi"&gt;3&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;prev&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;step&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;step&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="o"&gt;--&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;next&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;step&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;step&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;submit&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;step&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/script&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Our form behaves as follows:&lt;/p&gt;


  


&lt;p&gt;In this implementation, we track the form's step using a step variable with &lt;code&gt;ref()&lt;/code&gt;, store form data in a &lt;code&gt;reactive&lt;/code&gt; object, and define functions to handle navigation between steps and submission.&lt;/p&gt;

&lt;p&gt;For a simple form like this, this approach works fine. However, as the form grows in complexity, state management becomes more challenging. If we need to add more steps, handle asynchronous submission (including error and success states), or introduce additional logic, the code can quickly become difficult to maintain.&lt;/p&gt;

&lt;p&gt;So, how can we simplify this process while making it more predictable and scalable? Let's explore a better approach.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Are State Machines &amp;amp; XState?
&lt;/h2&gt;

&lt;p&gt;State machines are models that define a system's behavior by breaking it down into a finite number of states. Transitions between these states occur one at a time, triggered by predefined events.&lt;/p&gt;

&lt;p&gt;In simple terms, states act like nodes in a graph, while events function as edges connecting these nodes. Every state and transition is explicitly defined.&lt;/p&gt;

&lt;p&gt;State machines are particularly useful for managing complex systems because they provide a structured and predictable way to handle application state. A classic example is a traffic light system, which consists of three main states: red, yellow, and green. The system follows a strict sequence—transitioning from &lt;code&gt;red&lt;/code&gt; to &lt;code&gt;yellow&lt;/code&gt;, then from &lt;code&gt;yellow&lt;/code&gt; to &lt;code&gt;green&lt;/code&gt;, and back—using a &lt;code&gt;next()&lt;/code&gt; event that can be scheduled.&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%2Fres.cloudinary.com%2Fmayashavin%2Fimage%2Fupload%2Fq_auto%2Ff_auto%2Fv1727777568%2Farticles%2Fxstate%2Ftraffic_lights.gif" 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%2Fres.cloudinary.com%2Fmayashavin%2Fimage%2Fupload%2Fq_auto%2Ff_auto%2Fv1727777568%2Farticles%2Fxstate%2Ftraffic_lights.gif" alt="List of cards displayed in browser with minimum CSS" width="800" height="437"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Building on this concept, &lt;a href="https://stately.ai/docs/quick-start" rel="noopener noreferrer"&gt;XState&lt;/a&gt; provides a declarative and predictable approach to state management in TypeScript. It can integrate with modern front-end frameworks like React and Vue through dedicated packages such as &lt;code&gt;@xstate/react&lt;/code&gt; and &lt;code&gt;@xstate/vue&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Next, let's explore how we can refactor our &lt;code&gt;SignupFormWizard&lt;/code&gt; component to use XState.&lt;/p&gt;

&lt;h2&gt;
  
  
  Building a sign-up form wizard machine with XState
&lt;/h2&gt;

&lt;p&gt;To integrate XState, we install the necessary packages:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install &lt;/span&gt;xstate @xstate/vue
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Defining the State Machine
&lt;/h3&gt;

&lt;p&gt;We create a new file, &lt;code&gt;machines/signUpMachine.js&lt;/code&gt;, and set up the state machine:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;setup&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;xstate&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;signUpMachineConfig&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;setup&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;signUpMachine&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;signUpMachine&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;signUpMachineConfig&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createMachine&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;initial&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;name&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;context&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{},&lt;/span&gt;
  &lt;span class="na"&gt;states&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; 
      &lt;span class="na"&gt;on&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;NEXT&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;email&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; 
    &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="na"&gt;email&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; 
      &lt;span class="na"&gt;on&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;PREV&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;name&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;SUBMIT&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;onsubmit&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; 
    &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="na"&gt;onsubmit&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{},&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This defines a three-state machine (&lt;code&gt;name&lt;/code&gt;, &lt;code&gt;email&lt;/code&gt;, and &lt;code&gt;onsubmit&lt;/code&gt;) with transitions triggered by &lt;code&gt;NEXT&lt;/code&gt;, &lt;code&gt;PREV&lt;/code&gt;, and &lt;code&gt;SUBMIT&lt;/code&gt; events.&lt;/p&gt;

&lt;h3&gt;
  
  
  Integrating XState in the Component
&lt;/h3&gt;

&lt;p&gt;In &lt;code&gt;SignupFormWizard.vue&lt;/code&gt;, we connect the machine using &lt;code&gt;useMachine()&lt;/code&gt; from &lt;code&gt;@xstate/vue&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;script &lt;/span&gt;&lt;span class="na"&gt;setup&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;useMachine&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@xstate/vue&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;signUpMachine&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;../machines/signUpMachine&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;snapshot&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;send&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useMachine&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;signUpMachine&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/script&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We then replace the local &lt;code&gt;step&lt;/code&gt; variable with &lt;code&gt;state.matches()&lt;/code&gt; for checking the active state:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;isNameStep&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;computed&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;matches&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;name&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;isEmailStep&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;computed&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;matches&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;email&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;isSubmitStep&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;computed&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;matches&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;onsubmit&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We also refactor navigation methods to use &lt;code&gt;send()&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;prev&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;PREV&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;next&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;NEXT&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;submit&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;SUBMIT&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;By doing so, we keeps the UI the same but makes state management more predictable.&lt;/p&gt;

&lt;p&gt;Next, let's add more features to our form machine, such as asynchronous submission.&lt;/p&gt;

&lt;h3&gt;
  
  
  Adding Asynchronous Submission
&lt;/h3&gt;

&lt;p&gt;To handle an asynchronous function and its states, we use &lt;code&gt;fromPromise&lt;/code&gt; helper as follows:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;fromPromise&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;xstate&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;submitForm&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;fromPromise&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// Handle submission logic&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;fromPromise()&lt;/code&gt; then creates an XState actor that triggers the &lt;code&gt;onDone&lt;/code&gt; event when the async function is resolved, and &lt;code&gt;onError&lt;/code&gt; event when rejected.&lt;/p&gt;

&lt;p&gt;We then modify the &lt;code&gt;onsubmit&lt;/code&gt; state to &lt;code&gt;invoke&lt;/code&gt; this function:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;signUpMachine&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;signUpMachineConfig&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createMachine&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="c1"&gt;//...&lt;/span&gt;
  &lt;span class="na"&gt;states&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;onsubmit&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;invoke&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;src&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;submitForm&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;onDone&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;success&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;onError&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;error&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="na"&gt;success&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;on&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;RESET&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;name&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="na"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;on&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;RETRY&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;onsubmit&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In this setup, we also add &lt;code&gt;RETRY&lt;/code&gt; and &lt;code&gt;RESET&lt;/code&gt; events to &lt;code&gt;error&lt;/code&gt; and &lt;code&gt;success&lt;/code&gt; states, respectively, to handle the retry and reset functionalities.&lt;/p&gt;

&lt;p&gt;Next, let's modify our component to reflect these changes.&lt;/p&gt;

&lt;h3&gt;
  
  
  Updating the UI for Submission Status
&lt;/h3&gt;

&lt;p&gt;We modify the &lt;code&gt;template&lt;/code&gt; to display &lt;code&gt;success&lt;/code&gt; and &lt;code&gt;error&lt;/code&gt; states:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;template&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;form&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"form-main-view"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="c"&gt;&amp;lt;!--...--&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;v-else-if=&lt;/span&gt;&lt;span class="s"&gt;"isSuccess"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;p&amp;gt;&lt;/span&gt;Form submitted successfully!&lt;span class="nt"&gt;&amp;lt;/p&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;button&lt;/span&gt; &lt;span class="err"&gt;@&lt;/span&gt;&lt;span class="na"&gt;click=&lt;/span&gt;&lt;span class="s"&gt;"reset"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Reset&lt;span class="nt"&gt;&amp;lt;/button&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;v-else-if=&lt;/span&gt;&lt;span class="s"&gt;"isError"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;p&amp;gt;&lt;/span&gt;Submission failed&lt;span class="nt"&gt;&amp;lt;/p&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;button&lt;/span&gt; &lt;span class="err"&gt;@&lt;/span&gt;&lt;span class="na"&gt;click=&lt;/span&gt;&lt;span class="s"&gt;"retry"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Retry&lt;span class="nt"&gt;&amp;lt;/button&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
    &lt;span class="c"&gt;&amp;lt;!--...--&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/form&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/template&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And we update the component logic:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;isSuccess&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;computed&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;matches&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;success&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;isError&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;computed&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;matches&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;error&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;reset&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;RESET&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;retry&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;RETRY&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With these changes, users can now retry or reset the form after submission.&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%2Fres.cloudinary.com%2Fmayashavin%2Fimage%2Fupload%2Fq_auto%2Ff_auto%2Fv1738789692%2Farticles%2Fxstate%2Fform_reset.gif" 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%2Fres.cloudinary.com%2Fmayashavin%2Fimage%2Fupload%2Fq_auto%2Ff_auto%2Fv1738789692%2Farticles%2Fxstate%2Fform_reset.gif" alt="Flow of the form with successful submission" width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Passing Data to submitForm
&lt;/h3&gt;

&lt;p&gt;To store and pass form data, update the machine’s &lt;code&gt;context&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;signUpMachine&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;signupMachineConfigs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createMachine&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="c1"&gt;//...&lt;/span&gt;
  &lt;span class="na"&gt;context&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;formData&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{},&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="c1"&gt;//...&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then, we update the &lt;code&gt;SUBMIT&lt;/code&gt; event to perform side actions and update the context data with &lt;code&gt;assign()&lt;/code&gt; method:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;signUpMachine&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;signUpMachineConfig&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createMachine&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="c1"&gt;//...&lt;/span&gt;
  &lt;span class="na"&gt;states&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;//...&lt;/span&gt;
    &lt;span class="na"&gt;email&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;on&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;//...&lt;/span&gt;
        &lt;span class="na"&gt;SUBMIT&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="na"&gt;target&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;onsubmit&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="na"&gt;actions&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;assign&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
            &lt;span class="na"&gt;formData&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;formData&lt;/span&gt; &lt;span class="p"&gt;}),&lt;/span&gt;
          &lt;span class="p"&gt;}),&lt;/span&gt;
        &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In &lt;code&gt;SignupFormWizard&lt;/code&gt;, we modify the &lt;code&gt;submit()&lt;/code&gt; function to include the form data:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;submit&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;SUBMIT&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;formData&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;formData&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;To ensure the &lt;code&gt;submitForm&lt;/code&gt; function receives the required form data, we update the &lt;code&gt;onsubmit&lt;/code&gt; state to include an &lt;code&gt;input&lt;/code&gt; field:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;signUpMachine&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;signUpMachineConfig&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createMachine&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="c1"&gt;//...&lt;/span&gt;
  &lt;span class="na"&gt;states&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;//...&lt;/span&gt;
    &lt;span class="na"&gt;onsubmit&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;invoke&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;src&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;submitForm&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;input&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;context&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="o"&gt;=&amp;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;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;formData&lt;/span&gt; &lt;span class="p"&gt;}),&lt;/span&gt;
      &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Xstate then injects then &lt;code&gt;input&lt;/code&gt; value into &lt;code&gt;submitForm&lt;/code&gt; function, as follows:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;submitForm&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;fromPromise&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;input&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;email&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
  &lt;span class="c1"&gt;//actual logic&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We can also replace the local &lt;code&gt;formData&lt;/code&gt; state in the component with the machine's &lt;code&gt;context.formData&lt;/code&gt; instead. We do that next.&lt;/p&gt;

&lt;h2&gt;
  
  
  Mapping Component's Data to Context
&lt;/h2&gt;

&lt;p&gt;To bind form inputs to the machine's context, we can use &lt;code&gt;v-model&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;input&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"name"&lt;/span&gt; &lt;span class="na"&gt;placeholder=&lt;/span&gt;&lt;span class="s"&gt;"Name"&lt;/span&gt; &lt;span class="na"&gt;v-model=&lt;/span&gt;&lt;span class="s"&gt;"state.context.formData.name"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;input&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"email"&lt;/span&gt; &lt;span class="na"&gt;placeholder=&lt;/span&gt;&lt;span class="s"&gt;"Email"&lt;/span&gt; &lt;span class="na"&gt;v-model=&lt;/span&gt;&lt;span class="s"&gt;"state.context.formData.email"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Alternatively, we can use an &lt;code&gt;UPDATE&lt;/code&gt; event to modify context dynamically:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;signUpMachine&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;signupMachineConfigs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createMachine&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="c1"&gt;//...&lt;/span&gt;
  &lt;span class="na"&gt;states&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;on&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;NEXT&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;email&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;UPDATE&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="na"&gt;actions&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;assign&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
            &lt;span class="na"&gt;formData&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt;
              &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;formData&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
              &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="p"&gt;}),&lt;/span&gt;
          &lt;span class="p"&gt;}),&lt;/span&gt;
        &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="na"&gt;email&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;on&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;PREV&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;name&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;UPDATE&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="na"&gt;actions&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;assign&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
            &lt;span class="na"&gt;formData&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt;
              &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;formData&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
              &lt;span class="na"&gt;email&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="p"&gt;}),&lt;/span&gt;
          &lt;span class="p"&gt;}),&lt;/span&gt;
        &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="c1"&gt;//...&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And we bind inputs to this event:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;update&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;$event&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;UPDATE&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;$event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;target&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And update the template:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;input&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"name"&lt;/span&gt; &lt;span class="na"&gt;placeholder=&lt;/span&gt;&lt;span class="s"&gt;"Name"&lt;/span&gt; &lt;span class="err"&gt;@&lt;/span&gt;&lt;span class="na"&gt;input=&lt;/span&gt;&lt;span class="s"&gt;"update"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;input&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"email"&lt;/span&gt; &lt;span class="na"&gt;placeholder=&lt;/span&gt;&lt;span class="s"&gt;"Email"&lt;/span&gt; &lt;span class="err"&gt;@&lt;/span&gt;&lt;span class="na"&gt;input=&lt;/span&gt;&lt;span class="s"&gt;"update"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Since &lt;code&gt;@input&lt;/code&gt; triggers on every keystroke, consider wrapping it with a debounce function for better performance.&lt;/p&gt;

&lt;p&gt;With this setup, our machine’s context stays in sync with the UI, eliminating the need to pass &lt;code&gt;formData&lt;/code&gt; in the &lt;code&gt;SUBMIT&lt;/code&gt; event:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;submit&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;SUBMIT&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And removing formData from the machine definition:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;signUpMachine&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;signUpMachineConfig&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createMachine&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="c1"&gt;//...&lt;/span&gt;
  &lt;span class="na"&gt;states&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;//...&lt;/span&gt;
    &lt;span class="na"&gt;email&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; 
      &lt;span class="na"&gt;on&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; 
        &lt;span class="c1"&gt;//...&lt;/span&gt;
        &lt;span class="na"&gt;SUBMIT&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;target&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;onsubmit&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; 
      &lt;span class="p"&gt;}&lt;/span&gt; 
    &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's it! We've successfully refactored our multi-step form wizard to use XState for robust state management. Our state machine's flow now looks like this, with the initial state of &lt;code&gt;name&lt;/code&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%2Fres.cloudinary.com%2Fmayashavin%2Fimage%2Fupload%2Fq_auto%2Ff_auto%2Fv1727777568%2Farticles%2Fxstate%2Fstate_machine_multi_form" 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%2Fres.cloudinary.com%2Fmayashavin%2Fimage%2Fupload%2Fq_auto%2Ff_auto%2Fv1727777568%2Farticles%2Fxstate%2Fstate_machine_multi_form" alt="State machine diagram for multi-step form" width="800" height="433"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Additionally, we can use XState’s &lt;a href="https://stately.ai/editor" rel="noopener noreferrer"&gt;visualizer tool&lt;/a&gt; to visualize our machine logic, by importing our code.&lt;/p&gt;

&lt;h2&gt;
  
  
  Resources
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://stately.ai/docs/quick-start" rel="noopener noreferrer"&gt;XState&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://stately.ai/docs/xstate-vue" rel="noopener noreferrer"&gt;XState Vue&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://stately.ai/editor" rel="noopener noreferrer"&gt;XState Visualizer&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/mayashavin/multi-step-form-xstate-2025" rel="noopener noreferrer"&gt;Multi-step form wizard with Vue and XState&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;p&gt;In this article, we explored how to manage a multi-step form in Vue.js using XState. We refactored our form to leverage state machines for predictable state management and introduced features like asynchronous submission and context-based data handling. This approach enhances maintainability and can be applied across different front-end frameworks.&lt;/p&gt;

&lt;h3&gt;
  
  
  What's next?
&lt;/h3&gt;

&lt;p&gt;We can further improve our form by adding &lt;strong&gt;guards&lt;/strong&gt; to prevent invalid transitions and dynamically updating the UI based on conditions. Try experimenting with XState and see how it simplifies state management in your projects!&lt;/p&gt;

&lt;p&gt;👉 &lt;em&gt;Learn about Vue 3 and TypeScript with my new book &lt;a href="https://www.oreilly.com/library/view/learning-vue/9781492098812/" rel="noopener noreferrer"&gt;Learning Vue&lt;/a&gt;!&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;👉 &lt;em&gt;If you'd like to catch up with me sometimes, follow me on &lt;a href="https://x.com/MayaShavin" rel="noopener noreferrer"&gt;X&lt;/a&gt; | &lt;a href="https://www.linkedin.com/in/mayashavin" rel="noopener noreferrer"&gt;LinkedIn&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Like this post or find it helpful? Share it 👇🏼 😉&lt;/p&gt;

</description>
      <category>xstate</category>
      <category>vue</category>
      <category>frontend</category>
      <category>javascript</category>
    </item>
    <item>
      <title>My Top 5 VSCode Extensions to Supercharge Your Markdown Writing</title>
      <dc:creator>Maya Shavin 🌷☕️🏡</dc:creator>
      <pubDate>Wed, 29 Jan 2025 07:08:40 +0000</pubDate>
      <link>https://dev.to/mayashavin/my-top-5-vscode-extensions-to-supercharge-your-markdown-writing-29c</link>
      <guid>https://dev.to/mayashavin/my-top-5-vscode-extensions-to-supercharge-your-markdown-writing-29c</guid>
      <description>&lt;p&gt;&lt;em&gt;As a developer and tech blogger, I frequently write technical blog posts and documentation in Markdown to share knowledge with the community. VSCode has been my go-to editor for managing and crafting Markdown content efficiently, besides coding. Over time, I've discovered some VSCode extensions that have transformed my writing experience and made it faster, and more productive. In this article, I'll share my top 5 VSCode extensions for Markdown writing and explain how they can supercharge your workflow.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Table of Contents
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Table of Contents&lt;/li&gt;
&lt;li&gt;Writing Markdown in VSCode&lt;/li&gt;
&lt;li&gt;Markdownlint by David Anson&lt;/li&gt;
&lt;li&gt;Markdown All in One by Yu Zhang&lt;/li&gt;
&lt;li&gt;Word Count by Microsoft&lt;/li&gt;
&lt;li&gt;:emojisense: by Matt Bierner&lt;/li&gt;
&lt;li&gt;CoCover - The AI Assistant for Generating Cover Images&lt;/li&gt;
&lt;li&gt;Summary&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Writing Markdown in VSCode
&lt;/h2&gt;

&lt;p&gt;Introduced in 2004, &lt;a href="https://www.markdownguide.org/getting-started/" rel="noopener noreferrer"&gt;Markdown is a lightweight markup language with plain text formatting syntax&lt;/a&gt;. It's widely used for drafting and writing documentation, such as README files. Personally, I find writing and storing my articles as Markdown files for my website a much more convenient approach, &lt;em&gt;especially in VSCode&lt;/em&gt; , compared to using standard WYSIWYG editors. It helps me stay focused and manage my content more efficiently.&lt;/p&gt;

&lt;p&gt;VSCode, a free and popular IDE (or code editor), provides excellent built-in support for Markdown editing, including syntax highlighting and preview features, such as a full preview with &lt;code&gt;Shift + Ctrl/Cmd + V&lt;/code&gt; or a side-by-side preview with &lt;code&gt;Ctrl/Cmd + K + V&lt;/code&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%2Fres.cloudinary.com%2Fmayashavin%2Fimage%2Fupload%2Fq_auto%2Ff_auto%2Fv1727777568%2Farticles%2Ftools%2Fside_by_side_preview" 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%2Fres.cloudinary.com%2Fmayashavin%2Fimage%2Fupload%2Fq_auto%2Ff_auto%2Fv1727777568%2Farticles%2Ftools%2Fside_by_side_preview" alt="Markdown editor and preview side-by-side in VSCode" width="800" height="225"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Beyond these basic features, you can enhance your Markdown editing experience with extensions. Here are five VSCode extensions that have significantly improved my Markdown writing workflow:&lt;/p&gt;

&lt;h2&gt;
  
  
  Markdownlint by David Anson
&lt;/h2&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%2Fres.cloudinary.com%2Fmayashavin%2Fimage%2Fupload%2Fq_auto%2Ff_auto%2Fv1727777568%2Farticles%2Ftools%2Fmarkdownlint" 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%2Fres.cloudinary.com%2Fmayashavin%2Fimage%2Fupload%2Fq_auto%2Ff_auto%2Fv1727777568%2Farticles%2Ftools%2Fmarkdownlint" alt="Markdownlint Extension in Marketplace" width="800" height="214"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://marketplace.visualstudio.com/items?itemName=DavidAnson.vscode-markdownlint" rel="noopener noreferrer"&gt;Markdownlint&lt;/a&gt; is the first extension I installed when working with Markdown files. It keeps my Markdown clean and consistent by flagging common syntax issues such as missing spaces, trailing spaces, and inconsistent indentation.&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%2Fres.cloudinary.com%2Fmayashavin%2Fimage%2Fupload%2Fq_auto%2Ff_auto%2Fv1727777568%2Farticles%2Ftools%2Flinting_preview" 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%2Fres.cloudinary.com%2Fmayashavin%2Fimage%2Fupload%2Fq_auto%2Ff_auto%2Fv1727777568%2Farticles%2Ftools%2Flinting_preview" alt="Inline error displayed when there is markdown violation" width="800" height="98"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Markdown All in One by Yu Zhang
&lt;/h2&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%2Fres.cloudinary.com%2Fmayashavin%2Fimage%2Fupload%2Fq_auto%2Ff_auto%2Fv1727777568%2Farticles%2Ftools%2Fmarkdownallinone" 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%2Fres.cloudinary.com%2Fmayashavin%2Fimage%2Fupload%2Fq_auto%2Ff_auto%2Fv1727777568%2Farticles%2Ftools%2Fmarkdownallinone" alt="Markdownlint All in One in Marketplace" width="800" height="153"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://marketplace.visualstudio.com/items?itemName=yzhang.markdown-all-in-one" rel="noopener noreferrer"&gt;Markdown All in One&lt;/a&gt; is a powerful extension that offers a collection of features to streamline Markdown editing. It includes shortcuts for common syntax elements like headers, lists, and tables, along with keyboard shortcuts for toggling bold, italics, and code formatting.&lt;/p&gt;

&lt;p&gt;One of my favorite features is its Table of Contents (TOC) generator, which automatically updates the TOC based on the headings in your file. This is particularly useful in helping readers navigate lengthy documents.&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%2Fres.cloudinary.com%2Fmayashavin%2Fimage%2Fupload%2Fq_auto%2Ff_auto%2Fv1727777568%2Farticles%2Ftools%2Ftoc_generate" 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%2Fres.cloudinary.com%2Fmayashavin%2Fimage%2Fupload%2Fq_auto%2Ff_auto%2Fv1727777568%2Farticles%2Ftools%2Ftoc_generate" alt="Table of Contents generated by Markdown All in One" width="800" height="263"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;It also offers list-editing features for reorganizing content quickly, as well as handy tools like toggling code spans and automatic table formatting. These features make long-form document editing much more efficient and enjoyable.&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%2Fres.cloudinary.com%2Fmayashavin%2Fvideo%2Fupload%2Fv1737673973%2Farticles%2Ftools%2Flist_editing.gif" 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%2Fres.cloudinary.com%2Fmayashavin%2Fvideo%2Fupload%2Fv1737673973%2Farticles%2Ftools%2Flist_editing.gif" alt="List editing in Markdown All in One" width="1030" height="378"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Word Count by Microsoft
&lt;/h2&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%2Fres.cloudinary.com%2Fmayashavin%2Fimage%2Fupload%2Fq_auto%2Ff_auto%2Fv1727777568%2Farticles%2Ftools%2Fwordcount" 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%2Fres.cloudinary.com%2Fmayashavin%2Fimage%2Fupload%2Fq_auto%2Ff_auto%2Fv1727777568%2Farticles%2Ftools%2Fwordcount" alt="Word Count in Marketplace" width="800" height="156"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://marketplace.visualstudio.com/items?itemName=ms-vscode.wordcount" rel="noopener noreferrer"&gt;Word Count&lt;/a&gt; is a simple yet essential extension that shows the total word count of your current document in the status bar. It's perfect for tracking article length and ensuring your posts meet the word count requirements.&lt;/p&gt;

&lt;p&gt;Once installed, you'll find the word count displayed in the bottom status bar, updating in real-time as you type.&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%2Fres.cloudinary.com%2Fmayashavin%2Fimage%2Fupload%2Fq_auto%2Ff_auto%2Fv1727777568%2Farticles%2Ftools%2Fwordcount_statusbar" 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%2Fres.cloudinary.com%2Fmayashavin%2Fimage%2Fupload%2Fq_auto%2Ff_auto%2Fv1727777568%2Farticles%2Ftools%2Fwordcount_statusbar" alt="Word Count displayed in the status bar" width="800" height="90"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  :emojisense: by Matt Bierner
&lt;/h2&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%2Fres.cloudinary.com%2Fmayashavin%2Fimage%2Fupload%2Fq_auto%2Ff_auto%2Fv1727777568%2Farticles%2Ftools%2Femojisense" 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%2Fres.cloudinary.com%2Fmayashavin%2Fimage%2Fupload%2Fq_auto%2Ff_auto%2Fv1727777568%2Farticles%2Ftools%2Femojisense" alt="Emojisense in Marketplace" width="800" height="229"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://marketplace.visualstudio.com/items?itemName=bierner.emojisense" rel="noopener noreferrer"&gt;:emojisense:&lt;/a&gt; is a fun and practical extension for adding emojis to your Markdown files. It provides an inline, searchable emoji picker that makes finding and inserting emojis super easy.&lt;/p&gt;

&lt;p&gt;To use the extension, we type &lt;code&gt;:&lt;/code&gt; followed by the emoji name or keyword. The extension will display matching emojis for quick insertion. 💡&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%2Fres.cloudinary.com%2Fmayashavin%2Fvideo%2Fupload%2Fv1737673973%2Farticles%2Ftools%2Femoji_editing.gif" 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%2Fres.cloudinary.com%2Fmayashavin%2Fvideo%2Fupload%2Fv1737673973%2Farticles%2Ftools%2Femoji_editing.gif" alt="Emojisense in action" width="800" height="335"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  CoCover - The AI Assistant for Generating Cover Images
&lt;/h2&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%2Fres.cloudinary.com%2Fmayashavin%2Fimage%2Fupload%2Fq_auto%2Ff_auto%2Fv1727777568%2Farticles%2Ftools%2Fcocover" 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%2Fres.cloudinary.com%2Fmayashavin%2Fimage%2Fupload%2Fq_auto%2Ff_auto%2Fv1727777568%2Farticles%2Ftools%2Fcocover" alt="CoCover in Marketplace" width="800" height="154"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Last but not least, &lt;a href="https://marketplace.visualstudio.com/items?itemName=MayaShavinStudio.cocover" rel="noopener noreferrer"&gt;CoCover&lt;/a&gt; is an AI-powered extension I developed for generating beautiful cover images directly in VSCode. It uses GitHub Copilot and OpenAI's DALL-E to create covers based on your article's title and content.&lt;/p&gt;


  


&lt;p&gt;With CoCover, you can generate blog covers, upload them to Cloudinary, or save them locally—all without leaving VSCode. It's a fantastic tool for streamlining your content creation workflow while making your articles visually appealing.&lt;/p&gt;




&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;p&gt;These are my top five VSCode extensions for Markdown writing. They've helped me streamline my workflow, minimize context switching, and create better Markdown-based content.&lt;/p&gt;

&lt;p&gt;What about you? What are your favorite VSCode extensions for Markdown writing? I'd love to hear your recommendations and suggestions! 😄&lt;/p&gt;

&lt;p&gt;👉 &lt;em&gt;Learn about Vue 3 and TypeScript with my new book &lt;a href="https://www.oreilly.com/library/view/learning-vue/9781492098812/" rel="noopener noreferrer"&gt;Learning Vue&lt;/a&gt;!&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;👉 &lt;em&gt;If you'd like to catch up with me sometimes, follow me on &lt;a href="https://x.com/MayaShavin" rel="noopener noreferrer"&gt;X&lt;/a&gt; | &lt;a href="https://www.linkedin.com/in/mayashavin" rel="noopener noreferrer"&gt;LinkedIn&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Like this post or find it helpful? Share it 👇🏼 😉&lt;/p&gt;

</description>
      <category>markdown</category>
      <category>extensions</category>
      <category>vscode</category>
      <category>writing</category>
    </item>
    <item>
      <title>Mastering Flexible Layouts: CSS Flexbox VS Grid for Responsive Design</title>
      <dc:creator>Maya Shavin 🌷☕️🏡</dc:creator>
      <pubDate>Wed, 22 Jan 2025 08:21:13 +0000</pubDate>
      <link>https://dev.to/mayashavin/mastering-flexible-layouts-css-flexbox-vs-grid-for-responsive-design-46m3</link>
      <guid>https://dev.to/mayashavin/mastering-flexible-layouts-css-flexbox-vs-grid-for-responsive-design-46m3</guid>
      <description>&lt;p&gt;&lt;em&gt;In this post, we will discover different approaches to distribute a list of cards evenly, horizontally and responsively in different screen sizes using CSS Flex and Grid.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Table of Contents
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Table of Contents&lt;/li&gt;
&lt;li&gt;The Challenge&lt;/li&gt;
&lt;li&gt;Using CSS Flexbox to Create a Flexible List of Cards&lt;/li&gt;
&lt;li&gt;Displaying Cards Evenly with flex-grow and flex-basis&lt;/li&gt;
&lt;li&gt;Using CSS Grid for a Flexible List of Cards&lt;/li&gt;
&lt;li&gt;Summary&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The Challenge
&lt;/h2&gt;

&lt;p&gt;In a gallery (or list) component, we often want to display items as cards, such as articles, products, or images. The number of cards per row can be flexible, depending on the container's width. Each card should expand to fill the available space and shrink to its minimum width as needed.&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%2Fres.cloudinary.com%2Fmayashavin%2Fimage%2Fupload%2Fq_auto%2Ff_auto%2Fv1727777568%2Farticles%2Fcss%2Fdesign_sketch" 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%2Fres.cloudinary.com%2Fmayashavin%2Fimage%2Fupload%2Fq_auto%2Ff_auto%2Fv1727777568%2Farticles%2Fcss%2Fdesign_sketch" alt="Design of list card in two different width sizes" width="800" height="316"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The layout should remain consistent across screen sizes, with the cards distributed from left to right, wrapping to the next row as necessary. The cards should maintain equal height, width, and spacing between them.&lt;/p&gt;

&lt;p&gt;Here’s a simple starting point for the HTML structure and CSS:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;ul&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"list-items"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;li&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"item"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt; &amp;gt;
    &lt;span class="c"&gt;&amp;lt;!--item's content--&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/li&amp;gt;&lt;/span&gt;
  &lt;span class="c"&gt;&amp;lt;!--more items--&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/ul&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;style&amp;gt;&lt;/span&gt;
  &lt;span class="nc"&gt;.list-items&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nl"&gt;list-style&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;none&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nl"&gt;max-width&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;500px&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="nc"&gt;.item&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nl"&gt;border&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1px&lt;/span&gt; &lt;span class="nb"&gt;solid&lt;/span&gt; &lt;span class="no"&gt;gray&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nl"&gt;padding&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;10px&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/style&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With the above code, the browser displays the container according to the screen’s width but does not exceed 500px:&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%2Fres.cloudinary.com%2Fmayashavin%2Fimage%2Fupload%2Fq_auto%2Ff_auto%2Fv1727777568%2Farticles%2Fcss%2Fcss_grid_initial_list" 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%2Fres.cloudinary.com%2Fmayashavin%2Fimage%2Fupload%2Fq_auto%2Ff_auto%2Fv1727777568%2Farticles%2Fcss%2Fcss_grid_initial_list" alt="List of cards displayed in browser with minimum CSS" width="545" height="420"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Currently, the items are not distributed horizontally. We’ll fix this next using CSS Flexbox.&lt;/p&gt;

&lt;h2&gt;
  
  
  Using CSS Flexbox to Create a Flexible List of Cards
&lt;/h2&gt;

&lt;p&gt;CSS Flexbox provides a flexible and responsive layout structure using the &lt;code&gt;display: flex&lt;/code&gt; property. To achieve horizontal flow, we use the &lt;code&gt;flex-wrap&lt;/code&gt; property to wrap cards to the next row when necessary, and &lt;code&gt;gap&lt;/code&gt; to define spacing between cards:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="nc"&gt;.list-items&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;display&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;flex&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;flex-wrap&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;wrap&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="py"&gt;gap&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;10px&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;Now, the cards flow horizontally:&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%2Fres.cloudinary.com%2Fmayashavin%2Fimage%2Fupload%2Fq_auto%2Ff_auto%2Fv1727777568%2Farticles%2Fcss%2Fcss_flex_list" 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%2Fres.cloudinary.com%2Fmayashavin%2Fimage%2Fupload%2Fq_auto%2Ff_auto%2Fv1727777568%2Farticles%2Fcss%2Fcss_flex_list" alt="A list of horizontal distributed cards using CSS Flex" width="800" height="221"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;However, not all cards have the same width (e.g., Card 9 and Card 10). To fix this, we can set a fixed &lt;code&gt;width&lt;/code&gt; for &lt;code&gt;.item&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="nc"&gt;.item&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c"&gt;/* other styles */&lt;/span&gt;
  &lt;span class="nl"&gt;width&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;100px&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This ensures all cards have the same width. But when the container's width changes, the cards cannot expand to fill the space. This leaves gaps at the end of each row, as shown below:&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%2Fres.cloudinary.com%2Fmayashavin%2Fimage%2Fupload%2Fq_auto%2Ff_auto%2Fv1727777568%2Farticles%2Fcss%2Fcss_flex_fixed_width_border" 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%2Fres.cloudinary.com%2Fmayashavin%2Fimage%2Fupload%2Fq_auto%2Ff_auto%2Fv1727777568%2Farticles%2Fcss%2Fcss_flex_fixed_width_border" alt="Cards distributed to the left and left some space each row on the right" width="800" height="358"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;We can try to address this using &lt;code&gt;justify-content&lt;/code&gt; properties like &lt;code&gt;space-between&lt;/code&gt;, &lt;code&gt;space-around&lt;/code&gt;, &lt;code&gt;space-evenly&lt;/code&gt;, or &lt;code&gt;center&lt;/code&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%2Fres.cloudinary.com%2Fmayashavin%2Fimage%2Fupload%2Fq_auto%2Ff_auto%2Fv1727777568%2Farticles%2Fcss%2Fjustify_content_4" 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%2Fres.cloudinary.com%2Fmayashavin%2Fimage%2Fupload%2Fq_auto%2Ff_auto%2Fv1727777568%2Farticles%2Fcss%2Fjustify_content_4" alt="Different results in different value of justify content" width="800" height="405"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;While these approaches work differently, none fully meet our goal of evenly distributing cards in size and spacing.&lt;/p&gt;

&lt;p&gt;Instead, we can use &lt;code&gt;flex-grow&lt;/code&gt; and &lt;code&gt;flex-basis&lt;/code&gt;, which we’ll explore next.&lt;/p&gt;

&lt;h2&gt;
  
  
  Displaying Cards Evenly with flex-grow and flex-basis
&lt;/h2&gt;

&lt;p&gt;The &lt;code&gt;flex-basis&lt;/code&gt; property defines the &lt;strong&gt;initial&lt;/strong&gt; size of a card before any extra space is distributed, while &lt;code&gt;flex-grow: 1&lt;/code&gt; (or simply &lt;code&gt;flex: 1&lt;/code&gt;) allows the card to grow relative to its siblings. Combining these properties ensures that cards distribute evenly in size and spacing:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="nc"&gt;.item&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c"&gt;/* other styles */&lt;/span&gt;
  &lt;span class="nl"&gt;flex&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;flex-basis&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;100px&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;Here’s the result:&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%2Fres.cloudinary.com%2Fmayashavin%2Fimage%2Fupload%2Fq_auto%2Ff_auto%2Fv1727777568%2Farticles%2Fcss%2Fflex_basic" 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%2Fres.cloudinary.com%2Fmayashavin%2Fimage%2Fupload%2Fq_auto%2Ff_auto%2Fv1727777568%2Farticles%2Fcss%2Fflex_basic" alt="Create App button in LinkedIn Developer dashboard" width="800" height="349"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;However, the last card still expands to fill the remaining space because it has no sibling to share it with. This breaks our consistency goal. To address this issue, let's turn to CSS Grid.&lt;/p&gt;

&lt;h2&gt;
  
  
  Using CSS Grid for a Flexible List of Cards
&lt;/h2&gt;

&lt;p&gt;CSS Grid is a 2-dimensional layout system that organizes content into rows and columns. To create a flexible card layout, we can use the &lt;code&gt;display: grid&lt;/code&gt; property:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="nc"&gt;.list-items&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;display&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;grid&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="py"&gt;gap&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;10px&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;While CSS Grid supports many properties similar to Flexbox, such as &lt;code&gt;gap&lt;/code&gt; and &lt;code&gt;alignment&lt;/code&gt; options, it requires us to define the layout structure in advance. For a flexible layout, we can use &lt;code&gt;grid-template-columns&lt;/code&gt; with &lt;code&gt;auto-fit&lt;/code&gt;, &lt;code&gt;minmax()&lt;/code&gt;, and &lt;code&gt;repeat()&lt;/code&gt; functions:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="nc"&gt;.list-items&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c"&gt;/* other styles */&lt;/span&gt;
  &lt;span class="py"&gt;grid-template-columns&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;repeat&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;auto-fit&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;minmax&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;100px&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="n"&gt;fr&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;Here’s what the above code does:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;repeat(&amp;lt;number&amp;gt;, &amp;lt;size&amp;gt;)&lt;/code&gt;: Repeats columns of a specified size. Using &lt;code&gt;auto-fit&lt;/code&gt; automatically adjusts the number of columns to fit the available space.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;minmax(&amp;lt;min&amp;gt;, &amp;lt;max&amp;gt;)&lt;/code&gt;: Defines the size range for each column, setting a &lt;strong&gt;minimum&lt;/strong&gt; size of &lt;code&gt;100px&lt;/code&gt; and a &lt;strong&gt;maximum&lt;/strong&gt; size of &lt;code&gt;1fr&lt;/code&gt; (1 fraction of the available space).&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Combining these with &lt;code&gt;grid-template-columns&lt;/code&gt; ensures the layout adjusts responsively.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;And that's it! The result is a perfectly responsive layout:&lt;/p&gt;


  


&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;p&gt;CSS Flexbox and CSS Grid are powerful tools for creating flexible, responsive designs. While Flexbox excels at managing one-dimensional layouts, CSS Grid offers unparalleled flexibility for two-dimensional layouts. Understanding the strengths of each approach ensures you can choose the right tool for your design goals. I hope this article provides clarity and inspires you to create your next layout. If you have questions or feedback, feel free to reach out. &lt;/p&gt;

&lt;p&gt;Happy coding! 🚀&lt;/p&gt;

&lt;p&gt;👉 &lt;em&gt;Learn about Vue 3 and TypeScript with my new book &lt;a href="https://www.oreilly.com/library/view/learning-vue/9781492098812/" rel="noopener noreferrer"&gt;Learning Vue&lt;/a&gt;!&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;👉 &lt;em&gt;If you'd like to catch up with me sometimes, follow me on &lt;a href="https://x.com/MayaShavin" rel="noopener noreferrer"&gt;X&lt;/a&gt; | &lt;a href="https://www.linkedin.com/in/mayashavin" rel="noopener noreferrer"&gt;LinkedIn&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Like this post or find it helpful? Share it 👇🏼 😉&lt;/p&gt;

</description>
      <category>cssgrid</category>
      <category>css</category>
      <category>tutorial</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Resolving Auto-Scroll issues for overflow container in a Nuxt app</title>
      <dc:creator>Maya Shavin 🌷☕️🏡</dc:creator>
      <pubDate>Wed, 15 Jan 2025 08:47:00 +0000</pubDate>
      <link>https://dev.to/mayashavin/resolving-auto-scroll-issues-for-overflow-container-in-a-nuxt-app-ikn</link>
      <guid>https://dev.to/mayashavin/resolving-auto-scroll-issues-for-overflow-container-in-a-nuxt-app-ikn</guid>
      <description>&lt;p&gt;&lt;em&gt;In this article, I share how I resolved the auto-scroll issue caused by an overflow container within a non-scrollable &lt;code&gt;body&lt;/code&gt; in a Nuxt app, and improved the user experience when scrolling my website&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Table of Contents
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Table of Contents&lt;/li&gt;
&lt;li&gt;The Initial Design&lt;/li&gt;
&lt;li&gt;The Issue&lt;/li&gt;
&lt;li&gt;The Solution&lt;/li&gt;
&lt;li&gt;Summary&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The Initial Design
&lt;/h2&gt;

&lt;p&gt;When I built my website using Nuxt.js, my initial design was to have only the &lt;code&gt;main&lt;/code&gt; content container scrollable, while the &lt;code&gt;header&lt;/code&gt; and &lt;code&gt;footer&lt;/code&gt; remained fixed—without using CSS &lt;code&gt;fixed&lt;/code&gt; or &lt;code&gt;absolute&lt;/code&gt; positioning.&lt;/p&gt;





&lt;p&gt;To achieve this, I used a combination of CSS &lt;code&gt;flex&lt;/code&gt; and &lt;code&gt;overflow&lt;/code&gt; properties, starting with the &lt;code&gt;body&lt;/code&gt; tag:&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;body&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;overflow&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;hidden&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;height&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;100%&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;In the &lt;code&gt;default.vue&lt;/code&gt; layout:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"h-full flex flex-col bg-mayash-main-light dark-mode"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;nav-header&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;main&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"flex-1 overflow-y-auto overflow-x-hidden"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;slot&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/main&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;nav-footer&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In the above code, I ensured the body was not scrollable using &lt;code&gt;overflow-hidden&lt;/code&gt; while keeping the height statically set to &lt;code&gt;100%&lt;/code&gt;. The &lt;code&gt;main&lt;/code&gt; container was set to &lt;code&gt;flex-1&lt;/code&gt; to expand and fill the remaining space after the header and footer. I used &lt;code&gt;overflow-y-auto&lt;/code&gt; to make the container scrollable vertically only. This setup allowed the page to work as intended based on the initial design.&lt;/p&gt;

&lt;p&gt;However, problems arose when I added the table of contents to the article page. The table of contents is a list of links to section headings within an article, and clicking on a link should scroll to the corresponding section. Unfortunately, it didn’t.&lt;/p&gt;





&lt;p&gt;Additionally, when refreshing the article page with an HTML anchor (&lt;code&gt;#&lt;/code&gt;) in the URL, the browser didn’t scroll to the desired section. Navigating back to the article page from another page also failed to auto-scroll to the top of the page. For example, navigating to the &lt;strong&gt;Speaking&lt;/strong&gt; page from the &lt;strong&gt;Article&lt;/strong&gt; page left the user at the bottom instead of at the top.&lt;/p&gt;





&lt;p&gt;Furthermore, I received complaints about the footer being &lt;em&gt;fixed&lt;/em&gt; at the bottom of the page, which some users found distracting when reading the article.&lt;/p&gt;

&lt;p&gt;Clearly, I needed to change my design and fix these issues. But is it that simple? Let's find out.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Issue
&lt;/h2&gt;

&lt;p&gt;Nuxt.js 3 uses Vue Router to handle application routing, which includes auto-scrolling when navigating between pages, with a smooth transition effect. So, it should work, right?&lt;/p&gt;

&lt;p&gt;Unfortunately, it didn’t.&lt;/p&gt;

&lt;p&gt;I tried different workarounds, including &lt;a href="https://router.vuejs.org/guide/advanced/scroll-behavior" rel="noopener noreferrer"&gt;using Vue Router custom scroll behavior&lt;/a&gt;, &lt;a href="https://nuxt.com/docs/guide/recipes/custom-routing#using-approuteroptions" rel="noopener noreferrer"&gt;using &lt;code&gt;scrollBehaviorType&lt;/code&gt;&lt;/a&gt;, and even &lt;code&gt;window.scrollTo&lt;/code&gt;. None of these approaches worked.&lt;/p&gt;

&lt;p&gt;So, is there a way to fix this issue?&lt;/p&gt;

&lt;h2&gt;
  
  
  The Solution
&lt;/h2&gt;

&lt;p&gt;After some research, I discovered that the issue was caused by the combination of the &lt;code&gt;overflow&lt;/code&gt; and &lt;code&gt;height&lt;/code&gt; CSS properties. When &lt;code&gt;height&lt;/code&gt; is set to a fixed value (like &lt;code&gt;100%&lt;/code&gt;) and &lt;code&gt;overflow&lt;/code&gt; on the &lt;code&gt;body&lt;/code&gt; tag is set to &lt;code&gt;hidden&lt;/code&gt;, the scrollbar displayed on the right side of the page isn’t for the &lt;code&gt;body&lt;/code&gt; tag but for the &lt;code&gt;main&lt;/code&gt; container instead.&lt;/p&gt;

&lt;p&gt;When navigating between routes or sections, the browser, by default, tries to scroll the &lt;code&gt;body&lt;/code&gt; tag. Since the &lt;code&gt;body&lt;/code&gt; is not scrollable, the browser doesn’t know which container to scroll, leading to the issue.&lt;/p&gt;

&lt;p&gt;There are several ways to work around this problem, such as querying the &lt;code&gt;main&lt;/code&gt; container's DOM reference and &lt;a href="https://developer.mozilla.org/en-US/docs/Web/API/Element/scrollTo" rel="noopener noreferrer"&gt;using the &lt;code&gt;scrollTo&lt;/code&gt; method&lt;/a&gt; to manually scroll the container to the desired section whenever the route changes. However, this approach is not ideal — it’s complex to implement and not a good practice.&lt;/p&gt;

&lt;p&gt;A more straightforward solution is to remove the &lt;code&gt;height: 100%&lt;/code&gt; and &lt;code&gt;overflow: hidden&lt;/code&gt; properties from the &lt;code&gt;body&lt;/code&gt; tag and use &lt;code&gt;position: sticky&lt;/code&gt; for the &lt;code&gt;header&lt;/code&gt; to keep it fixed at the top instead:&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;body&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c"&gt;/* overflow: hidden; */&lt;/span&gt;
  &lt;span class="c"&gt;/* height: 100%; */&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nt"&gt;header&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;position&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;sticky&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;top&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;z-index&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;100&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We also need to set &lt;code&gt;top: 0&lt;/code&gt; and &lt;code&gt;z-index&lt;/code&gt; to ensure the header remains at the top of the page and appears above other elements.&lt;/p&gt;

&lt;p&gt;And that’s it! The page now has the header fixed at the top, and the content automatically scrolls based on route changes or HTML anchor links, utilizing the browser’s default smooth transition effect.&lt;/p&gt;








&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;p&gt;In this article, I shared how I resolved the auto-scroll issue caused by an overflow container within a non-scrollable &lt;code&gt;body&lt;/code&gt; in a Nuxt app. The issue stemmed from the combination of &lt;code&gt;overflow&lt;/code&gt; and a fixed &lt;code&gt;height&lt;/code&gt; CSS property and can affect any web project, not just those using Nuxt or Vue Router. Depending on your goals, the solution may involve simply removing this CSS combination or implementing a more complex workaround, such as manually triggering scrollTo on the target scrollable container. So, the next time you encounter this issue, you’ll know how to fix it 😉!&lt;/p&gt;

&lt;p&gt;👉 &lt;em&gt;Learn about Vue 3 and TypeScript with my new book &lt;a href="https://www.oreilly.com/library/view/learning-vue/9781492098812/" rel="noopener noreferrer"&gt;Learning Vue&lt;/a&gt;!&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;👉 &lt;em&gt;If you'd like to catch up with me sometimes, follow me on &lt;a href="https://x.com/MayaShavin" rel="noopener noreferrer"&gt;X&lt;/a&gt; | &lt;a href="https://www.linkedin.com/in/mayashavin" rel="noopener noreferrer"&gt;LinkedIn&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Like this post or find it helpful? Share it 👇🏼 😉&lt;/p&gt;

</description>
      <category>nuxt</category>
      <category>css</category>
      <category>vue</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Building Social Media Automation: LinkedIn Sharing with Serverless Function</title>
      <dc:creator>Maya Shavin 🌷☕️🏡</dc:creator>
      <pubDate>Wed, 08 Jan 2025 06:32:32 +0000</pubDate>
      <link>https://dev.to/mayashavin/building-social-media-automation-linkedin-sharing-with-serverless-function-47j5</link>
      <guid>https://dev.to/mayashavin/building-social-media-automation-linkedin-sharing-with-serverless-function-47j5</guid>
      <description>&lt;p&gt;&lt;em&gt;After publishing a new article or blog post, the need to promote it on social media arises. Manually sharing the post can be time-consuming and inefficient. In this article, we will explore how to build a serverless function to share an article URL on LinkedIn using its JavaScript API client and Netlify serverless functions. This is part of building an automated workflow for social media promotion.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Table of Contents
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Table of Contents&lt;/li&gt;
&lt;li&gt;Prerequisites&lt;/li&gt;
&lt;li&gt;
Getting started

&lt;ul&gt;
&lt;li&gt;Setting up the permissions&lt;/li&gt;
&lt;li&gt;Configuring OAuth 2.0 settings&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;

Sharing a post with URL using LinkedIn API Client

&lt;ul&gt;
&lt;li&gt;Getting the user's unique id&lt;/li&gt;
&lt;li&gt;Sharing a post URL&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;Exposing as a Netlify serverless function&lt;/li&gt;

&lt;li&gt;Testing the Functionality&lt;/li&gt;

&lt;li&gt;Deploying with Netlify&lt;/li&gt;

&lt;li&gt;Summary&lt;/li&gt;

&lt;/ul&gt;

&lt;h2&gt;
  
  
  Prerequisites
&lt;/h2&gt;

&lt;p&gt;To follow along with this tutorial, you will need the following:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A LinkedIn account&lt;/li&gt;
&lt;li&gt;Node.js and &lt;a href="https://docs.netlify.com/cli/get-started/" rel="noopener noreferrer"&gt;Netlify CLI&lt;/a&gt; installed.&lt;/li&gt;
&lt;li&gt;A Netlify account and a site created for deploying the serverless function.&lt;/li&gt;
&lt;li&gt;Basic knowledge of JavaScript and TypeScript.&lt;/li&gt;
&lt;/ul&gt;

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

&lt;p&gt;To start working with LinkedIn APIs, we need to perform the following steps:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Head to &lt;a href="https://www.linkedin.com/developers/" rel="noopener noreferrer"&gt;LinkedIn Developer Console&lt;/a&gt; with your LinkedIn account.&lt;/li&gt;
&lt;li&gt;Create a new app by clicking on the &lt;code&gt;Create App&lt;/code&gt; button. 
&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fres.cloudinary.com%2Fmayashavin%2Fimage%2Fupload%2Fq_auto%2Ff_auto%2Fv1727777568%2Farticles%2Flinkedin%2Fcreate_new_app" alt="Create App button in LinkedIn Developer dashboard" width="800" height="86"&gt;
&lt;/li&gt;
&lt;li&gt;Fill in the details such as App Name (&lt;em&gt;Social Media Tester&lt;/em&gt;, for example) and App logo image.&lt;/li&gt;
&lt;li&gt;You will need to a LinkedIn Company page to associate with the app you are creating (any page you have admin access to be able to verify the connection afterwards).&lt;/li&gt;
&lt;/ol&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%2Fres.cloudinary.com%2Fmayashavin%2Fimage%2Fupload%2Fq_auto%2Ff_auto%2Fv1727777568%2Farticles%2Flinkedin%2Fcreate_app" 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%2Fres.cloudinary.com%2Fmayashavin%2Fimage%2Fupload%2Fq_auto%2Ff_auto%2Fv1727777568%2Farticles%2Flinkedin%2Fcreate_app" alt="The form of filling new app details" width="800" height="465"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Once done, the portal will redirect you to the app dashboard, where we can start configuring the permissions and API products we need for the app.&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%2Fres.cloudinary.com%2Fmayashavin%2Fimage%2Fupload%2Fq_auto%2Ff_auto%2Fv1727777568%2Farticles%2Flinkedin%2Fapp_created" 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%2Fres.cloudinary.com%2Fmayashavin%2Fimage%2Fupload%2Fq_auto%2Ff_auto%2Fv1727777568%2Farticles%2Flinkedin%2Fapp_created" alt="App dashboard after created" width="754" height="258"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Setting up the permissions
&lt;/h3&gt;

&lt;p&gt;In the app dashboard, click on &lt;strong&gt;Products&lt;/strong&gt; tabs and request access to the &lt;code&gt;Share on LinkedIn&lt;/code&gt; and &lt;code&gt;Sign In with LinkedIn using OpenID Connect&lt;/code&gt; products.&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%2Fres.cloudinary.com%2Fmayashavin%2Fimage%2Fupload%2Fq_auto%2Ff_auto%2Fv1727777568%2Farticles%2Flinkedin%2Flinkedin_products" 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%2Fres.cloudinary.com%2Fmayashavin%2Fimage%2Fupload%2Fq_auto%2Ff_auto%2Fv1727777568%2Farticles%2Flinkedin%2Flinkedin_products" alt="Request access to Share on LinkedIn and Sign In with LinkedIn using OpenID Connect" width="682" height="258"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Configuring OAuth 2.0 settings
&lt;/h3&gt;

&lt;p&gt;With these permissions granted, we can head to &lt;a href="https://www.linkedin.com/developers/tools/oauth/token-generator" rel="noopener noreferrer"&gt;the OAuth 2.0 token generator tool&lt;/a&gt; to generate an access token for the app. The token should include the following scopes: &lt;code&gt;w_member_social&lt;/code&gt; for posting on behalf of the user, and &lt;code&gt;profile&lt;/code&gt; and &lt;code&gt;openid&lt;/code&gt; for user authentication and profile information.&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%2Fres.cloudinary.com%2Fmayashavin%2Fimage%2Fupload%2Fq_auto%2Ff_auto%2Fv1727777568%2Farticles%2Flinkedin%2Ftoken_scope" 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%2Fres.cloudinary.com%2Fmayashavin%2Fimage%2Fupload%2Fq_auto%2Ff_auto%2Fv1727777568%2Farticles%2Flinkedin%2Ftoken_scope" alt="Select the scopes available for the token" width="569" height="399"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This access token is a 3-legged OAuth token, ensuring that the user has explicitly authorized the application to act on their behalf. After generating the token, we can use it to authenticate and securely make requests to the LinkedIn APIs. Additionally, we can review the permissions and scopes granted to the app in the &lt;strong&gt;Auth&lt;/strong&gt; tab of the app dashboard.&lt;/p&gt;

&lt;p&gt;Great! Now that we have the access token and the app set up, we can start building the automation to post on LinkedIn on behalf of the user (which, in this case, is us).&lt;/p&gt;

&lt;h2&gt;
  
  
  Sharing a post with URL using LinkedIn API Client
&lt;/h2&gt;

&lt;p&gt;To start sharing posts programmatically, we can use &lt;a href="https://github.com/linkedin-developers/linkedin-api-client?tab=readme-ov-file#linkedin-api-javascript-client" rel="noopener noreferrer"&gt;the official LinkedIn API JavaScript Client for Node.js&lt;/a&gt; by installing it as a project dependency:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install &lt;/span&gt;linkedin-api-client

&lt;span class="c"&gt;# or with yarn&lt;/span&gt;
yarn add linkedin-api-client
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This library provides a straightforward and lightweight way to interact with LinkedIn API endpoints, leveraging Axios and TypeScript under the hood.&lt;/p&gt;

&lt;p&gt;Next, let's create a new file, &lt;code&gt;linkedin.ts&lt;/code&gt;, to encapsulate the logic for sharing posts on LinkedIn. We start by initializing a client instance to interact with the API, as shown below:&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="c1"&gt;// linkedin.ts&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;RestliClient&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;linkedin-api-client&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;client&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;RestliClient&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Getting the user's unique id
&lt;/h3&gt;

&lt;p&gt;To post on behalf of a user, we first need to retrieve the user's unique ID (which is different from the user's LinkedIn handle). This can be done by using the &lt;code&gt;/userinfo&lt;/code&gt; endpoint with the access token generated earlier:&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="c1"&gt;// linkedin.ts&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;getUserId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;accessToken&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;userResponse&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;resourcePath&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/userinfo&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;accessToken&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;userResponse&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;sub&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 unique ID is located in the &lt;code&gt;sub&lt;/code&gt; field of the response's &lt;code&gt;data&lt;/code&gt;. This value is required for the next step: sharing a post on the user's behalf.&lt;/p&gt;

&lt;h3&gt;
  
  
  Sharing a post URL
&lt;/h3&gt;

&lt;p&gt;Within &lt;code&gt;linkedin.ts&lt;/code&gt;, we define a function to share a post's URL as follows:&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;type&lt;/span&gt; &lt;span class="nx"&gt;SharePostArgs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;sharePost&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;token&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;content&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;SharePostArgs&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;//logic&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;sharePost&lt;/code&gt; function takes the access token and the content to share, which includes the URL and the text to accompany the post. We will then &lt;code&gt;create&lt;/code&gt; a new post &lt;code&gt;entity&lt;/code&gt; on the User Generated Contents resource using &lt;code&gt;/ugcPosts&lt;/code&gt; endpoint, as shown below:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;sharePost&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;token&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;content&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;SharePostArgs&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;resourcePath&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/ugcPosts&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;accessToken&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;token&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;entity&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="c1"&gt;//entity payload&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;entity&lt;/code&gt; payload is configured to include the user’s unique ID, retrieved earlier, as the &lt;code&gt;author&lt;/code&gt;. The &lt;code&gt;author&lt;/code&gt; field follows the format &lt;code&gt;urn:li:person:${userId}&lt;/code&gt;. Additionally, we specify:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The &lt;code&gt;lifecycleState&lt;/code&gt; as &lt;code&gt;"PUBLISHED"&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;visibility&lt;/code&gt; as &lt;code&gt;"PUBLIC"&lt;/code&gt; so the post is visible to the LinkedIn network.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Here’s the updated implementation:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;sharePost&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;token&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;content&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;SharePostArgs&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;//Get user's unique id&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;userId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;getUserId&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;token&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;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;resourcePath&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/ugcPosts&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;accessToken&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;token&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;entity&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;author&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`urn:li:person:&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;lifecycleState&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;PUBLISHED&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;      
      &lt;span class="na"&gt;visibility&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;com.linkedin.ugc.MemberNetworkVisibility&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;PUBLIC&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Next, we define the sharing content within the &lt;code&gt;specificContent&lt;/code&gt; field of the &lt;code&gt;entity&lt;/code&gt; object. For this scenario, the &lt;code&gt;specificContent&lt;/code&gt; field includes a &lt;code&gt;com.linkedin.ugc.ShareContent&lt;/code&gt; object, which has the following properties:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;shareCommentary&lt;/code&gt;: Accepts &lt;code&gt;content.text&lt;/code&gt; as the main text content to display.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;shareMediaCategory&lt;/code&gt;: Specifies the type of media shared in the post (set as &lt;code&gt;"ARTICLE"&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;media&lt;/code&gt;: An array of media assets for the &lt;code&gt;"ARTICLE"&lt;/code&gt; category, where each item includes: the URL to share and a &lt;code&gt;READY&lt;/code&gt; status.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Below is the updated code:&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="c1"&gt;//...&lt;/span&gt;
  &lt;span class="nx"&gt;entity&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;//...&lt;/span&gt;
    &lt;span class="nl"&gt;specificContent&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;com.linkedin.ugc.ShareContent&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;shareCommentary&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="na"&gt;text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;content&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="na"&gt;shareMediaCategory&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;ARTICLE&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;media&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
          &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;READY&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="na"&gt;originalUrl&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;content&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;]&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Upon successfully completing the request, the response contains a &lt;code&gt;createdEntityId&lt;/code&gt;, representing the unique ID of the created entity. We can return this value to the caller for further reference:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;sharePost&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;token&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;content&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;SharePostArgs&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;//...&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;createdEntityId&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And that's it! We’ve created a function that leverages the LinkedIn API to share a post URL on behalf of a user. In the next step, we’ll expose this function as a serverless endpoint using Netlify, bringing us closer to fully automating the process of sharing articles on social media.&lt;/p&gt;

&lt;h2&gt;
  
  
  Exposing as a Netlify serverless function
&lt;/h2&gt;

&lt;p&gt;We run the CLI command &lt;code&gt;netlify functions:create&lt;/code&gt; and follow the prompts to scaffold a new Netlify serverless function named &lt;code&gt;share-on-linkedin&lt;/code&gt;. The Netlify CLI will generate the function in the functions directory with the following initial code:&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="cm"&gt;/* functions/share-on-linkedin.mts */&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Context&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@netlify/functions&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Request&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Response&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Hello&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;content-type&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;application/json&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="p"&gt;})&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// Return an error response&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Response&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;An error occurred&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;}),&lt;/span&gt;
      &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;500&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;content-type&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;application/json&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In this above code, we use TypeScript and define the function as &lt;code&gt;async&lt;/code&gt; to handle the asynchronous nature of LinkedIn API calls.&lt;/p&gt;

&lt;p&gt;Next, we update the serverless function to perform the following actions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Parse the request body to extract the content to share, &lt;/li&gt;
&lt;li&gt;Retrieve the access token from environment variables (can be set in &lt;code&gt;.env&lt;/code&gt; file in the project root),&lt;/li&gt;
&lt;li&gt;Call the &lt;code&gt;sharePost&lt;/code&gt; function (defined earlier in &lt;code&gt;linkedin.ts&lt;/code&gt; ) with the extracted parameters, and&lt;/li&gt;
&lt;li&gt;Return the created entity ID as the response.
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Context&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@netlify/functions&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;sharePost&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;../utils/linkedin&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;//Retrieve the access token from environment variables&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;TOKEN&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;LINKEDIN_ACCESS_TOKEN&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Request&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;//Parse the request body&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;body&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;    
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;url&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="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;body&lt;/span&gt;

    &lt;span class="c1"&gt;//Share the post&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;createdEntityId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;sharePost&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;TOKEN&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;url&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="c1"&gt;//Return the created entity ID&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Response&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;createdEntityId&lt;/span&gt; &lt;span class="p"&gt;}),&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;content-type&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;application/json&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="p"&gt;})&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;//...&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;At this point, the serverless function is ready. We can deploy it to Netlify and test its functionality by making POST requests to the endpoint.&lt;/p&gt;

&lt;h2&gt;
  
  
  Testing the Functionality
&lt;/h2&gt;

&lt;p&gt;To test the serverless function, start a local server using the CLI command &lt;code&gt;netlify dev&lt;/code&gt;. Then, use a tool like Postman or Insomnia to send a POST request to the server endpoint with the following JSON payload:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"url"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://mayashavin.com/articles/share-onbehalf-linkedin"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"text"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Check out this awesome article!"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Alternatively, we can create a simple form UI to interact with the serverless function API, and verify that the post is successfully shared on LinkedIn.&lt;/p&gt;

&lt;p&gt;Once the function is working as expected, let's proceed to deploy it to Netlify to make it available for use.&lt;/p&gt;

&lt;h2&gt;
  
  
  Deploying with Netlify
&lt;/h2&gt;

&lt;p&gt;To deploy our function to Netlify, run the following command:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;netlify deploy &lt;span class="nt"&gt;--prod&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The CLI deploys the function to your Netlify production environment. You can then find the function endpoint in the Netlify dashboard.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Important:&lt;/strong&gt; Make sure to configure the &lt;code&gt;LINKEDIN_ACCESS_TOKEN&lt;/code&gt; environment variable in the dashboard. This step is essential for the function to authenticate and operate correctly.&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%2Fres.cloudinary.com%2Fmayashavin%2Fimage%2Fupload%2Fq_auto%2Ff_auto%2Fv1727777568%2Farticles%2Flinkedin%2Fenvironment_linkedin" 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%2Fres.cloudinary.com%2Fmayashavin%2Fimage%2Fupload%2Fq_auto%2Ff_auto%2Fv1727777568%2Farticles%2Flinkedin%2Fenvironment_linkedin" alt="Environment variable" width="685" height="92"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;p&gt;We have successfully built a serverless API to share an article URL on LinkedIn on behalf of a user, leveraging the LinkedIn API JavaScript Client and Netlify serverless functions. This marks a significant step toward automating the social media sharing process for blog posts.&lt;/p&gt;

&lt;p&gt;From here, we can extend the automation workflow to include other social media platforms and scheduled tasks. For example, we could integrate platforms like X (formerly Twitter), Facebook, or BlueSky, and customize the timing and content of posts to maximize audience engagement and reach.&lt;/p&gt;

&lt;p&gt;With that, stay tuned for more updates on this series!&lt;/p&gt;

&lt;p&gt;👉 &lt;em&gt;Learn about Vue 3 and TypeScript with my new book &lt;a href="https://www.oreilly.com/library/view/learning-vue/9781492098812/" rel="noopener noreferrer"&gt;Learning Vue&lt;/a&gt;!&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;👉 &lt;em&gt;Want to support me? &lt;a href="https://www.buymeacoffee.com/VTLRKH6" rel="noopener noreferrer"&gt;Buy me a coffee&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Like this post or find it helpful? Share it 😉&lt;/p&gt;

</description>
      <category>linkedin</category>
      <category>serverless</category>
      <category>javascript</category>
      <category>netlify</category>
    </item>
    <item>
      <title>Efficient Blog Cover Image Generation with CoCover for VS Code</title>
      <dc:creator>Maya Shavin 🌷☕️🏡</dc:creator>
      <pubDate>Mon, 04 Nov 2024 13:43:20 +0000</pubDate>
      <link>https://dev.to/mayashavin/efficient-blog-cover-image-generation-with-cocover-for-vs-code-2hja</link>
      <guid>https://dev.to/mayashavin/efficient-blog-cover-image-generation-with-cocover-for-vs-code-2hja</guid>
      <description>&lt;p&gt;&lt;em&gt;As a developer, I often find myself writing technical blog posts and articles in markdown format to share my knowledge with the community. Part of my content creation routine is choosing the cover image, which helps attract readers and make the content more engaging. However, this task is always time-consuming and requires additional tools and resources, leading to a significant amount of context-switching and breaking my flow.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;To address this challenge, I've recently developed and lauched &lt;a href="https://marketplace.visualstudio.com/items?itemName=mayashavinstudio.cocover" rel="noopener noreferrer"&gt;CoCover&lt;/a&gt;, a new GitHub Copilot extension for VS Code, aiming to make your content creation process experience with markdown and VS Code more efficient and seamless.&lt;/p&gt;


  


&lt;p&gt;So what does CoCover offer? Let's take a look!&lt;/p&gt;

&lt;h2&gt;
  
  
  CoCover - The AI assistant for generating cover images and more
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://marketplace.visualstudio.com/items?itemName=mayashavinstudio.cocover" rel="noopener noreferrer"&gt;CoCover&lt;/a&gt; - &lt;strong&gt;Co&lt;/strong&gt;pilot for &lt;strong&gt;Cover&lt;/strong&gt; image - leverages the power of GitHub CoPilot and Dalle-3 for image generation based on your content, without leaving your VS Code editor.&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%2Fres.cloudinary.com%2Fmayashavin%2Fimage%2Fupload%2Ff_auto%2Cq_auto%2Fv1730363067%2Fcocover%2Fcocover_marketplace_2" 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%2Fres.cloudinary.com%2Fmayashavin%2Fimage%2Fupload%2Ff_auto%2Cq_auto%2Fv1730363067%2Fcocover%2Fcocover_marketplace_2" alt="CoCover in VS Code Marketplace" width="800" height="245"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This extension adds a new &lt;a href="https://code.visualstudio.com/api/extension-guides/chat" rel="noopener noreferrer"&gt;Chat Participant&lt;/a&gt; as &lt;code&gt;@CoCover&lt;/code&gt; assistant using the new VS Code's Chat extension API, and offers the following features:&lt;/p&gt;

&lt;h3&gt;
  
  
  Generate cover image based on given content
&lt;/h3&gt;

&lt;p&gt;After selecting a section of relevant information (e.g., title and description) in your markdown file and typing &lt;code&gt;@cocover /image&lt;/code&gt;, the extension will generate a cover image based on the selected content and any additional prompt you provide in your Copilot chat. &lt;a href="https://platform.openai.com/api-keys" rel="noopener noreferrer"&gt;OpenAI API key&lt;/a&gt; is required to use this feature, as CoCover uses OpenAI's Dalle-3 model to complete the task.&lt;/p&gt;


  


&lt;p&gt;You can then preview the image in the editor and decide whether to save it locally, or upload it to Cloudinary.&lt;/p&gt;

&lt;h3&gt;
  
  
  Upload generated image to Cloudinary
&lt;/h3&gt;

&lt;p&gt;Upon selecting the &lt;a href="https://cloudinary.com/" rel="noopener noreferrer"&gt;Cloudinary&lt;/a&gt; option, CoCover will prompt you for your Cloudinary credentials for uploading, including your Cloud name, API key, and API secret. And then, it will upload the generated image to your Cloudinary account and provide you with the secure image URL, ready for use.&lt;/p&gt;


  


&lt;p&gt;Once uploaded, you can copy the image URL to your clipboard, or have CoCover insert it directly into your markdown file, as a &lt;code&gt;cover_image&lt;/code&gt; frontmatter, completing the cover image creation process.&lt;/p&gt;


  


&lt;p&gt;Another option is to save the image locally, which we'll explore next.&lt;/p&gt;

&lt;h3&gt;
  
  
  Save generated image to a local destination
&lt;/h3&gt;

&lt;p&gt;CoCover will prompt you to select a destination folder to save the generated image, in &lt;code&gt;.png&lt;/code&gt; or &lt;code&gt;.jpg&lt;/code&gt; format.&lt;/p&gt;


  


&lt;p&gt;Once saved, CoCover will also give you the option to insert the image URL into your markdown file, as a &lt;code&gt;cover_image&lt;/code&gt; frontmatter, or copy the image URL to your clipboard.&lt;/p&gt;

&lt;p&gt;With these features, CoCover aims to streamline your content creation process directly from your VS Code editor and reduce the time, effort and amount of context-switching involved in getting a professional-looking cover image for your blog posts and articles. No more searching for stock images or manually creating and uploading cover images - let CoCover do the heavy lifting for you!&lt;/p&gt;




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

&lt;p&gt;I'm excited to see how CoCover can help making your content creation experience in VS Code more delightful and to add more features such as suggesting content fixes to improve your writing quality. I'm looking forward to your feedback and suggestions for future improvements. &lt;/p&gt;

&lt;p&gt;Feel free to &lt;a href="https://marketplace.visualstudio.com/items?itemName=mayashavinstudio.cocover" rel="noopener noreferrer"&gt;install CoCover&lt;/a&gt;, give it a try, and let me know what you think! Or star 🌟 it on &lt;a href="https://github.com/mayashavin/cocover" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;!&lt;/p&gt;

&lt;p&gt;👉 &lt;em&gt;Learn about Vue 3 and TypeScript with my new book &lt;a href="https://www.oreilly.com/library/view/learning-vue/9781492098812/" rel="noopener noreferrer"&gt;Learning Vue&lt;/a&gt;!&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;👉 &lt;em&gt;If you'd like to catch up with me sometimes, follow me on &lt;a href="https://x.com/MayaShavin" rel="noopener noreferrer"&gt;X&lt;/a&gt; | &lt;a href="https://www.linkedin.com/in/mayashavin" rel="noopener noreferrer"&gt;LinkedIn&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Like this post or find it helpful? Share it 👇🏼 😉&lt;/p&gt;

</description>
      <category>vscode</category>
      <category>githubcopilot</category>
      <category>ai</category>
      <category>llm</category>
    </item>
    <item>
      <title>Effective Visual Regression Testing for Developers: Vitest vs Playwright</title>
      <dc:creator>Maya Shavin 🌷☕️🏡</dc:creator>
      <pubDate>Wed, 30 Oct 2024 06:27:15 +0000</pubDate>
      <link>https://dev.to/mayashavin/effective-visual-regression-testing-for-developers-vitest-vs-playwright-3la</link>
      <guid>https://dev.to/mayashavin/effective-visual-regression-testing-for-developers-vitest-vs-playwright-3la</guid>
      <description>&lt;p&gt;&lt;em&gt;Visual regression testing plays a crucial role in ensuring the UI and UX's consistency across different browsers, devices, and even screen sizes, especially for large applications. In this post, we will covers snapshot and pixel-to-pixel comparison with Vitest and Playwright, explore limitations, and provide insights into choosing the right visual testing approach for your project.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Table of Contents
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Table of Contents&lt;/li&gt;
&lt;li&gt;Prequisites&lt;/li&gt;
&lt;li&gt;What is visual regression testing?&lt;/li&gt;
&lt;li&gt;
Using snapshot for visual regression comparison in Vitest

&lt;ul&gt;
&lt;li&gt;Limitations of snapshot visual testing&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;Pixel-to-pixel screenshot testing in Playwright&lt;/li&gt;

&lt;li&gt;Screenshot visual testing with Vitest's browser mode and Playwright&lt;/li&gt;

&lt;li&gt;Summary&lt;/li&gt;

&lt;/ul&gt;

&lt;h2&gt;
  
  
  Prequisites
&lt;/h2&gt;

&lt;p&gt;For the demo purpose, we will use a Vue 3 project with Vitest and Playwright installed, with a &lt;code&gt;SearchBox&lt;/code&gt; component as follows:&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%2Fres.cloudinary.com%2Fmayashavin%2Fimage%2Fupload%2Fq_auto%2Ff_auto%2Fv1727777568%2Farticles%2Ftesting%2FSearchbox" 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%2Fres.cloudinary.com%2Fmayashavin%2Fimage%2Fupload%2Fq_auto%2Ff_auto%2Fv1727777568%2Farticles%2Ftesting%2FSearchbox" alt="Search input box component" width="648" height="178"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The implementation of the &lt;code&gt;SearchBox&lt;/code&gt; component is available in &lt;a href="https://mayashavin.com/articles/component-testing-browser-vitest#the-searchbox-component" rel="noopener noreferrer"&gt;this post&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;For React developers&lt;/strong&gt;, you can replace the Vue component with a React one, and Vue Test Utils with React Testing Library to render component. The rest of the steps below remains the same.&lt;/p&gt;

&lt;h2&gt;
  
  
  What is visual regression testing?
&lt;/h2&gt;

&lt;p&gt;Visual regression testing (&lt;em&gt;or visual testing&lt;/em&gt;) is a technique to verify the visual appearance and usability of an application's UI between changes, helping to identify any unintended bugs that might occur to the existing functionalities during the development process. It focuses solely on validating the visual aspects of a component that a user sees or interacts with, including layout, styling, and other visual elements.&lt;/p&gt;

&lt;p&gt;There are several types of visual testing, including DOM-based comparison (snapshot testing), pixel-based comparison and manual comparison using screenshots. Depending on the requirements, we can implement visual testing at different levels with different tools, such as unit testing with Vitest or E2E testing with Playwright, or combining the two.&lt;/p&gt;

&lt;p&gt;We'll start with visual unit testing using Vitest.&lt;/p&gt;

&lt;h2&gt;
  
  
  Using snapshot for visual regression comparison in Vitest
&lt;/h2&gt;

&lt;p&gt;Vitest provides a &lt;code&gt;toMatchSnapshot()&lt;/code&gt; method to take a snapshot of the component and compare it with an existing one, as seen in the following test for &lt;code&gt;SearchBox&lt;/code&gt; Vue component:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;render&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@vitest/vue&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;SearchBox&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./SearchBox.vue&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="nf"&gt;describe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;SearchBox component&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;it&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;should match snapshot&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;wrapper&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;mount&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;SearchBox&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="cm"&gt;/** props and other global plugins */&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;

    &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;wrapper&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toMatchSnapshot&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Upon the first run, Vitest will take a snapshot of the &lt;code&gt;SearchBox&lt;/code&gt; component and store it in a &lt;code&gt;.snap&lt;/code&gt; file within the &lt;code&gt;__snapshots__&lt;/code&gt; folder:&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%2Fres.cloudinary.com%2Fmayashavin%2Fimage%2Fupload%2Fq_auto%2Ff_auto%2Fv1727777568%2Farticles%2Ftesting%2Fsnapshot_folder" 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%2Fres.cloudinary.com%2Fmayashavin%2Fimage%2Fupload%2Fq_auto%2Ff_auto%2Fv1727777568%2Farticles%2Ftesting%2Fsnapshot_folder" alt="Screenshot of the snapshot file's location" width="416" height="253"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Below shows an example of the snapshot file, containing the component's DOM structures and HTML layout:&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%2Fres.cloudinary.com%2Fmayashavin%2Fimage%2Fupload%2Fq_auto%2Ff_auto%2Fv1727777568%2Farticles%2Ftesting%2Fsnapshot_vitest.gif" 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%2Fres.cloudinary.com%2Fmayashavin%2Fimage%2Fupload%2Fq_auto%2Ff_auto%2Fv1727777568%2Farticles%2Ftesting%2Fsnapshot_vitest.gif" alt="Screenshot of how a snapshot file for SearchBox looks like" width="800" height="497"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;In the next run, Vitest will generate a new snapshot and compare it with the stored one. If they are different, for example, when we modify the input's placeholder to &lt;code&gt;Search for a beer&lt;/code&gt;, the test will fail and Vitest will highlight the difference will in the terminal as follows:&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%2Fres.cloudinary.com%2Fmayashavin%2Fimage%2Fupload%2Fq_auto%2Ff_auto%2Fv1727777568%2Farticles%2Ftesting%2Ffailed_snapshot_test" 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%2Fres.cloudinary.com%2Fmayashavin%2Fimage%2Fupload%2Fq_auto%2Ff_auto%2Fv1727777568%2Farticles%2Ftesting%2Ffailed_snapshot_test" alt="Screenshot of how a snapshot file for SearchBox looks like" width="468" height="110"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Base on this result, we can decide whether to re-visit the changes, or update the snapshot file with the &lt;code&gt;-u&lt;/code&gt; flag in the Vitest run command to accept them.&lt;/p&gt;

&lt;p&gt;With that, we have enabled snapshot visual testing for our &lt;code&gt;SearchBox&lt;/code&gt; component. However, there is a downside of using this method, which we will discuss in the next section.&lt;/p&gt;

&lt;h3&gt;
  
  
  Limitations of snapshot visual testing
&lt;/h3&gt;

&lt;p&gt;Regular snapshot visual testing, unfortunately, does not fully provide a great developer experience, and high reliability level in visual consistency. It only compares the DOM structure and basic HTML layout of the component generated, without considering other styling aspects like colors, fonts, etc. For instance, if we change the input field &lt;code&gt;#searchbox&lt;/code&gt;'s color from black to red:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;style&amp;gt;&lt;/span&gt;
&lt;span class="nf"&gt;#searchbox&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;color&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="no"&gt;red&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/style&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The snapshot test will still pass, since it is unable to validate the style changes of the component. This limitation makes it less reliable for visual testing, especially for components with dynamic content or styles.&lt;/p&gt;

&lt;p&gt;Another limitation is the readability of the snapshot files, which can lead to a maintenance nightmare, as developers may find it hard to identify the changes in these files for large components. After all, visual means what you see, and this snapshot testing does not provide a proper "see" representation.&lt;/p&gt;

&lt;p&gt;A better alternative is using screenshot-based visual testing with Playwright, which we will discuss next.&lt;/p&gt;

&lt;h2&gt;
  
  
  Pixel-to-pixel screenshot testing in Playwright
&lt;/h2&gt;

&lt;p&gt;To perform an E2E visual regression validation, Playwright provides a &lt;code&gt;toHaveScreenshot()&lt;/code&gt; method that takes and compares screenshots of a specific component (in component testing), an element, or the whole page, as seen below for our &lt;code&gt;SearchBox&lt;/code&gt; component:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="cm"&gt;/** e2e/searchbox.spec.js */&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;test&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;expect&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@playwright/experimental-ct-vue&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;SearchBox&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;../src/components/SearchBox.vue&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;should match screenshot&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;mount&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;component&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;mount&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;SearchBox&lt;/span&gt; &lt;span class="nx"&gt;searchTerm&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;hello&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;/&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toHaveScreenshot&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;component&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toHaveScreenshot&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

  &lt;span class="cm"&gt;/** other assertions */&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On the first run, Playwright captures and stores the screenshots as an &lt;code&gt;.png&lt;/code&gt; files to the &lt;code&gt;__snapshots__\searchbox.spec.js-snapshots&lt;/code&gt; folder, with each file named based on the test case's name (&lt;em&gt;should match screenshot&lt;/em&gt;), the testing browser (&lt;em&gt;chrome&lt;/em&gt;) and the platform (&lt;em&gt;darwin&lt;/em&gt;), as follows:&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%2Fres.cloudinary.com%2Fmayashavin%2Fimage%2Fupload%2Fq_auto%2Ff_auto%2Fv1727777568%2Farticles%2Ftesting%2Fscreenshot_folder" 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%2Fres.cloudinary.com%2Fmayashavin%2Fimage%2Fupload%2Fq_auto%2Ff_auto%2Fv1727777568%2Farticles%2Ftesting%2Fscreenshot_folder" alt="Screenshot of the screenshot files' location" width="582" height="261"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The generated screenshot file contains the view of the full page (&lt;em&gt;1&lt;/em&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%2Fres.cloudinary.com%2Fmayashavin%2Fimage%2Fupload%2Fq_auto%2Ff_auto%2Fv1730224858%2Farticles%2Ftesting%2Fsearchbox_page" 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%2Fres.cloudinary.com%2Fmayashavin%2Fimage%2Fupload%2Fq_auto%2Ff_auto%2Fv1730224858%2Farticles%2Ftesting%2Fsearchbox_page" alt="browser's page contains an input field with hello as initial value" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;And of the component (&lt;em&gt;2&lt;/em&gt;), as follows:&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%2Fres.cloudinary.com%2Fmayashavin%2Fimage%2Fupload%2Fq_auto%2Ff_auto%2Fv1727777568%2Farticles%2Ftesting%2Fsearchbox_full" 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%2Fres.cloudinary.com%2Fmayashavin%2Fimage%2Fupload%2Fq_auto%2Ff_auto%2Fv1727777568%2Farticles%2Ftesting%2Fsearchbox_full" alt="input field with hello as initial value" width="800" height="13"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The runner also throws an error. From the second run onwards, Playwright will compare the new screenshots with the current stored ones, and highlight the pixel difference (with ratio) in the terminal, such as when we change the input's font color to red:&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%2Fres.cloudinary.com%2Fmayashavin%2Fimage%2Fupload%2Fq_auto%2Ff_auto%2Fv1727777568%2Farticles%2Ftesting%2Fpixel_error_terminal" 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%2Fres.cloudinary.com%2Fmayashavin%2Fimage%2Fupload%2Fq_auto%2Ff_auto%2Fv1727777568%2Farticles%2Ftesting%2Fpixel_error_terminal" alt="failed screenshot comparison with 65 pixels difference" width="800" height="123"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;An image of the expected result, the actual result, and the difference between them is also available in the test result dashboard in the browser:&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%2Fres.cloudinary.com%2Fmayashavin%2Fimage%2Fupload%2Fq_auto%2Ff_auto%2Fv1727777568%2Farticles%2Ftesting%2Fdifference_screenshot" 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%2Fres.cloudinary.com%2Fmayashavin%2Fimage%2Fupload%2Fq_auto%2Ff_auto%2Fv1727777568%2Farticles%2Ftesting%2Fdifference_screenshot" alt="actual vs expected side by side" width="800" height="610"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Additionally, we can adjust different the screenshot matcher's configurations like the maximum pixel difference &lt;code&gt;maxPixelDifference&lt;/code&gt;, and &lt;code&gt;threshold&lt;/code&gt; level by providing them as options to the &lt;code&gt;toHaveScreenshot()&lt;/code&gt; method as follows:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toHaveScreenshot&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;maxPixelDifference&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;threshold&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;0.1&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;With the above settings, Playwright will allow a maximum of 100 pixels difference and a threshold of 0.1 for the screenshot comparison. This way, we can fine-tune our visual testing to meet the project's requirements.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;toHaveScreenshot()&lt;/code&gt; method is a great tool, as it provides a pixel-to-pixel visual comparison for the component. However, it only works with native Playwright tests, and isn't supported in Vitest's browser mode. For that, we need to use &lt;code&gt;screenshot()&lt;/code&gt; method in the next section.&lt;/p&gt;

&lt;h2&gt;
  
  
  Screenshot visual testing with Vitest's browser mode and Playwright
&lt;/h2&gt;

&lt;p&gt;We first need to install the relevant packages and enable the browser mode in Vitest, following &lt;a href="https://mayashavin.com/articles/component-testing-browser-vitest" rel="noopener noreferrer"&gt;this tutorial&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Once enabled, Vitest will then use Playwright to run all the tests in browser mode. Similar to the native Playwright, within a test, we can use &lt;code&gt;screenshot()&lt;/code&gt; method to take a screenshot of a specific element, such as &lt;code&gt;input&lt;/code&gt;, and save it to the &lt;code&gt;path&lt;/code&gt; provided:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="cm"&gt;/**... */&lt;/span&gt;
  &lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;renders search input&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="cm"&gt;/**... */&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;input&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;component&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getByTestId&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;search-input&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;screenshot&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;screenshots/searchbox_default_full.png&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;})&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The above code results in a screenshot of the input field, as shown below:&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%2Fres.cloudinary.com%2Fmayashavin%2Fimage%2Fupload%2Fq_auto%2Ff_auto%2Fv1727777568%2Farticles%2Ftesting%2Fsearchbox_default_full" 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%2Fres.cloudinary.com%2Fmayashavin%2Fimage%2Fupload%2Fq_auto%2Ff_auto%2Fv1727777568%2Farticles%2Ftesting%2Fsearchbox_default_full" alt="input field with hello as initial value" width="228" height="34"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;For the page's screenshot, we use the &lt;code&gt;page&lt;/code&gt; object, from &lt;code&gt;@vitest/browser/context&lt;/code&gt;, which is similar to the native Playwright's &lt;code&gt;page&lt;/code&gt; object:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@vitest/browser/context&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="cm"&gt;/**... */&lt;/span&gt;
  &lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;renders search input&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="cm"&gt;/**... */&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;screenshot&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;screenshots/searchbox_page.png&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Similar to &lt;code&gt;toHaveScreenshot()&lt;/code&gt;, we can also adjust the screenshot's configuration by providing additional options to the &lt;code&gt;screenshot()&lt;/code&gt; method, such as &lt;code&gt;type&lt;/code&gt;, &lt;code&gt;clip&lt;/code&gt;, &lt;code&gt;omitBackground&lt;/code&gt;, etc., to define the file format, or capture the component's specific area.&lt;/p&gt;

&lt;p&gt;Upon running the test, Vitest will take the screenshots and store them accordingly, ready for us to manually verify the UI, or integrate with an analyzer such as &lt;a href="https://applitools.com/" rel="noopener noreferrer"&gt;Applitools&lt;/a&gt; or &lt;a href="https://www.browserstack.com/docs/percy/integrate/playwright" rel="noopener noreferrer"&gt;Percy&lt;/a&gt; for visual comparison in an automated workflow.&lt;/p&gt;

&lt;p&gt;But that's also the limitation of using Playwright's &lt;code&gt;screenshot()&lt;/code&gt;, as it requires manual verification or integration with third-party tools, which can generate additional costs. At the time of writing, Vitest browser mode does not support Playwright's &lt;code&gt;toHaveScreenshot()&lt;/code&gt; method or &lt;code&gt;toMatchImageSnapshot()&lt;/code&gt; matcher from &lt;a href="https://vitest.dev/guide/snapshot#image-snapshots" rel="noopener noreferrer"&gt;Jest-Image-Snapshot&lt;/a&gt;, which would have been a better option.&lt;/p&gt;




&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;p&gt;We have explored briefly different methods of visual testing with Vitest and Playwright, from manual to automated visual comparison with snapshot and screenshot, and the pros and cons of each approach. It is essential to consider your project's requirements and budget for choosing the suitable testing approach and keep the process efficient.&lt;/p&gt;

&lt;p&gt;What's next? How about trying it out yourself and see which method works best for your project? Let me know your thoughts in the comments below!&lt;/p&gt;

&lt;p&gt;👉 &lt;em&gt;Learn about Vue 3 and TypeScript with my new book &lt;a href="https://www.oreilly.com/library/view/learning-vue/9781492098812/" rel="noopener noreferrer"&gt;Learning Vue&lt;/a&gt;!&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;👉 &lt;em&gt;Follow me on &lt;a href="https://x.com/MayaShavin" rel="noopener noreferrer"&gt;X&lt;/a&gt; | &lt;a href="https://www.linkedin.com/in/mayashavin" rel="noopener noreferrer"&gt;LinkedIn&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Like this post or find it helpful? Share it 👇🏼 or &lt;a href="https://www.buymeacoffee.com/VTLRKH6" rel="noopener noreferrer"&gt;Buy me a coffee&lt;/a&gt;&lt;/p&gt;

</description>
      <category>testing</category>
      <category>vue</category>
      <category>playwrightjs</category>
      <category>vitest</category>
    </item>
    <item>
      <title>Reliable Component Testing with Vitest's Browser Mode and Playwright</title>
      <dc:creator>Maya Shavin 🌷☕️🏡</dc:creator>
      <pubDate>Tue, 08 Oct 2024 14:10:00 +0000</pubDate>
      <link>https://dev.to/mayashavin/reliable-component-testing-with-vitests-browser-mode-and-playwright-k9m</link>
      <guid>https://dev.to/mayashavin/reliable-component-testing-with-vitests-browser-mode-and-playwright-k9m</guid>
      <description>&lt;p&gt;&lt;em&gt;Vitest is great for unit testing. But for frontend components that rely on user interactions, browser events, and other visual states, unit testing alone is not enough. We also need to ensure the component looks and behaves as expected in an actual browser. And to simulate the browser environment, Vitest requires packages like JSDOM or HappyDOM, which are not always reliable as the real ones.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;An alternative is to use &lt;a href="https://mayashavin.com/articles/component-testing-router-playwright" rel="noopener noreferrer"&gt;Playwright's Component Testing&lt;/a&gt;. However, this solution requires separate setup and run, which can be cumbersome in many cases.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;This is where Vitest's browser mode comes in.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Table of Contents
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Table of Contents&lt;/li&gt;
&lt;li&gt;Prequisites&lt;/li&gt;
&lt;li&gt;The SearchBox component&lt;/li&gt;
&lt;li&gt;Enable Vitest's browser mode with Playwright&lt;/li&gt;
&lt;li&gt;Add the first browser test for SearchBox&lt;/li&gt;
&lt;li&gt;Using the workspace configuration file&lt;/li&gt;
&lt;li&gt;Run and view the results&lt;/li&gt;
&lt;li&gt;Summary&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Prequisites
&lt;/h2&gt;

&lt;p&gt;You should have a Vue project set up with &lt;a href="https://router.vuejs.org/" rel="noopener noreferrer"&gt;Vue Router&lt;/a&gt; and &lt;a href="https://vitejs.dev/guide/features.html#testing" rel="noopener noreferrer"&gt;Vitest&lt;/a&gt;. If you haven't done so, refer to &lt;a href="https://mayashavin.com/articles/testing-components-with-vitest" rel="noopener noreferrer"&gt;this post&lt;/a&gt; to set up the essentisal Vitest testing environment for your Vue project.&lt;/p&gt;

&lt;p&gt;Once ready, let's create our testing component &lt;code&gt;SearchBox&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  The SearchBox component
&lt;/h2&gt;

&lt;p&gt;Our SearchBox component accepts a search term and syncs it with the URL query params. Its template is as follows:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;  &lt;span class="nt"&gt;&amp;lt;label&lt;/span&gt; &lt;span class="na"&gt;for=&lt;/span&gt;&lt;span class="s"&gt;"searchbox"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Search&lt;span class="nt"&gt;&amp;lt;/label&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;input&lt;/span&gt; &lt;span class="na"&gt;v-model=&lt;/span&gt;&lt;span class="s"&gt;"search"&lt;/span&gt; 
    &lt;span class="na"&gt;placeholder=&lt;/span&gt;&lt;span class="s"&gt;"Search for a pizza"&lt;/span&gt; 
    &lt;span class="na"&gt;data-testid=&lt;/span&gt;&lt;span class="s"&gt;"search-input"&lt;/span&gt; 
    &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"searchbox"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With the &lt;code&gt;script&lt;/code&gt; section:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;useRouter&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;vue-router&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;useSearch&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;../composables/useSearch&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;watch&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;vue&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;props&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;defineProps&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;searchTerm&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;String&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;required&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="na"&gt;default&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;""&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;router&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useRouter&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;search&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useSearch&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;defaultSearch&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;props&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;searchTerm&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="nf"&gt;watch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;search&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;prevValue&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nx"&gt;prevValue&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nx"&gt;router&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;query&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;search&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;immediate&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And in the browser, it will look like this:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fres.cloudinary.com%2Fmayashavin%2Fimage%2Fupload%2Fq_auto%2Ff_auto%2Fv1727777568%2Farticles%2Ftesting%2FSearchbox" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fres.cloudinary.com%2Fmayashavin%2Fimage%2Fupload%2Fq_auto%2Ff_auto%2Fv1727777568%2Farticles%2Ftesting%2FSearchbox" alt="Search input box component" width="648" height="178"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Next, we will set up the browser mode for Vitest.&lt;/p&gt;

&lt;h2&gt;
  
  
  Enable Vitest's browser mode with Playwright
&lt;/h2&gt;

&lt;p&gt;In &lt;code&gt;vitest.config.js&lt;/code&gt;, we will setup &lt;code&gt;browser&lt;/code&gt; mode as below:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// vitest.config.js&lt;/span&gt;
&lt;span class="cm"&gt;/*...*/&lt;/span&gt;
&lt;span class="nf"&gt;defineConfig&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;test&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="cm"&gt;/**... */&lt;/span&gt;
    &lt;span class="na"&gt;browser&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;enabled&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;chromium&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;provider&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;playwright&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;providerOptions&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{},&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In which, we configure the following:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;enabled&lt;/code&gt;: enable the browser mode&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;name&lt;/code&gt;: the browser to run the tests in (&lt;code&gt;chromium&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;provider&lt;/code&gt;: the test provider for running the browser, such as &lt;code&gt;playwright&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;providerOptions&lt;/code&gt;: additional configuration for the test provider.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;We also specify which folder (&lt;code&gt;tests\browser&lt;/code&gt;) and the file convention to use, avoiding any conflicts with any existing regular unit tests:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// vitest.config.js&lt;/span&gt;
&lt;span class="cm"&gt;/*...*/&lt;/span&gt;
&lt;span class="nf"&gt;defineConfig&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;test&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="cm"&gt;/**... */&lt;/span&gt;
    &lt;span class="na"&gt;include&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;tests/browser/**/*.{spec,test}.{js,ts}&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With that, we are ready to write our first browser test for &lt;code&gt;SearchBox&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Add the first browser test for SearchBox
&lt;/h2&gt;

&lt;p&gt;In the &lt;code&gt;tests/browser&lt;/code&gt; folder, we create a new file &lt;code&gt;SearchBox.spec.js&lt;/code&gt; with the following code:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="cm"&gt;/**SearchBox.spec.js */&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;test&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;describe&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;vitest&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;SearchBox&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@/components/SearchBox.vue&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nf"&gt;describe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;SearchBox&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;renders search input&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="cm"&gt;/** Test logic here */&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;To render &lt;code&gt;SearchBox&lt;/code&gt;, we use &lt;code&gt;render()&lt;/code&gt; from &lt;code&gt;vitest-browser-vue&lt;/code&gt;, and pass the initial search term as a &lt;code&gt;prop&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="cm"&gt;/**SearchBox.spec.js */&lt;/span&gt;
&lt;span class="cm"&gt;/**... */&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;render&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;vitest-browser-vue&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nf"&gt;describe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;SearchBox&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;renders search input&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;component&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;render&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;SearchBox&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;props&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;searchTerm&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;hello&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Since &lt;code&gt;SearchBox&lt;/code&gt; is using &lt;code&gt;router&lt;/code&gt; from &lt;code&gt;useRouter()&lt;/code&gt; from Vue Router, we need the following router setup:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Create a mock router using &lt;code&gt;createRouter()&lt;/code&gt;:
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;  &lt;span class="cm"&gt;/** SearchBox.spec.js */&lt;/span&gt;
  &lt;span class="cm"&gt;/**... */&lt;/span&gt;
  &lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;routes&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@/router&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;createRouter&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;createWebHistory&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;vue-router&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;router&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;createRouter&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;history&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;createWebHistory&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
    &lt;span class="na"&gt;routes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;routes&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;ul&gt;
&lt;li&gt;Pass it as a global plugin to &lt;code&gt;render()&lt;/code&gt;:
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;  &lt;span class="cm"&gt;/** SearchBox.spec.js */&lt;/span&gt;
    &lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;renders search input&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;component&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;render&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;SearchBox&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="cm"&gt;/**... */&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
        &lt;span class="na"&gt;global&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="na"&gt;plugins&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;router&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
      &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Once done, we locate the input element by its &lt;code&gt;data-testid&lt;/code&gt;, and assert its initial value using &lt;code&gt;toHaveValue()&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;  &lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;renders search input&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="cm"&gt;/**... */&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;input&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;component&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getByTestId&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;search-input&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;element&lt;/span&gt;&lt;span class="p"&gt;()).&lt;/span&gt;&lt;span class="nf"&gt;toHaveValue&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;hello&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Note here &lt;code&gt;input&lt;/code&gt; received is just a &lt;code&gt;Locator&lt;/code&gt; and not a valid HTML element. We need &lt;code&gt;input.element()&lt;/code&gt; to get the HTML instance. Otherwise, Vitest will throw the below error:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fres.cloudinary.com%2Fmayashavin%2Fimage%2Fupload%2Fq_auto%2Ff_auto%2Fv1727777568%2Farticles%2Ftesting%2Ferror_element" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fres.cloudinary.com%2Fmayashavin%2Fimage%2Fupload%2Fq_auto%2Ff_auto%2Fv1727777568%2Farticles%2Ftesting%2Ferror_element" alt="Error of value needed to be HTML or SVG element" width="800" height="93"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;To change the input's value, we use &lt;code&gt;input.fill()&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;  &lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;renders search input&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="cm"&gt;/**... */&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fill&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;test&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Alternatively, we can use &lt;code&gt;userEvent()&lt;/code&gt; from &lt;code&gt;@vitest/browser/context&lt;/code&gt; as follows:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;userEvent&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@vitest/browser/context&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;

&lt;span class="cm"&gt;/**... */&lt;/span&gt;
  &lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;renders search input&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="cm"&gt;/**... */&lt;/span&gt;    
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;userEvent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fill&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;test&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Both approaches perform the same. We can then assert the new value as usual:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;element&lt;/span&gt;&lt;span class="p"&gt;()).&lt;/span&gt;&lt;span class="nf"&gt;toHaveValue&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;test&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's it! We have successfully written our first browser test.&lt;/p&gt;

&lt;p&gt;At this point, we have one test configuration set for our Vitest runner. This setup will be problematic when Vitest need to run both unit and browser tests together in an automation workflow. For such cases, we use workspace and separate the settings per test type, which we explore next.&lt;/p&gt;

&lt;h2&gt;
  
  
  Using the workspace configuration file
&lt;/h2&gt;

&lt;p&gt;We create a new file &lt;code&gt;vitest.workspace.js&lt;/code&gt; to store the workspace configurations as follows:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;defineWorkspace&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;vitest/config&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="nf"&gt;defineWorkspace&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;extends&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;vitest.config.js&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;test&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;jsdom&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;unit&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;include&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;**/*/unit/*.{spec,test}.{js,ts}&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;])&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In which, we define the first configuration for &lt;code&gt;unit&lt;/code&gt; tests using &lt;code&gt;jsdom&lt;/code&gt;, based on the existing &lt;code&gt;vitest.config.js&lt;/code&gt; settings. We also specify the folder and file convention for the unit tests.&lt;/p&gt;

&lt;p&gt;Similarly, we define the second configuration for &lt;code&gt;browser&lt;/code&gt; tests using &lt;code&gt;playwright&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="nf"&gt;defineWorkspace&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;
  &lt;span class="cm"&gt;/** ... */&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;extends&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;vitest.config.js&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;test&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;include&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;**/*/browser/*.{spec,test}.{js,ts}&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
      &lt;span class="na"&gt;browser&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;enabled&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;chromium&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;provider&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;playwright&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;browser&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;])&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And with that, we can run all our tests in a single command, which we will see next.&lt;/p&gt;

&lt;h2&gt;
  
  
  Run and view the results
&lt;/h2&gt;

&lt;p&gt;We add the following command to our &lt;code&gt;package.json&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"scripts"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"test"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"vitest --workspace=vitest.workspace.js"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Upon executing &lt;code&gt;yarn test&lt;/code&gt;, Vitest runs the tests based on &lt;code&gt;vitest.workspace.js&lt;/code&gt; and displays the results in a GUI dashboard as follows:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fres.cloudinary.com%2Fmayashavin%2Fimage%2Fupload%2Fq_auto%2Ff_auto%2Fv1727777568%2Farticles%2Ftesting%2FVitest_UI" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fres.cloudinary.com%2Fmayashavin%2Fimage%2Fupload%2Fq_auto%2Ff_auto%2Fv1727777568%2Farticles%2Ftesting%2FVitest_UI" alt="Dashboard of Vitest result with each test labeled by type" width="800" height="386"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Vitest labels each test by &lt;code&gt;unit&lt;/code&gt; or &lt;code&gt;browser&lt;/code&gt; status. We can then filter the tests by their statuses, or perform further debugging with the given browser UI per test suite.&lt;/p&gt;




&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;p&gt;We have learned how to set up browser mode for Vitest using Playwright, and write the first browser test. We have also explored how to take screenshots for visual testing, and use the workspace configuration to separate the settings per testing mode. One big limitation of Vitest's browser mode in comparison to Playwright's Component Testing is the lack of browser's address bar, limiting us from testing the component's state synchronization with URL query params in the browser. But it's a good start to build a scalable testing strategy for our Vue projects.&lt;/p&gt;

&lt;p&gt;👉 &lt;em&gt;Learn about Vue 3 and TypeScript with my new book &lt;a href="https://www.oreilly.com/library/view/learning-vue/9781492098812/" rel="noopener noreferrer"&gt;Learning Vue&lt;/a&gt;!&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;👉 &lt;em&gt;Follow me on &lt;a href="https://x.com/MayaShavin" rel="noopener noreferrer"&gt;X&lt;/a&gt; | &lt;a href="https://www.linkedin.com/in/mayashavin" rel="noopener noreferrer"&gt;LinkedIn&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Like this post or find it helpful? Share it 👇🏼 or &lt;a href="https://www.buymeacoffee.com/VTLRKH6" rel="noopener noreferrer"&gt;Buy me a coffee&lt;/a&gt;&lt;/p&gt;

</description>
      <category>testing</category>
      <category>vue</category>
      <category>playwrightjs</category>
      <category>vitest</category>
    </item>
    <item>
      <title>Seamless Contact Form experience with Netlify Form in Nuxt 3</title>
      <dc:creator>Maya Shavin 🌷☕️🏡</dc:creator>
      <pubDate>Wed, 07 Aug 2024 09:04:08 +0000</pubDate>
      <link>https://dev.to/mayashavin/seamless-contact-form-experience-with-netlify-form-in-nuxt-3-3amn</link>
      <guid>https://dev.to/mayashavin/seamless-contact-form-experience-with-netlify-form-in-nuxt-3-3amn</guid>
      <description>&lt;p&gt;&lt;em&gt;Contact form is an essential part of any portfolio site, where people can reach you for further queries and questions. In this article, we will explore how to use Netlify Form service to create a contact form and handle its submission from end to end in a Nuxt static site.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Prerequisites
&lt;/h2&gt;

&lt;p&gt;You need to have &lt;a href="https://app.netlify.com/signup" rel="noopener noreferrer"&gt;an active Netlify account&lt;/a&gt;, and create a Nuxt 3 application using the following command:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx nuxi@latest init your-nuxt-app
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Once done, we are ready to build our contact form, starting with enabling the Netlify Form service.&lt;/p&gt;

&lt;h2&gt;
  
  
  Enabling Netlify Form service
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://docs.netlify.com/forms/setup/" rel="noopener noreferrer"&gt;Netlify Serverless Form&lt;/a&gt; is an out-of-the-box service from Netlify that allows us to manage forms in our static applications at ease, without extra API calls or server setup. &lt;/p&gt;

&lt;p&gt;In the Netlify dashboard, we navigate to our target site project, select the &lt;strong&gt;Form&lt;/strong&gt; section on the left sidebar, and scroll down to the &lt;strong&gt;Form Detection&lt;/strong&gt; section. Here, we can enable the form detection by clicking the &lt;strong&gt;Enable Form detection&lt;/strong&gt; button.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fres.cloudinary.com%2Fmayashavin%2Fimage%2Fupload%2Fq_auto%2Cf_auto%2Fv1722972945%2Farticles%2FNuxt%2Fnetlify_form_not_enabled" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fres.cloudinary.com%2Fmayashavin%2Fimage%2Fupload%2Fq_auto%2Cf_auto%2Fv1722972945%2Farticles%2FNuxt%2Fnetlify_form_not_enabled" alt="Netlify Form Detection in disabled mode"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Once enabled, Netlify is ready to detect and manage our target HTML form during the deployment process. But to fullly use this feature, we need to create the form with the necessary Netlify attributes, which we will do next.&lt;/p&gt;

&lt;h2&gt;
  
  
  Creating a contact form
&lt;/h2&gt;

&lt;p&gt;In our Nuxt application, let's create a &lt;code&gt;contact.vue&lt;/code&gt; page, located in &lt;code&gt;pages/&lt;/code&gt; directory. This page contains an HTML &lt;code&gt;form&lt;/code&gt; with the following attributes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;netlify&lt;/code&gt; or &lt;code&gt;data-netlify="true"&lt;/code&gt; - Netlify detects the target form using this attribute.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;data-netlify-honeypot&lt;/code&gt; - Netlify uses this attribute to locate a hidden "honeypot" field for spam protection.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;name&lt;/code&gt; - unique name of the form to appear in the Netlify Form panel.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;method&lt;/code&gt; - HTTP method to use when submitting the form.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;id&lt;/code&gt; - The unique identifier for the form.
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;form&lt;/span&gt; 
  &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"contact-form"&lt;/span&gt; 
  &lt;span class="na"&gt;method=&lt;/span&gt;&lt;span class="s"&gt;"POST"&lt;/span&gt;
  &lt;span class="na"&gt;netlify&lt;/span&gt;
  &lt;span class="na"&gt;data-netlify=&lt;/span&gt;&lt;span class="s"&gt;"true"&lt;/span&gt; 
  &lt;span class="na"&gt;data-netlify-honeypot=&lt;/span&gt;&lt;span class="s"&gt;"bot-field"&lt;/span&gt; 
  &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"contact-form"&lt;/span&gt;
&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="c"&gt;&amp;lt;!-- form fields --&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/form&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For the spam protection, we add a hidden &lt;code&gt;input&lt;/code&gt; as the "honeybot" field, which is visible to bots but not for real users. If the field is filled, Netlify will reject the submission. Its &lt;code&gt;name&lt;/code&gt; should be identical with the value of &lt;code&gt;data-netlify-honeypot&lt;/code&gt;, as shown below:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"hidden"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;label&amp;gt;&lt;/span&gt;
    Don’t fill this out if you’re human: &lt;span class="nt"&gt;&amp;lt;input&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"bot-field"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/label&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And since we are working with static Nuxt site (SSR pre-rendering mode), we need to add an additional hidden input field with the name &lt;code&gt;form-name&lt;/code&gt; and the &lt;code&gt;value&lt;/code&gt; of the form's name, as follows:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;input&lt;/span&gt; &lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"hidden"&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"form-name"&lt;/span&gt; &lt;span class="na"&gt;value=&lt;/span&gt;&lt;span class="s"&gt;"contact-form"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Next, we add the actual form fields for the user to fill in:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;div&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;label&lt;/span&gt; &lt;span class="na"&gt;for=&lt;/span&gt;&lt;span class="s"&gt;"name"&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"label"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    Your Name
  &lt;span class="nt"&gt;&amp;lt;/label&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;input&lt;/span&gt; &lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"text"&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"name"&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"name"&lt;/span&gt; 
    &lt;span class="na"&gt;placeholder=&lt;/span&gt;&lt;span class="s"&gt;"What is your name?"&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"field"&lt;/span&gt; &lt;span class="na"&gt;required&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;div&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;label&lt;/span&gt; &lt;span class="na"&gt;for=&lt;/span&gt;&lt;span class="s"&gt;"email"&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"label"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    Your Email
  &lt;span class="nt"&gt;&amp;lt;/label&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;input&lt;/span&gt; &lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"email"&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"email"&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"email"&lt;/span&gt; 
    &lt;span class="na"&gt;placeholder=&lt;/span&gt;&lt;span class="s"&gt;"What is your email?"&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"field"&lt;/span&gt; &lt;span class="na"&gt;required&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;div&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;label&lt;/span&gt; &lt;span class="na"&gt;for=&lt;/span&gt;&lt;span class="s"&gt;"message"&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"label"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    Your Message
  &lt;span class="nt"&gt;&amp;lt;/label&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;textarea&lt;/span&gt; &lt;span class="na"&gt;rows=&lt;/span&gt;&lt;span class="s"&gt;"4"&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"message"&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"message"&lt;/span&gt; 
    &lt;span class="na"&gt;placeholder=&lt;/span&gt;&lt;span class="s"&gt;"What do you want to talk about?"&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"field"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And some &lt;code&gt;button&lt;/code&gt; for submitting and reseting the form when needed:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;div&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;button&lt;/span&gt; &lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"submit"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Send message&lt;span class="nt"&gt;&amp;lt;/button&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;button&lt;/span&gt; &lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"reset"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Clear&lt;span class="nt"&gt;&amp;lt;/button&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;At this point, we have created a simple contact form with the necessary attributes for Netlify Form to detect. Upon a successful form submission, Netlify will redirect the user to a default form's success page. We can override this behavior by giving a custom success page URL with the &lt;code&gt;action&lt;/code&gt; attribute in the &lt;code&gt;form&lt;/code&gt; element.&lt;/p&gt;

&lt;p&gt;Or, we can stop the redirecting mechanism and handle the form submission programatically, which we will do next.&lt;/p&gt;

&lt;h2&gt;
  
  
  Handling form submission with JavaScript
&lt;/h2&gt;

&lt;p&gt;In our page's &lt;code&gt;script&lt;/code&gt; section, we define a &lt;code&gt;handleSubmit&lt;/code&gt; method to handle the form submission. We use the &lt;code&gt;fetch&lt;/code&gt; API to send the form data as a URL-encoded string using the &lt;code&gt;FormData&lt;/code&gt; API, with the content type of &lt;code&gt;"application/x-www-form-urlencoded"&lt;/code&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;handleSubmit&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;form&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;target&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;formData&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;FormData&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;form&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;try&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;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;POST&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Content-Type&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;application/x-www-form-urlencoded&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="na"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;URLSearchParams&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;formData&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toString&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;To manage the form submission's status, we define the &lt;code&gt;FormState&lt;/code&gt; enum and &lt;code&gt;formState&lt;/code&gt; variable as follows:&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;FormState&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;IDLE&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;IDLE&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;PENDING&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;PENDING&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;SUCCESS&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;SUCCESS&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;ERROR&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;ERROR&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="kd"&gt;const&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;contactFormState&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;ref&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kr"&gt;keyof&lt;/span&gt; &lt;span class="k"&gt;typeof&lt;/span&gt; &lt;span class="nx"&gt;FormState&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;FormState&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;IDLE&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And in the &lt;code&gt;handleSubmit&lt;/code&gt; method, we update the state accordingly:&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;handleSubmit&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;any&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;contactFormState&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;FormState&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;PENDING&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="cm"&gt;/**... */&lt;/span&gt;
  &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="cm"&gt;/**... */&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;contactFormState&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;FormState&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;SUCCESS&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;contactFormState&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;FormState&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ERROR&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="cm"&gt;/**... */&lt;/span&gt;
    &lt;span class="nx"&gt;contactFormState&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;FormState&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ERROR&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We can also reset the form's data once the form is submitted successfully, as shown below:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="cm"&gt;/** ... */&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;contactFormState&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;FormState&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;SUCCESS&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nx"&gt;form&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;reset&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And we reset the form state back to &lt;code&gt;IDLE&lt;/code&gt; after a few seconds for re-submission:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="cm"&gt;/**... */&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="cm"&gt;/**... */&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;finally&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;setTimeout&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;contactFormState&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;FormState&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;IDLE&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="mi"&gt;5000&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;We then bind the form submission event &lt;code&gt;submit&lt;/code&gt; to &lt;code&gt;handleSubmit&lt;/code&gt;, with the modifier &lt;code&gt;.prevent&lt;/code&gt; for &lt;code&gt;preventDefault()&lt;/code&gt;, as follows:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;form&lt;/span&gt;
  &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"contact-form"&lt;/span&gt;
  &lt;span class="err"&gt;@&lt;/span&gt;&lt;span class="na"&gt;submit.prevent=&lt;/span&gt;&lt;span class="s"&gt;"handleSubmit"&lt;/span&gt;
&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Finally, we display a custom message to the user based on the form's state as below:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="c"&gt;&amp;lt;!--contact.vue--&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;p&lt;/span&gt; &lt;span class="na"&gt;v-if=&lt;/span&gt;&lt;span class="s"&gt;"formState === FormState.SUCCESS"&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"text-green-400 text-center"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  Form has been submitted successfully!
&lt;span class="nt"&gt;&amp;lt;/p&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;p&lt;/span&gt; &lt;span class="na"&gt;v-else-if=&lt;/span&gt;&lt;span class="s"&gt;"formState === FormState.ERROR"&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"text-red-500 text-center"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  Form submission failed. Please try again.
&lt;span class="nt"&gt;&amp;lt;/p&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;At this point, our contact page will look similar to the following upon a successful submission:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fres.cloudinary.com%2Fmayashavin%2Fimage%2Fupload%2Fq_auto%2Cf_auto%2Fv1722972945%2Farticles%2FNuxt%2Fsuccessful_submission_1" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fres.cloudinary.com%2Fmayashavin%2Fimage%2Fupload%2Fq_auto%2Cf_auto%2Fv1722972945%2Farticles%2FNuxt%2Fsuccessful_submission_1" alt="Contact form after submission"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;With that, our app is ready for deployment. However, when we use Nuxt 3 in SSR pre-rendering mode (or static site generating mode), Netlify is sometimes unable to detect and parse the form properly during the build process. We need an additional workaround to make sure that Netlify detects the form.&lt;/p&gt;

&lt;p&gt;Let's do it in the next section.&lt;/p&gt;

&lt;h2&gt;
  
  
  Making sure Netlify detects the form on deployment
&lt;/h2&gt;

&lt;p&gt;To do so, in the &lt;code&gt;public&lt;/code&gt; directory, we create an HTML file - &lt;code&gt;contact-duplicate.html&lt;/code&gt; - with the form's main HTML code, including all the form fields, and the required attributes for Netlify to use:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="cp"&gt;&amp;lt;!DOCTYPE html&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;html&lt;/span&gt; &lt;span class="na"&gt;lang=&lt;/span&gt;&lt;span class="s"&gt;"en"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;head&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;meta&lt;/span&gt; &lt;span class="na"&gt;charset=&lt;/span&gt;&lt;span class="s"&gt;"UTF-8"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;meta&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"viewport"&lt;/span&gt; &lt;span class="na"&gt;content=&lt;/span&gt;&lt;span class="s"&gt;"width=device-width, initial-scale=1.0"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;title&amp;gt;&lt;/span&gt;Contact Form&lt;span class="nt"&gt;&amp;lt;/title&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/head&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;body&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;form&lt;/span&gt; 
    &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"contact-form"&lt;/span&gt; 
    &lt;span class="na"&gt;method=&lt;/span&gt;&lt;span class="s"&gt;"POST"&lt;/span&gt;
    &lt;span class="na"&gt;netlify&lt;/span&gt;
    &lt;span class="na"&gt;data-netlify=&lt;/span&gt;&lt;span class="s"&gt;"true"&lt;/span&gt; 
    &lt;span class="na"&gt;data-netlify-honeypot=&lt;/span&gt;&lt;span class="s"&gt;"bot-field"&lt;/span&gt; 
    &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"contact-form"&lt;/span&gt;
    &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"hidden"&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="c"&gt;&amp;lt;!--form fields &amp;amp; buttons--&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/form&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/body&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/html&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's it! Nuxt will automatically include this file in the build process, allowing Netlify to detect the form and perform the necessary actions during deployment. Now, when we deploy our site to Netlify for the first time, it will detect the form and add it to the system automatically.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fres.cloudinary.com%2Fmayashavin%2Fimage%2Fupload%2Fq_auto%2Cf_auto%2Fv1722972945%2Farticles%2FNuxt%2Factiveforms_netlify" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fres.cloudinary.com%2Fmayashavin%2Fimage%2Fupload%2Fq_auto%2Cf_auto%2Fv1722972945%2Farticles%2FNuxt%2Factiveforms_netlify" alt="Netlify Active forms in the dashboard"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;p&gt;In this post, we learned how to create and manage a contact form submission using Netlify Form in Nuxt SSR pre-rendering mode. We also learned how to add basic spam protection to the form, handle the form submission programmatically using JavaScript, and display a custom message to the user based on the form's state without redirecting the user.&lt;/p&gt;

&lt;p&gt;What's next? How about adding more validations or integrate a third-party service like &lt;a href="https://vee-validate.logaretm.com/v4/" rel="noopener noreferrer"&gt;VeeValidate&lt;/a&gt; for better user experience?&lt;/p&gt;

&lt;p&gt;👉 &lt;em&gt;Learn about Vue 3 and TypeScript with my new book &lt;a href="https://www.oreilly.com/library/view/learning-vue/9781492098812/" rel="noopener noreferrer"&gt;Learning Vue&lt;/a&gt;!&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;👉 &lt;em&gt;If you'd like to catch up with me sometimes, follow me on &lt;a href="https://x.com/MayaShavin" rel="noopener noreferrer"&gt;X&lt;/a&gt; | &lt;a href="https://www.linkedin.com/in/mayashavin" rel="noopener noreferrer"&gt;LinkedIn&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Like this post or find it helpful? Share it 👇🏼 😉&lt;/p&gt;

</description>
      <category>nuxt</category>
      <category>netlify</category>
      <category>vue</category>
      <category>tutorial</category>
    </item>
  </channel>
</rss>
