Getting Started with LitElement and Tailwind
By taking advantage of LitElement's thin layer over Web Components combined with Tailwind's near infinite offering of CSS utility classes, you can create a beautiful, completely custom, and incredibly performant data-driven user-interface with ease.
Tooling
We will be using a combination of tools to give us as great of a developer experience as possible, as quickly as possible.
- RollupJS - Rollup's approach to module bundling relies on the native JavaScript module standard, ES Modules, as opposed to Node's CommonJS module system.
- Babel - Babel will allow us to use the latest JavaScript features by compiling down our code to ES5, something older browsers will understand.
- TypeScript - Better to have silly errors during development than insanity-inducing errors in production. TypeScript's got our backs by reducing the bugs (accidental features?) we introduce in our code.
- ESLint
- Prettier
Installing RollupJS and initial configuration
Rollup has an excellent starter-app template that we can use to get going quickly. Clone their repo and we'll use that as our starting point.
Create/Edit/Remove these files:
LICENSE
- This is just a prototype, so we don't need this file.README.md
- Again, this is just a prototype, so we don't need this either.src/update.js
- We can remove this.public/index.html
- We can remove the<h1>
and the<p>
tags. We'll start fresh.package.json
- Remove thedate-fns
dependency.src/main.js
- Update it so it has a single command to test our config,console.log('Hello, World!')
After that's done, let's test our initial Rollup setup:
yarn install
ornpm install
to install our dependenciesyarn dev
ornpm dev
to run our development environment- Navigate to localhost:5000
- Open dev-tools to see our test message
Configuring Babel & TypeScript
To configure Babel and TypeScript, we'll take advantage of Rollup's Babel plugin, and we'll configure babel to compile our TypeScript code.
Install the following dependencies:
@babel/core
@rollup/plugin-babel
@babel/preset-env
typescript
@rollup/plugin-typescript
@babel/preset-typescript
Edit the following files:
src/main.js
rename tosrc/main.ts
rollup.config.js
Import and use the Babel plugin, and set the input file as a .ts
file (for TypeScript).
import resolve from '@rollup/plugin-node-resolve';
import commonjs from '@rollup/plugin-commonjs';
import { terser } from 'rollup-plugin-terser';
import typescript from '@rollup/plugin-typescript';
import { babel } from '@rollup/plugin-babel'
// `npm run build` -> `production` is true
// `npm run dev` -> `production` is false
const production = !process.env.ROLLUP_WATCH;
export default {
input: 'src/main.ts',
output: {
file: 'public/bundle.js',
format: 'es',
sourcemap: true
},
plugins: [
resolve(),
commonjs(),
typescript({
include: ['src/**/*.ts']
}),
babel({
babelHelpers: 'bundled',
exclude: "node_modules/**"
}),
production && terser({ format: { comments: false } }), // minify, but only in production
]
};
.babelrc
- Create and setup Babel configuration file to compile down to a minimum of ES5 code.
{
"presets": ["@babel/preset-env"]
}
tsconfig/json
- Create the TypeScript configuration file with the commandyarn tsc --init
Configuring ESLint and Prettier
Install the following dependencies:
eslint
prettier
eslint-config-prettier
eslint-plugin-prettier
Run yarn eslint --init
and choose the following options to create .eslintrc.js
, ESLint's config file.
- enforce code style
- JavaScript modules
- No Framework
- TypeScript
- Project runs in the browser
- Use Airbnb style guide
- config format JavaScript
After this, we can see that our IDE (in my scenario, VS Code) recognizes the ESLint configuration and is throwing some issues my way.
In .eslintrc.js
- Add
'plugin:prettier/recommended'
to theextends
array - Add
'prettier'
to the plugins array - Add
"prettier/prettier": "error"
to the rules object
Create and configure .prettierrc
, Prettier's configuration file. This single line is for any Window's developers, preventing any weird end-of-line prettier issues.
{
"endOfLine": "auto"
}
Using LitElement to make a data-driven User Interface
Now that we have a solid tooling setup, lets code up some UI with LitElement.
LitElement is a thin wrapper around Web Components. It provides us a way to declaratively write our UI as a function of state. A difference though between LitElement and another framework like React or Vue is that LitElement is built on top of the native component model of the web.
Let's code the obligatory 'Hello, World!'
First, install lit-element
as a dependency to the project:
yarn add lit-element
To use a web component, we'll define it in src/main.ts
, and then we'll use the new component in public/index.html
.
src/main.ts
import { LitElement, customElement, html } from "lit-element";
@customElement("app-component")
export default class AppComponent extends LitElement {
render() {
return html` <h1>Hello, World!</h1> `;
}
}
public/index.html
<!doctype html>
<html>
<head lang='en'>
<meta charset='utf-8'>
<meta name='viewport' content='width=device-width'>
<title>LitElement w/ Tailwind</title>
<style>
body {
font-family: 'Helvetica Neue', Arial, sans-serif;
color: #333;
font-weight: 300;
}
</style>
</head>
<body>
<app-component></app-component>
<script type="module" src='bundle.js'></script>
</body>
</html>
You'll most likely run into several ESLint and TypeScript issues. A few to be wary of are:
- TypeScript Decorators: The line with
@customElement()
is a TypeScript feature called a decorator. In order to use them, you have to explicitly turn the feature in in yourtsconfig.json
. - Expected 'this' to be used by class method 'render': This is an ESLint rule noticing that we aren't using
this
in the instance methodrender()
. We can tell ESLint to ignore specific functions by adding a rule to oureslintrc.js
After resolving any linting issues you may have, you should have your Hello, World!
Styling components with Tailwind
Now we are going to add Tailwind to our project.
Install the following dependencies:
rollup-plugin-postcss
tailwindcss
postcss
autoprefixer
Run the following command to generate a Tailwind configuration file:
npx tailwindcss init
tailwindcss.config.js
module.exports = {
purge: [
'./src/**/*.html',
'./src/**/*.js',
],
darkMode: false, // or 'media' or 'class'
theme: {
extend: {},
},
variants: {
extend: {},
},
plugins: [],
}
Create/Edit the following files:
rollup.config.js
import resolve from '@rollup/plugin-node-resolve';
import commonjs from '@rollup/plugin-commonjs';
import { terser } from 'rollup-plugin-terser';
import typescript from '@rollup/plugin-typescript';
import { babel } from '@rollup/plugin-babel'
import postcss from 'rollup-plugin-postcss' // import postcss plugin
const production = !process.env.ROLLUP_WATCH;
export default {
input: 'src/main.ts',
output: {
file: 'public/bundle.js',
format: 'es',
sourcemap: true
},
plugins: [
resolve(),
commonjs(),
postcss(), // use postcss plugin
typescript({
include: ['src/**/*.ts']
}),
babel({
babelHelpers: 'bundled',
exclude: "node_modules/**"
}),
production && terser({ format: { comments: false } }),
]
};
postcss.config.js
module.exports = {
plugins: {
tailwindcss: {},
autoprefixer: {},
}
}
src/styles.css
@tailwind base;
@tailwind components;
@tailwind utilities;
src/main.ts
import './styles.css' // import the tailwind styles
import { LitElement, customElement, html } from "lit-element";
@customElement("app-component")
export default class AppComponent extends LitElement {
createRenderRoot() {
return this; // turn off shadow dom to access external styles
}
render() {
return html` <h1 class="mx-auto my-4 py-4 text-center shadow-lg text-xl w-1/2">Hello, World!</h1> `; // use tailwind css utility classes
}
}
After our configuration, we now have a working LitElement & Tailwind setup!
Conclusion
After our configuration, we now have a strong base to start creating a beautiful data-driven user interface with powered by both LitElement and Tailwind CSS.