DEV Community

Cover image for Create VS Code Extension with React, TypeScript, Tailwind
Rakshit Raj
Rakshit Raj

Posted on

Create VS Code Extension with React, TypeScript, Tailwind

TL;DR

This article also deals with basics of Making of VS Code Extension other than react, typescript, tailwindcss. For just get to the point directly, click here, or else we can dive into it.

Table Of Contents


Introduction

This article is dedicated for those people who prefer a code editor called VS Code, which is quite underrated and under used all along.

Inside the code editor there is a section for extensions which enables additional helping hand while coding like snippets (auto complete), debugging, theme and more such more useful extensions.

For example, The Postman VS Code extension enables you to develop and test your APIs in Postman directly from Visual Studio Code.

VS Code Postman Extension

In the above extension, the shown UI is inside VS Code which is possible due the help of extension by Postman. As it seems an integral part of code editor too, but for sure this was made using html , css and javascript only, and any one can make it easily.

To make sure yourself just press, Ctrl + Shift + P and select Developer: Toggle Developer Tools to reveal the dev tools inside VS Code.

VS Code Web

As mentioned in the above gif, one can inspect any extension easily as they inspect any web page. Now coming to the important part, how to make such extension.

Let's begin with it...


Understanding Extension Creation

For creating an extension VS Code uses a cli tool called Yeoman and Code-Generator.

Before getting started, you need npm installed and basic knowledge of node.

Installing package globally

npm install --global yo generator-code
Enter fullscreen mode Exit fullscreen mode

Initiating the extension installation

yo code
Enter fullscreen mode Exit fullscreen mode

After which you will be introduced with this Yeomen menu of extensions


     _-----_     ╭──────────────────────────╮
    |       |       Welcome to the Visual  
    |--(o)--|       Studio Code Extension  
   `---------´           generator!        
    ( U`_ )    ╰──────────────────────────╯
    /___A___\   /
     |  ~  |     
   __'.___.'__   
 ´   `  |° ´ Y ` 

? What type of extension do you want to create? (Use arrow keys)
> New Extension (TypeScript) 
  New Extension (JavaScript)
  New Color Theme
  New Language Support
  New Code Snippets
  New Keymap
  New Extension Pack
  New Language Pack (Localization)
  New Web Extension (TypeScript)
  New Notebook Renderer (TypeScript)
Enter fullscreen mode Exit fullscreen mode

Selection for different kind of extension, but in our case we are going with certain options

? What type of extension do you want to create? New Extension (TypeScript)
? Whats the name of your extension? HelloWorld
? Whats the identifier of your extension? helloworld
? Whats the description of your extension? LEAVE BLANK
? Initialize a git repository? Yes
? Bundle the source code with webpack? No
? Which package manager to use? npm

? Do you want to open the new folder with Visual Studio Code? Open with `code`
Enter fullscreen mode Exit fullscreen mode

Inside the editor, open src/extension.ts and press F5. This will compile and run the extension in a new Extension Development Host window.

Run the Hello World command from the Command Palette Ctrl + Shift + P in the new window

Output will result in a VS Code Information Message (default): Hello World

You can play around by changing stuffs inside src/extension.ts:

// The module 'vscode' contains the VS Code extensibility API
// Import the module and reference it with the alias vscode in your code below
import * as vscode from 'vscode';

// This method is called when your extension is activated
// Your extension is activated the very first time the command is executed
export function activate(context: vscode.ExtensionContext) {

    // Use the console to output diagnostic information (console.log) and errors (console.error)
    // This line of code will only be executed once when your extension is activated
    console.log('Congratulations, your extension "helloworld" is now active!');

    // The command has been defined in the package.json file
    // Now provide the implementation of the command with registerCommand
    // The commandId parameter must match the command field in package.json
    let disposable = vscode.commands.registerCommand('helloworld.helloWorld', () => {
        // The code you place here will be executed every time your command is executed
        // Display a message box to the user
        vscode.window.showInformationMessage('Hello World from Helloworld!');
    });

    context.subscriptions.push(disposable);
}

