Skip to main content

Visual Studio Code as a Joomla Code Editor (updated 2026)

A person coding on a laptop
Share this

Why VS Code for Joomla?

Joomla development moves between several languages in a single extension. You write PHP for models, controllers and services, and XML for manifests and forms. Layouts mix PHP with HTML, language files are INI, and the web assets need JavaScript, SCSS and joomla.asset.json.

VS Code handles all of these in one window, and it's free, fast and cross-platform. With the right extensions it reaches most of what a paid PHP IDE offers, while staying lighter and far more customizable.

1. The Essential Extension Stack

Install these, then add more only when you need them.

Purpose

Extension

ID

PHP intelligence

PHP Intelephense

bmewburn.vscode-intelephense-client

Step debugging

PHP Debug (Xdebug)

xdebug.php-debug

Code style

PHP CS Fixer

junstyle.php-cs-fixer

Static analysis

PHPStan

SanderRonde.phpstan-vscode

XML manifests and forms

XML (Red Hat)

redhat.vscode-xml

Inline errors

Error Lens

usernamehw.errorlens

Git history and blame

GitLens

eamodio.gitlens

Consistent formatting

EditorConfig

EditorConfig.EditorConfig

Remote servers

Remote - SSH

ms-vscode-remote.remote-ssh

Containers

Dev Containers

ms-vscode-remote.remote-containers

You don't need extensions for Emmet, SCSS or Less. VS Code supports them out of the box, as Sections 6 and 7 show.

Tip: Disable VS Code's built-in PHP language features. Search for @builtin php in the Extensions panel and disable "PHP Language Features". Otherwise you get duplicate suggestions next to Intelephense's.

2. Make VS Code Understand Joomla

This is the step most people skip. Out of the box, Intelephense knows PHP but not Joomla. It can't autocomplete Factory::getApplication()-> or resolve Joomla\CMS\MVC\Model\ListModel unless it can see the Joomla source.

The fix is to point Intelephense at a Joomla installation, even while your extension lives in its own folder:

  
  
    // .vscode/settings.json
{
  "intelephense.environment.phpVersion": "8.2.0",
  "intelephense.environment.includePaths": [
    "/path/to/joomla-5-site/libraries",
    "/path/to/joomla-5-site/administrator/components"
  ],
  "intelephense.files.exclude": [
    "**/node_modules/**",
    "**/media/vendor/**",
    "**/cache/**",
    "**/tmp/**"
  ]
}

After this you get:

  • Autocomplete across the entire Joomla API, including Text::_(), HTMLHelper, DatabaseInterface and CMSApplication.
  • Go to Definition (F12) straight into core classes. It's the fastest way to learn how Joomla really works.
  • Hover documentation from core docblocks.
  • Namespace auto-import when you type a class name.

Excluding media/vendor, cache and tmp keeps indexing fast and stops false duplicates.

3. Real Step Debugging with Xdebug

var_dump(); die; is not a debugging strategy. With Xdebug 3 you can pause a Joomla request mid-flight, inspect $this->input, walk the call stack and watch variables change.

php.ini:

zend_extension=xdebug
xdebug.mode=debug
xdebug.start_with_request=trigger
xdebug.client_port=9003

.vscode/launch.json:

{
  "version": "0.2.0",
  "configurations": [
    {
      "name": "Listen for Xdebug (Joomla)",
      "type": "php",
      "request": "launch",
      "port": 9003,
      "pathMappings": {
        "/var/www/html": "${workspaceFolder}"
      }
    }
  ]
}

Set a breakpoint in your component's DisplayController, press F5, and load the page with an Xdebug browser helper enabled (or add ?XDEBUG_TRIGGER=1). VS Code pauses on your line.

Xdebug paused inside a Joomla controller

Xdebug paused inside a Joomla controller: inspect $viewName and $id, trace the call stack back to index.php, then step over line by line.

A few techniques are worth knowing:

  • Conditional breakpoints. Right-click a breakpoint and set $id === 42, so it only stops on the record that's misbehaving.
  • Logpoints. Print {$query} to the debug console without touching the code. They're perfect for inspecting generated SQL.
  • Debugging a plugin event. Put a breakpoint inside onContentPrepare and watch exactly which articles trigger it, and with what context.

