How to Use Vue in WordPress (with Webpack)

Combining Vue and Webpack is very powerful. It allows you to create .vue file that contains HTML, JS, and CSS in one place.

Recommended before reading this: How to use Webpack and How to use Vue without Webpack.

Combining Vue and Webpack is very powerful. It allows you to create .vue file that contains HTML, JS, and CSS in one place.

Wait, that sounds like a bad idea! We’ve always been separating those three, why combine them now?

Continue reading to know why this is a good thing.


1. The Problem with Splitting HTML, JS, and CSS
2. The .vue File
3. Package and Webpack Setup
4. App Setup (Enqueue and Shortcode)
5. The Script

1. The Problem with Splitting HTML, JS, and CSS

Let’s say you saw this HTML button in your WordPress theme:

<button id="calculate"> Calculate </button>

Do you know what it does? No! It tells nothing.

You need to find the JS file that contains its event listener. Maybe it’s even mixed within thousand lines of other code!

The .vue file solves this problem.

2. The .vue File

  <!-- HTML -->  

// JS
export default {

  /** CSS */

That is what the file looks like: You have <template>, <script>, and <style> to hold the HTML, JS, and CSS respectively.

Everything is in one place. If you see a button, the listener is just a few lines away. If you want to change its color, simply write it under the style tag.

If you’re convinced, then let’s do the initial setup.

3. Package and Webpack Setup

This is the package.json file I’m using for most of my WordPress Vue projects:

  "name": "vue-test",
  "private": true,
  "dependencies": {
    "@babel/polyfill": "^7.11.5",
    "vue": "^2.6.12"
  "devDependencies": {
    "@babel/cli": "^7.15.7",
    "@babel/core": "^7.11.6",
    "@babel/preset-env": "^7.11.5",
    "@vue/test-utils": "^1.1.0",
    "autoprefixer": "^10.0.0",
    "babel-core": "^7.0.0-bridge.0",
    "browser-sync": "^2.27.5",
    "browser-sync-webpack-plugin": "^2.2.2",
    "css-loader": "^4.3.0",
    "babel-eslint": "^10.1.0",
    "babel-jest": "^26.3.0",
    "eslint": "^7.32.0",
    "eslint-config-airbnb-base": "^14.2.1",
    "eslint-plugin-import": "^2.24.2",
    "eslint-plugin-jest": "^24.4.0",
    "eslint-plugin-vue": "^6.2.2",
    "file-loader": "^6.1.0",
    "jest": "^26.4.2",
    "jest-transform-stub": "^2.0.0",
    "mini-css-extract-plugin": "^0.11.2",
    "node-sass": "^4.14.1",
    "postcss": "^8.3.9",
    "postcss-loader": "^4.0.2",
    "sass-loader": "^10.0.2",
    "url-loader": "^4.1.0",
    "vue-jest": "^3.0.7",
    "vue-loader": "^15.9.3",
    "vue-style-loader": "^4.1.2",
    "vue-template-compiler": "^2.6.12",
    "webpack": "^4.44.1",
    "webpack-cli": "^3.3.12"
  "scripts": {
    "build": "webpack --mode production",
    "dev": "webpack --mode development --watch",
    "test": "jest"
  "browserslist": [
    "last 2 versions"
  "babel": {
    "presets": [
  "jest": {
    "moduleFileExtensions": [
    "transform": {
      "^.+\\.vue$": "vue-jest",
      "^.+\\.js$": "babel-jest"

Then my webpack.config.js setup:

const { VueLoaderPlugin } = require('vue-loader');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const BrowserSyncPlugin = require('browser-sync-webpack-plugin');
const path = require('path');

// Change this to fit your project structure
const jsPath= './js';
const cssPath = './css';
const outputPath = 'dist';
const localDomain = 'http://mysite.local';

module.exports = {
  entry: {
    'calc-app': `${jsPath}/calc-app.js`,
  output: {
    path: path.resolve(__dirname, outputPath),
    filename: '[name].js',
  module: {
    rules: [
        test: /\.vue$/,
        use: 'vue-loader',
        test: /\.s?[c]ss$/i,
        use: [
        test: /\.sass$/i,
        use: [
            loader: 'sass-loader',
            options: {
              sassOptions: { indentedSyntax: true },
        test: /\.(jpg|jpeg|png|gif|woff|woff2|eot|ttf|svg)$/i,
        use: 'url-loader?limit=2048',
  plugins: [
    new VueLoaderPlugin(),
    new BrowserSyncPlugin({
      proxy: localDomain,
      files: [`${outputPath}/*.css`],
      injectCss: true,
    }, {
      reload: false,
    new MiniCssExtractPlugin({
      filename: '[name].css',
  resolve: {
    alias: { vue: 'vue/dist/vue.esm.js' },

After creating those two files, run the command to install the packages. Read our Webpack tutorial if you don’t know how.

4. App Setup (Enqueue and Shortcode)

We will output the Vue app using shortcode.

I know shortcode is archaic, but it’s still the quickest way to insert a custom code into whatever page builder you’re using. It also lets us enqueue the Vue script only when it’s used.

For example, we want to create a cost calculator:

add_action('wp_enqueue_scripts', 'my_enqueue_cost_calculator');
add_shortcode('cost-calculator', 'my_shortcode_cost_calculator');

function my_enqueue_cost_calculator() {

  $dir = get_stylesheet_directory_uri() . '/dist';

  wp_register_style('calc-app', $dir . '/calc-app.css', [], '');
  wp_register_script('calc-app', $dir . '/calc-app.js', [], '', false);

function my_shortcode_cost_calculator($atts, $content = null) {

  // Assume this is taken from get_posts() or from API
  $items = [
    [ 'id' => 0, 'name' => 'Fried Rice', 'price' => 8 ],
    [ 'id' => 1, 'name' => 'Dumpling',   'price' => 5 ],
    [ 'id' => 2, 'name' => 'Tea',        'price' => 2 ],
  $items_json = json_encode($items);

  return "<div id='cost-calculator'>
    <cost-calculator data-items='{$items_json}' />

5. The Script

import Vue from 'vue';
import CostCalculator from './CostCalculator.vue';

function onReady() {
  const app = new Vue({
    el: '#cost-calculator',
    components: {
      'cost-calculator': CostCalculator,

document.addEventListener('DOMContentLoaded', onReady);

First, we import Vue, so we don’t need to enqueue it separately.

After that, we import the .vue file that we will register as a component.

Then, we add a ready listener that will convert #cost-calculator (that we echoed using shortcode) into a Vue app.

Now let’s take a look at CostCalculator.vue:

  <div class="cost-calculator">
      <li v-for="i in items" :key="">
        <strong>{{ }}</strong>
        <span>${{ i.price }}</span>
        <a class="button" @click="addToCart(i)">Buy This</a>

    <h2>Total Cost: ${{ total }}</h2>

export default {
  props: {
    dataItems: {
      type: [String],
      required: true,

  data() {
    return {
      total: 0,
      items: [],

  created() {
    this.items = JSON.parse(this.$props.dataItems);

  methods: {
    addToCart(item) { += item.price;

<style lang="sass">
    display: flex
    flex-direction: column
    row-gap: 1rem
    list-style-type: none
    padding: 0

    display: flex
    column-gap: 1rem

    display: inline-flex
    align-items: center
    padding: 0.25rem 0.5rem
    font-size: 0.75rem

As you can see, everything is in one place.

If you’re wondering what is addToCart() function, the answer is just a few lines below it.

If you want to change the button styling, it’s just under the style tag. Additionally, we can use Sass by adding lang="sass" in the style tag.


Hopefully, this article will encourage you to try using Vue in WordPress. It took me months to figure out this workflow because there aren’t many resources about it.

Vue helps immensely in creating an interactive section of your website. But not everything has to be Vue. Simple stuff like a menu toggle is better done with jQuery or plain JavaScript.

Also if you’re curious about my ESLint setup, get it here.

If you have any question, feel free to comment below.

Default image
Henner Setyono
A web developer who mainly build custom theme for WordPress since 2013. Young enough to never had to deal with using Table as layout.
Leave a Reply