// This method is called when your extension is deactivated
export function deactivate() {}

Enter fullscreen mode Exit fullscreen mode

This file is the base of your extension. It contains 2 important methods: activate and deactivate . Imported vscode.command module registers command and context argument collectively subscribe to them.

Within the package.json

{
...,
  "activationEvents": [],
  "contributes": {
    "commands": [
      {
        "command": "helloworld.helloWorld",
        "title": "Hello World"
      }
    ]
  },
}
Enter fullscreen mode Exit fullscreen mode

Contributes.command : Helps in displaying the command with a title in the command palate also consists of views and more.

Activation Events : Automates the triggering of a registered command on a certain condition

For more clarity you can visit Your First Extension by VS Code

But, this article is not just about any normal extension. For, UI part we will use WebViews by VS Code.


Webview API for UI

The Webview allows extensions to create fully customizable views within Visual Studio Code. For quick implementation follow these steps:
Overwrite the src/extension.ts file with the code below:

import * as vscode from 'vscode';

export function activate(context: vscode.ExtensionContext) {

    console.log('Congratulations, your extension "Webview" is up and running now');

    let webview = vscode.commands.registerCommand('helloworld.webview', () => {

        let panel = vscode.window.createWebviewPanel("webview", "Web View", {
            viewColumn: vscode.ViewColumn.One,
        })

        // will set the html here
                 panel.webview.html = `<h1>This is Heading 1</h1>
    <h2>This is Heading 2</h2>
    <h3>This is Heading 3</h3>
    <h4>This is Heading 4</h4>
    <h5>This is Heading 5</h5>`

    });

    });

    context.subscriptions.push(webview);
}

export function deactivate() { }
Enter fullscreen mode Exit fullscreen mode

Key Points to be noticed:

  1. For creating a Webview Panel with the help of a command helloworld.webview, we need to register it with it's callback. panel is used for setting html into the webview.

  2. With in the callback, vscode.window.createWebviewPanel method is called which takes 4 arguments, viewType , title (title of the webview panel), showOption (which column and to focus), options (important, as it deals with security like scripting and accessibility of resources to the panel) and it returns vscode.WebviewPanel

  3. Important Step: If you ever register a new command, always remember to add it to package.json under contributes.commands

{
...,
"contributes": {
    "commands": [
      {
        "command": "helloworld.webview",
        "title": "Web View"
      }
    ]
  },
}
Enter fullscreen mode Exit fullscreen mode

and, run the extension with F5, which eventually leads to opening another vscode window Extension Development. Then press Ctrl + Shift + P, enter on Web View (or command title you mentioned inside package.json) in the command palate.

The appearing window must look like this

Web View Panel


Styling UI

Styling is same as the web using css as usual but here is a catch, if you are writing css externally, you need to first allow that resource.

By default, all the local resources within the extension location and workspace are accessible. To control them just alter the code for createWebviewPanel by providing it with 4th argument localResourceRoots.

// while creating panel allow the path you want to add
// in my case adding media as rootDir to localResources
let panel = vscode.window.createWebviewPanel("webview", "Web View", {  
       viewColumn: vscode.ViewColumn.One
},{    localResourceRoots: [vscode.Uri.joinPath(context.extensionUri, "media")]
     })
Enter fullscreen mode Exit fullscreen mode

Next step is to create a folder named media in the root directory inside the extension folder.

Inside the folder you can put all your styling files such as images, svg, css files, etc.

Craete a file vscode.css (you can name anything) inside media folder. Add the content below

:root {
  --container-paddding: 20px;
  --input-padding-vertical: 6px;
  --input-padding-horizontal: 4px;
  --input-margin-vertical: 4px;
  --input-margin-horizontal: 0;
}

html {
  box-sizing: border-box;
  font-size: 13px;
}

*,
*:before,
*:after {
  box-sizing: inherit;
}

body,
h1,
h2,
h3,
h4,
h5,
h6,
p,
ol,
ul {
  margin: 0;
  padding: 0;
  font-weight: normal;
}

img {
  max-width: 100%;
  height: auto;
}