pathMappings matters when your site runs in Docker or DDEV. It maps the server's paths to your local folders.

4. Enforce Joomla Coding Standards Automatically

Joomla 4 and later follow PSR-12, and the core repository ships its own .php-cs-fixer.dist.php. Copy it into your extension project and let the editor do the formatting:

  // .vscode/settings.json
{
  "[php]": {
    "editor.defaultFormatter": "junstyle.php-cs-fixer",
    "editor.formatOnSave": true
  },
  "php-cs-fixer.config": ".php-cs-fixer.dist.php"
}

Add PHPStan at level 5 or higher to catch real bugs before users do: null method calls, wrong return types and undefined properties. Error Lens then shows those problems inline, right on the offending line.

5. User Snippets: Your Personal Joomla Boilerplate Library

Joomla development repeats the same patterns constantly: the _JEXEC guard, language strings, database queries, form fields and manifest blocks. VS Code's User Snippets turn each of these into a few keystrokes, with tab stops, placeholders and even logic based on the file name.

Where snippets live

Open File → Preferences → Configure Snippets (on macOS, Code → Settings → Configure Snippets). There are three scopes to choose from:

Scope

File

Best for

Language

php.json, xml.json, ini.json

Personal snippets for one language, available in every project

Global

joomla.code-snippets (with a scope key)

Your cross-project Joomla library

Project

.vscode/*.code-snippets

Extension-specific snippets, committed to Git and shared with your team

Keep your general Joomla snippets global. Put component-specific ones, such as your own language prefix or table names, in the project's .vscode folder.

Snippet syntax essentials

Syntax

Meaning

$1, $2

Tab stops, in order

$0

Final cursor position

${1:default}

Placeholder with default text

${1|site,administrator|}

Drop-down list of choices

$TM_FILENAME_BASE

Current file name without the extension

$WORKSPACE_NAME

Name of the opened folder, e.g. com_events

$TM_SELECTED_TEXT

Text selected before inserting (wrap-with snippets)

$CURRENT_YEAR

For copyright headers

${VAR/regex/format/}

Transforms, e.g. upper-casing

Smart Joomla snippets

The following joomla.code-snippets file puts these features to work:

  {
  "Joomla: File header": {
    "scope": "php",
    "prefix": "jhead",
    "body": [
      "<?php",
      "",
      "/**",
      " * @package     ${1:${WORKSPACE_NAME}}",
      " * @copyright   (C) ${CURRENT_YEAR} ${2:Your Company}. All rights reserved.",
      " * @license     GNU General Public License version 2 or later",
      " */",
      "",
      "\\defined('_JEXEC') or die;",
      "",
      "$0"
    ]
  },

  "Joomla: Language string (auto prefix)": {
    "scope": "php",
    "prefix": "jt",
    "body": ["Text::_('${WORKSPACE_NAME/(.*)/${1:/upcase}/}_${1:KEY}')"],
    "description": "In workspace com_events, produces COM_EVENTS_KEY"
  },

  "Joomla: Escaped output": {
    "scope": "php",
    "prefix": "jesc",
    "body": ["<?php echo \\$this->escape(\\$${1:item}->${2:title}); ?>"]
  },

  "Joomla: Controller class from filename": {
    "scope": "php",
    "prefix": "jcontroller",
    "body": [
      "namespace ${1:Vendor}\\\\Component\\\\${2:Name}\\\\${3|Site,Administrator|}\\\\Controller;",
      "",
      "\\defined('_JEXEC') or die;",
      "",
      "use Joomla\\\\CMS\\\\MVC\\\\Controller\\\\${4|BaseController,FormController,AdminController|};",
      "",
      "class ${TM_FILENAME_BASE} extends ${4}",
      "{",
      "    $0",
      "}"
    ]
  },

  "Joomla: Form field (XML)": {
    "scope": "xml",
    "prefix": "jfield",
    "body": [
      "<field",
      "    name=\"${1:title}\"",
      "    type=\"${2|text,textarea,list,radio,editor,media,calendar,sql,user,category|}\"",
      "    label=\"${WORKSPACE_NAME/(.*)/${1:/upcase}/}_FIELD_${1/(.*)/${1:/upcase}/}_LABEL\"",
      "    ${3:required=\"true\"}",
      "/>"
    ]
  },

  "Joomla: Language line (INI)": {
    "scope": "ini",
    "prefix": "jlang",
    "body": ["${WORKSPACE_NAME/(.*)/${1:/upcase}/}_${1:KEY}=\"${2:Text}\""]
  }
}