body {
  padding: 0 var(--container-paddding);
  color: var(--vscode-foreground);
  font-size: var(--vscode-font-size);
  font-weight: var(--vscode-font-weight);
  font-family: var(--vscode-font-family);
  background-color: var(--vscode-editor-background);
}

ol,
ul {
  padding-left: var(--container-paddding);
}

body > *,
form > * {
  margin-block-start: var(--input-margin-vertical);
  margin-block-end: var(--input-margin-vertical);
}

*:focus {
  outline-color: var(--vscode-focusBorder) !important;
}

a {
  color: var(--vscode-textLink-foreground);
}

a:hover,
a:active {
  color: var(--vscode-textLink-activeForeground);
}

code {
  font-size: var(--vscode-editor-font-size);
  font-family: var(--vscode-editor-font-family);
}

button {
  border: none;
  padding: var(--input-padding-vertical) var(--input-padding-horizontal);
  width: 100%;
  text-align: center;
  outline: 1px solid transparent;
  outline-offset: 2px !important;
  color: var(--vscode-button-foreground);
  background: var(--vscode-button-background);
}

button:hover {
  cursor: pointer;
  background: var(--vscode-button-hoverBackground);
}

button:focus {
  outline-color: var(--vscode-focusBorder);
}

button.secondary {
  color: var(--vscode-button-secondaryForeground);
  background: var(--vscode-button-secondaryBackground);
}

button.secondary:hover {
  background: var(--vscode-button-secondaryHoverBackground);
}

input:not([type="checkbox"]),
textarea {
  display: block;
  width: 100%;
  border: none;
  font-family: var(--vscode-font-family);
  padding: var(--input-padding-vertical) var(--input-padding-horizontal);
  color: var(--vscode-input-foreground);
  outline-color: var(--vscode-input-border);
  background-color: var(--vscode-input-background);
}

input::placeholder,
textarea::placeholder {
  color: var(--vscode-input-placeholderForeground);
}

.container {
  display: flex;
}

.form {
  display: flex;
  flex-direction: column;
  flex: 1;
  padding: 10px;
  gap: 10px;
}

Enter fullscreen mode Exit fullscreen mode

Styling above will help you the reset the default styling property of the element of html to once looks like native styling of vscode. Variables mentioned above is directly provided by vscode.

You can also add any image of your choice, just place image file inside media folder.

For applying the css stylesheet and image, just add this lines and apply to html

const cssStyle = panel.webview.asWebviewUri(vscode.Uri.joinPath(context.extensionUri, "media", "vscode.css"))

const imgSrc = panel.webview.asWebviewUri(vscode.Uri.joinPath(context.extensionUri, "media", "vim.svg"))

panel.webview.html = `<!DOCTYPE html>
    <html lang="en">
    <head>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <link rel="stylesheet" type="text/css" href="${cssStyle}" />
    </head>
    <body>
    <div class="container">
        <img src="${imgSrc}" width="200" />
        <div class="form">
            <code>Title</code>
            <input />
            <code>Code</code>
            <textarea></textarea>
            <button>Submit</button>
        </div>
    </div>
    </body>
    </html>`
Enter fullscreen mode Exit fullscreen mode

Quick Tip: To see the changes in html you need to reload the entire extension, Ctrl + R to quick reload, no need to restart every time with Ctrl + F5

Expected Output:

webview preview


Scripting

You can also update DOM inside your extension using javascript. But it needs to be enabled before.

let panel = vscode.window.createWebviewPanel("webview", "Web View", {
            viewColumn: vscode.ViewColumn.One
        },{
        enableScripts: true
    })

const scriptPath= panel.webview.asWebviewUri(vscode.Uri.joinPath(context.extensionUri, "media", "script.js"))

panel.webview.html = `<!DOCTYPE html>
    <html lang="en">
    <head>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <script src="${scripts}"></script>
    </head>
    <body>
    <h1>Count:</h1>
        <p id="count">0</p>
    <button onclick="changeHeading()">Add</button>
    </body>
    </html>`
Enter fullscreen mode Exit fullscreen mode