Several ideas in this file are worth pointing out:

  • The class name comes from the file name. Create EventController.php, type jcontroller, and the class is already called EventController.
  • The language prefix comes from the folder. When you open the com_events folder, every jt, jfield and jlang snippet produces COM_EVENTS_... automatically.
  • Choice lists encode Joomla knowledge. The field-type drop-down doubles as a reminder of the core field types.
  • The field label follows Joomla's naming convention. Typing publish_up as the field name gives COM_EVENTS_FIELD_PUBLISH_UP_LABEL with no manual typing.

$this->escape() only exists inside a component view's layout. In a module layout, use htmlspecialchars($item->title, ENT_QUOTES, 'UTF-8') instead.

Power-user tricks

Wrap selected text in a snippet. Select some existing text, run Snippets: Insert Snippet, and choose a snippet that uses $TM_SELECTED_TEXT:

"Joomla: Wrap in Text::_()": {
  "scope": "php",
  "prefix": "jwrap",
  "body": ["Text::_('${TM_SELECTED_TEXT}')"]
}

Bind a snippet to a keyboard shortcut in keybindings.json:

{
  "key": "ctrl+alt+j",
  "command": "editor.action.insertSnippet",
  "when": "editorTextFocus && editorLangId == php",
  "args": { "name": "Joomla: Escaped output" }
}

Use snippets as file templates. Add "isFileTemplate": true to a snippet. When you create an empty file, the Snippets: Fill File with Snippet command offers it, so a new layout or controller starts complete.

Show snippets first in suggestions:

{
  "editor.snippetSuggestions": "top",
  "editor.tabCompletion": "on"
}

6. Emmet: Built-In HTML and CSS Shorthand

VS Code includes Emmet out of the box, with nothing to install. You type a CSS-selector-style abbreviation, press Tab, and it expands into full markup. For Joomla layouts and template overrides, which are mostly Bootstrap 5 HTML with bits of PHP, it's a large time-saver.

Enable it for PHP layout files

Emmet is active by default in HTML, CSS, SCSS, Less and XML. Joomla layouts are .php files, so tell Emmet to treat them as HTML:

{
  "emmet.includeLanguages": { "php": "html" },
  "emmet.triggerExpansionOnTab": true
}

Abbreviation syntax in 30 seconds

Syntax

Meaning

Example

>

Child

div>p

+

Sibling

h3+p

^

Climb up one level

div>p^span

*

Repeat

li*5

$

Numbering

li.item-$*3

. / #

Class / id

div.card#event

[]

Attributes