Now, create file script.js inside media folder, and add following lines.

//script.js
function changeHeading() {
    document.getElementById("count").textContent = +document.getElementById("count").textContent + 1
} 
Enter fullscreen mode Exit fullscreen mode

Expected Output after running extension

webview scripting

Fun Fact: You don't need to reload to see the changes for the scripting part, just close and reopen the web view.


Implementing React, TypeScript, Tailwind

After learning much about the extension and webviews, it's time to get along with frameworks, for obvious reasons that they make things easier while using different libraries.

Starting with react, you can choose any framework of your choice.

Step 1 Initializing extension with yo code

yo code

? What type of extension do you want to create? New Extension (TypeScript)
? Whats the name of your extension? react-ext
? Whats the identifier of your extension? react-ext
? Whats the description of your extension? LEAVE BLANK
? Initialize a git repository? Yes
? Bundle the source code with webpack? No
? Which package manager to use? npm

? Do you want to open the new folder with Visual Studio Code? Open with `code`
Enter fullscreen mode Exit fullscreen mode

Step 2 Inside root directory creating a react app with typescript.

npx create-react-app web --template typescript
cd web
Enter fullscreen mode Exit fullscreen mode

Step 3 Inside react app directory web, Setting up tailwindcss

npm i -D tailwindcss postcss
npx tailwindcss init
Enter fullscreen mode Exit fullscreen mode

Editing tailwind.config.js

//tailwind.config.js
module.exports = {
  content: ['./src/**/*.{js,jsx,ts,tsx}'],
  theme: {
    extend: {},
  },
  plugins: [],
}
Enter fullscreen mode Exit fullscreen mode

Editing web/src/index.css

@tailwind base;
@tailwind components;
@tailwind utilities;

body {
  @apply p-0;
}
Enter fullscreen mode Exit fullscreen mode

Step 4 Inside React directory, creating file named .postcssrc

//.postcssrc
{
  plugins: {
    tailwindcss: { },
  },
}
Enter fullscreen mode Exit fullscreen mode

Step 5 Editing react app web/src/App.tsx

function App() {
  return (
    <div className="bg-gradient-to-r from-blue-600 to-purple-500 p-10">
      <p className="text-white/80 text-xl font-semibold">
        Lorem ipsum dolor sit amet consectetur adipisicing elit. Ea, explicabo
        doloremque deserunt, voluptates, fugiat dolorem consectetur odio autem
        quas ipsa veniam ducimus necessitatibus exercitationem numquam assumenda
        natus beatae sed velit!
      </p>
    </div>
  );
}

export default App;
Enter fullscreen mode Exit fullscreen mode

Step 6 Setup Parcel, most lightweight bundler (honestly after trying webpack and babel and rollup, i felt wastage of time)

Installing Parcel as Dev Dependency inside React Root Directory web

npm i -D parcel
Enter fullscreen mode Exit fullscreen mode

Inside React app, Editing package.json

//web/package.json
{
...,
"source": "src/index.tsx",
"scripts": {
    "start": "parcel", //overwrite 
    "build": "parcel build", //overwrite
    "test": "react-scripts test",
    "eject": "react-scripts eject"
  },
}
Enter fullscreen mode Exit fullscreen mode

Running Parcel (Parcelling)

npm start

#> web@0.1.0 start
#> parcel

#Server running at http://localhost:1234
Enter fullscreen mode Exit fullscreen mode

Every time you change something inside src or related to src/index.tsx like index.css, it will rerun the quick build and store it in dist folder inside react app directory, as index.js and index.css.

Step 7 Now coming to the extension integration part, editing src/extension.ts

import * as vscode from 'vscode';

export function activate(context: vscode.ExtensionContext) {

    let webview = vscode.commands.registerCommand('react-ext.namasteworld', () => {

        let panel = vscode.window.createWebviewPanel("webview", "React", vscode.ViewColumn.One, {
            enableScripts: true
        })

        // web is for my react root directory, rename for yours

        let scriptSrc = panel.webview.asWebviewUri(vscode.Uri.joinPath(context.extensionUri, "web", "dist", "index.js"))

        let cssSrc = panel.webview.asWebviewUri(vscode.Uri.joinPath(context.extensionUri, "web", "dist", "index.css"))

        panel.webview.html = `<!DOCTYPE html>
        <html lang="en">
          <head>
            <link rel="stylesheet" href="${cssSrc}" />
          </head>
          <body>
            <noscript>You need to enable JavaScript to run this app.</noscript>
            <div id="root"></div>
            <script src="${scriptSrc}"></script>
          </body>
        </html>
        `
    });

    context.subscriptions.push(webview);
}

export function deactivate() { }
Enter fullscreen mode Exit fullscreen mode

MOST IMPORTANT PROCESS!
Don't forget manually write all you registered command inside package.json, this will lead to absence of UI Options in the command palate Ctrl+Shift+P

To do so, go to your extension's root directory's package.json

{
...,
"contributes": {
    "commands": [
      {
        "command": "react-ext.namasteworld",
        "title": "React Web View"
      }
    ]
  },
}
Enter fullscreen mode Exit fullscreen mode

Final Step Now, for the moment of truth

  1. Just press Ctrl F5, another Extension Development will open.
  2. On another test vscode editor Extension Development, open command palate by press Ctrl+Shift+P
  3. Search for your registered command, your given the name in package.json, in my case React Web View

And similar window must pop-up as shown below

react webview

Expected Steps:

react extension steps

Fun Fact: You don't need to rerun after making changes inside your react app or App.tsx, reopen the window as shown below

react vscode hot reload


Bonus: VS Code UI Toolkit (React)

Advantages of using frameworks instead of raw html, you can use UI libraries like shadcn ui, framer motion, and many more

One such useful example is there own developed Webview UI Toolkit

It is an amazing ui library which makes your made extension feels like their native once.

To implement you just need to install vscode/webview-ui-toolkit inside your react app

npm install --save @vscode/webview-ui-toolkit 
Enter fullscreen mode Exit fullscreen mode

Editing App.tsx

import {
  VSCodeButton,
  VSCodeDataGrid,
  VSCodeDataGridRow,
  VSCodeDataGridCell,
  VSCodeTextField,
  VSCodeProgressRing,
} from "@vscode/webview-ui-toolkit/react";

function App() {
  const rowData = [
    {
      cell1: "Cell Data",
      cell2: "Cell Data",
      cell3: "Cell Data",
      cell4: "Cell Data",
    },
    {
      cell1: "Cell Data",
      cell2: "Cell Data",
      cell3: "Cell Data",
      cell4: "Cell Data",
    },
    {
      cell1: "Cell Data",
      cell2: "Cell Data",
      cell3: "Cell Data",
      cell4: "Cell Data",
    },
  ];

  return (
    <div className="grid gap-3 p-2 place-items-start">
      <VSCodeDataGrid>
        <VSCodeDataGridRow row-type="header">
          <VSCodeDataGridCell cell-type="columnheader" grid-column="1">
            A Custom Header Title
          </VSCodeDataGridCell>
          <VSCodeDataGridCell cell-type="columnheader" grid-column="2">
            Another Custom Title
          </VSCodeDataGridCell>
          <VSCodeDataGridCell cell-type="columnheader" grid-column="3">
            Title Is Custom
          </VSCodeDataGridCell>
          <VSCodeDataGridCell cell-type="columnheader" grid-column="4">
            Custom Title
          </VSCodeDataGridCell>
        </VSCodeDataGridRow>
        {rowData.map((row) => (
          <VSCodeDataGridRow>
            <VSCodeDataGridCell grid-column="1">{row.cell1}</VSCodeDataGridCell>
            <VSCodeDataGridCell grid-column="2">{row.cell2}</VSCodeDataGridCell>
            <VSCodeDataGridCell grid-column="3">{row.cell3}</VSCodeDataGridCell>
            <VSCodeDataGridCell grid-column="4">{row.cell4}</VSCodeDataGridCell>
          </VSCodeDataGridRow>
        ))}
      </VSCodeDataGrid>

      <span className="flex gap-3">
        <VSCodeProgressRing />
        <VSCodeTextField />
        <VSCodeButton>Add</VSCodeButton>
        <VSCodeButton appearance="secondary">Remove</VSCodeButton>
      </span>
    </div>
  );
}