a[href=#][title=More]

{}

Text content

button{Save}

()

Grouping

(dt+dd)*3

When you leave out the tag name, Emmet assumes div, so .card-body becomes <div class="card-body">.

A Joomla example

This abbreviation builds a Bootstrap 5 card:

article.card>.card-body>h3.card-title+p.card-text+a.btn.btn-primary{Read more}

Press Tab and it expands to:

<article class="card">
    <div class="card-body">
        <h3 class="card-title"></h3>
        <p class="card-text"></p>
        <a href="/" class="btn btn-primary">Read more</a>
    </div>
</article>

Next, drop in the jesc snippet from Section 5 for each dynamic value, and the layout is done. Emmet builds the structure, and your snippets fill in the Joomla-specific PHP.

One Emmet abbreviation plus one user snippet: a complete, safely escaped Joomla card layout in seconds.

One Emmet abbreviation plus one user snippet: a complete, safely escaped Joomla card layout in seconds.

A few more abbreviations cover common Joomla layouts:

  • Responsive grid: div.row.g-4>div.col-md-6.col-lg-4*3>div.card
  • Admin list table head: table.table.table-striped>thead>tr>th[scope=col]*5
  • Definition list for event details: dl.row>(dt.col-sm-3+dd.col-sm-9)*4
  • Placeholder text for mockups: p>lorem20

Emmet in stylesheets

Emmet also works in CSS, SCSS and Less:

Abbreviation

Result

m10

margin: 10px;

p10-20

padding: 10px 20px;

d:f

display: flex;

pos:r

position: relative;

w100p

width: 100%;

bd1-s#ccc

border: 1px solid #ccc;

The Emmet commands most people never find

Open the Command Palette (Ctrl+Shift+P) and type "Emmet" to see these:

  • Wrap with Abbreviation. Select existing markup and wrap it in div.container>div.row, for example. It's ideal when restructuring a core layout you've copied into an override.
  • Balance (outward / inward). Selects the enclosing tag, then the next one out. It's the fastest way to select a whole <div> block in a long layout file.
  • Go to Matching Pair. Jumps between opening and closing tags.
  • Remove Tag. Removes a wrapper but keeps its contents.
  • Update Tag. Renames the opening and closing tags together.
  • Increment / Decrement Number. Nudges mb-3 to mb-4, or 12px to 13px.
  • Evaluate Math Expression. Turns 1200/3 into 400 inline.

Tip: Assign Wrap with Abbreviation and Balance Outward to keyboard shortcuts. They become second nature within a week.

7. LESS and Sass/SCSS Support

VS Code understands SCSS and Less natively, with no extension needed. This matters for Joomla because the platform's styling has changed over the years:

  • Joomla 4, 5 and 6 use SCSS throughout, including Cassiopeia, the Atum admin template and Bootstrap 5.
  • Joomla 3 templates, such as Protostar and many third-party templates, used Less. You'll still meet them on legacy sites and during migrations.

What you get out of the box

Feature

SCSS

Less

Syntax highlighting

✔

✔

IntelliSense for properties and values

✔

✔

Syntax validation and linting

✔

✔

Go to Definition for variables and mixins

✔

✔

Hover previews, including selector specificity

✔

✔

Inline colour swatches and colour picker

✔

✔

Outline and breadcrumb navigation

✔

✔

Folding, including // #region markers

✔

✔

Emmet expansion

✔

✔

Hovering a selector such as .blog-item .page-header h2 shows its specificity. That explains immediately why your override isn't beating Cassiopeia's rule.

The indented .sass syntax (without braces) is not built in. For that, install the Sass extension (Syler.sass). Most Joomla work uses .scss, so you probably won't need it.

VS Code edits but doesn't compile

The editor understands your SCSS but doesn't turn it into CSS. You have three good options.

Option A: a Dart Sass watch task. This is the recommended option. Put the following in .vscode/tasks.json:

{
  "label": "Sass: watch template",
  "type": "shell",
  "command": "npx sass scss:css --watch --style=compressed --source-map",
  "options": { "cwd": "${workspaceFolder}/media/templates/site/cassiopeia_child" },
  "isBackground": true,
  "problemMatcher": []
}

Run it once. From then on, every save recompiles the CSS, and the source maps make browser DevTools point back to your .scss line numbers.

Option B: an extension such as Live Sass Compiler (glenn2223.live-sass). It compiles on save with a click in the status bar, which is convenient for quick template tweaks.

Option C: Joomla core's own build. When you work on joomla-cms itself, run npm ci and then use the repository's npm build scripts, which compile all core SCSS.

For legacy Less templates, run npx lessc less/template.less css/template.css --source-map from a similar task.

A practical workflow: a Cassiopeia child template

  1. In the Joomla administrator, go to System → Site Templates → Cassiopeia → Create Child Template.
  2. Open the child's media folder in VS Code.
  3. Override Bootstrap and Cassiopeia variables in your own SCSS. Colour swatches appear inline, and clicking one opens the colour picker.
  4. Start the Sass watch task, save, and refresh the browser.
  5. Register the compiled CSS through the Web Asset Manager in joomla.asset.json.

The variable overrides from step 3 look like this:

// scss/_variables.scss
$primary:          #1d4ed8;
$border-radius:    .75rem;
$font-family-base: "Inter", system-ui, sans-serif;

The asset registration from step 5:

{
  "$schema": "https://developer.joomla.org/schemas/json-schema/web_assets.json",
  "name": "cassiopeia_child",
  "version": "1.0.0",
  "assets": [
    {
      "name": "template.cassiopeia_child.custom",
      "type": "style",
      "uri": "custom.min.css"
    }
  ]
}

Thanks to the $schema line, VS Code validates the file and autocompletes asset keys as you type. Many developers don't realise the built-in JSON support does this.

{
  "scss.lint.duplicateProperties": "warning",
  "less.lint.duplicateProperties": "warning",
  "scss.lint.zeroUnits": "warning",
  "editor.colorDecorators": true,
  "[scss]": { "editor.formatOnSave": true },
  "[less]": { "editor.formatOnSave": true }
}

8. Build Your Extension ZIP with One Keystroke

Joomla installs extensions from ZIP files, so automate the packaging with a VS Code task:

// .vscode/tasks.json
{
  "version": "2.0.0",
  "tasks": [
    {
      "label": "Build Joomla package",
      "type": "shell",
      "command": "mkdir -p dist && cd src && zip -r ../dist/${workspaceFolderBasename}.zip . -x '*.DS_Store' -x '*/node_modules/*'",
      "group": { "kind": "build", "isDefault": true },
      "problemMatcher": []
    }
  ]
}

Press Ctrl+Shift+B (Cmd+Shift+B on macOS) and your installable package appears in dist/. You can extend the task to run PHP CS Fixer, compile SCSS or bump the manifest version before zipping.

For faster iteration, symlink your source folders into a local Joomla site. You edit in one place and refresh the browser to see the change, with no reinstall needed.

9. Work Directly on Servers and Containers

Remote - SSH opens a folder on a staging server as if it were local. You get full IntelliSense, a terminal and search, and nothing is copied back and forth. It's the modern replacement for editing over FTP, and it's far safer.

Dev Containers and DDEV let you define the PHP version, database and Xdebug in a config file. Every developer, and every client project, gets an identical environment. That's valuable when one site runs PHP 8.1 and another runs 8.3.

10. Multi-Root Workspaces for Large Projects

A real client project might combine a custom component, a system plugin and a set of template overrides. Save them as one .code-workspace file:

{
  "folders": [
    { "path": "com_events" },
    { "path": "plg_system_eventsync" },
    { "path": "tpl_client_overrides" }
  ],
  "settings": {}
}

One window then gives you one search across everything, and one Git view per repository.

11. AI-Assisted Joomla Development

VS Code is now also a front end for AI coding assistants, such as GitHub Copilot or Claude Code running in the integrated terminal. VS Code also supports the Model Context Protocol (MCP), which lets an assistant connect to external tools. When an assistant can reach a Joomla site through an MCP server, it can check the actual installed extensions, articles or configuration instead of guessing.

Here are useful ways to put this to work:

  • Generate a full MVC skeleton (service provider, dispatcher, controllers, views, forms, manifest), then review it yourself.
  • Ask it to explain a core class you've jumped into with F12.
  • Convert a Joomla 3 extension's legacy JFactory or JModelLegacy calls to their namespaced Joomla 4/5 equivalents.
  • Draft language INI files from the strings used in your layouts.

The golden rule still applies: AI writes the first draft, and the Joomla developer owns the review.

12. Quality-of-Life Settings Worth Stealing

{
  "files.associations": {
    "*.ini": "ini",
    "joomla.asset.json": "jsonc"
  },
  "editor.rulers": [120],
  "files.trimTrailingWhitespace": true,
  "files.insertFinalNewline": true,
  "search.exclude": {
    "**/cache": true,
    "**/tmp": true,
    "**/administrator/logs": true,
    "**/media/vendor": true
  }
}

A few keyboard shortcuts pay off every day:

Shortcut

What it does

Ctrl+P

Jump to any file, e.g. tmpl/default.php

Ctrl+Shift+O

Jump to a method in the current class

F12 / Alt+F12

Go to or peek at the definition of a Joomla core method

Ctrl+Shift+F

Search the whole project for a language constant

F2

Rename a symbol everywhere it's used

Ctrl+Shift+B

Run the default build task (your extension ZIP)

On macOS, use Cmd instead of Ctrl.

Conclusion

VS Code becomes a genuine Joomla IDE once it knows where Joomla lives. Point Intelephense at the core, wire up Xdebug, and let PHP CS Fixer and PHPStan guard quality. Then add snippets and Emmet for writing speed, compile SCSS on save, and automate packaging with tasks. That setup takes about an hour and saves hours on every extension you build.

Next step: commit your .vscode/ folder (settings, launch, tasks and snippets) to your extension's repository, so every project starts fully configured.

_Support