export default App;
Enter fullscreen mode Exit fullscreen mode

The best part of this library this is, it accommodates with every available themes in vscode, using their css varibles (i guess), which is amazing.

Here is a sample

vscode ui toolkit theme changes

For more usage info look into their Repo

If you are here, i don't believe you actually reached here, btw thanks for reading.

Here is a github link to my react setup repo, you can easily clone and try for yourself too. Clone Repo

Top comments (16)

Collapse
 
fidalmathew profile image
Fidal Mathew

Awesome! Thanks for sharing!

Collapse
 
rakshit47 profile image
Rakshit Raj

Thanks for commenting, all these appreciations helps a lot.

Collapse
 
florianrappl profile image
Florian Rappl
const cssStyle = panel.webview.asWebviewUri(vscode.Uri.joinPath(context.extensionUri, "media", "vscode.css"))
Enter fullscreen mode Exit fullscreen mode

These lines (i.e., the mentioned one and lines, e.g., to reference an image) are totally unnecessary. You anyway use a bundler. Configure the bundler to inline the resources as base64 data URLs. The result will be also faster as you don't need to go over the extension bridge; it's already loaded (and don't forget - files are anyway served through the bridge).

In general I'd also recommend CSS-in-JS for VS Code extensions. It's just a much simplified / improved DX and you don't get any perf hit as JS is running anyway + it's all coming from local bridge.

Besides that - nice article!

Check out the Piral VS Code extension if you want to see how such tricks can be applied (and more): github.com/smapiot/vscode-piral

Collapse
 
rakshit47 profile image
Rakshit Raj

Actually after going through all these bundlers webpack, babel, rollup, I found parcel the fastest.

Although I don't have much depth knowledge in parcel but CSS-in-JS was only available for CSS not Tailwind. For Tailwind there was a sperate section, for sure looking forward to learning it.

Thanks for suggesting, I will keep that in mind onwards. 👨‍💻

Collapse
 
florianrappl profile image
Florian Rappl • Edited

Actually after going through all these bundlers webpack, babel, rollup, I found parcel the fastest.

Hm, not sure if you wanted to write that to my comment? I never questioned that. But now that you bring it up - it's not. If you care about performance you'd pick either Vite, rspack, or esbuild. The last one is certainly the most performant, but comes with the least DX - however, sufficiently enough for that scenario.

CSS-in-JS is quite bundler independent. After all - the only thing you need is JS (every web bundler handles JS).

Thread Thread
 
rakshit47 profile image
Rakshit Raj

Thank you sir 🫡, I'll keep that in mind

Collapse
 
anmolbaranwal profile image
Anmol Baranwal

Saving it for later. It seems detailed and will surely help in the future :D
Gonna read it in a day or two.
By the way -> you only have 1 commit. LOL!

Collapse
 
rakshit47 profile image
Rakshit Raj • Edited

Actually I followed this blog and made it in one go, that's why 1 commit #promotion 😁

Collapse
 
yogini16 profile image
yogini16

Nice one!! Thanks for sharing

Collapse
 
rakshit47 profile image
Rakshit Raj

Glad you liked 😇

Collapse
 
ricardogesteves profile image
Ricardo Esteves

Bookmarked! 👌

Collapse
 
rakshit47 profile image
Rakshit Raj

🥳

Collapse
 
anhunghezo profile image
Along

thank you for sharing!

Collapse
 
akbatra567 profile image
Akshit Batra

Great article, seems like now I need to start thinking what extension to make.

Collapse
 
rakshit47 profile image
Rakshit Raj

For sure, glad you liked it.

Collapse
 
essijunior profile image
NDANG ESSI Pierre Junior

Wow!!! Thanks alot

Some comments may only be visible to logged-in visitors. Sign in to view all comments.