Plain Vanilla Styling

Modern CSS

Modern web applications are built on top of rich tooling for dealing with CSS, relying on plenty of NPM packages and build steps. A vanilla web application however has to choose a lighter weight path, abandoning the preprocessed modern CSS approaches and choosing strategies that are browser native.

#

Reset

Resetting the styles to a cross-browser common baseline is standard practice in web development, and vanilla web apps are no different.

A minimal reset is the one used by this site:

Other options, in increasing order of complexity:

modern-normalize

A more comprehensive solution for resetting CSS for modern browsers.

Include it from CDN

Kraken

A starting point for front-end projects. It includes a CSS reset, typography, a grid, and other conveniences.

Include it from CDN

Pico CSS

A complete starter kit for styling of semantic HTML that includes a CSS reset.

Include it from CDN

Tailwind

If you're going to be using Tailwind anyway, you may as well lean on its CSS reset.

Include it from CDN

#

Fonts

Typography is the keystone of a web site or web application. A lean approach like vanilla web development should be matched with a lean approach for typography. Modern Font Stacks describes a varied selection of commonly available fonts with good fallbacks, avoiding the need to load custom fonts and add external dependencies.

This site uses the Geometric Humanist stack for normal text, and the Monospace Code stack for source code.

#

The Toolbox

In any real world web project the amount of CSS quickly becomes unwieldy unless it is well-structured. Let's look at the toolbox that CSS provides us in modern browsers to provide that structure.

@import

The most basic structuring technique is separating CSS into multiple files. We could add all those files in order as <link> tags into the index.html but this quickly becomes unworkable if we have multiple HTML pages. Instead it is better to import them into the index.css

For example, here's the main CSS file for this site:

Below is a recommended organization of CSS files.

Custom properties (variables)

CSS variables can be used to define the site's font and theme in a central place.

For example, here are the variables for this site:

CSS variables become even more capable when combined with calc().

Custom elements

Styles can be easily scoped to a custom element's tag.

For example, the styles of the avatar component from the components page are all prepended by the x-avatar selector:

Custom elements can also have custom attributes that selectors can leverage, as with the [size=lg] style in this example.

Shadow DOM

Adding a shadow DOM to a web component further isolates its styles from the rest of the page. For example, the x-header component from the previous page styles its h1 element inside its CSS, without affecting the containing page or the header's child elements.

All CSS files that need to apply to the shadow DOM must be loaded into it explicitly, but CSS variables are passed into the shadow DOM.

A limitation of shadow DOMs is that to use custom fonts inside them, they must first be loaded into the light DOM.

#

Files

There are many ways to organize CSS files in a repository, but this is the one used here:

/index.css
The root CSS file that imports all the other ones using @import.
/styles/reset.css
The reset stylesheet is the first thing imported.
/styles/variables.css
All CSS variables are defined in this separate file, including the font system.
/styles/global.css
The global styles that apply across the web pages of the site.
/components/example/example.css
All styles that aren't global are specific to a component, in a CSS file located next to the component's JS file.
#

Scope

In order to avoid conflicting styles between pages and components we want to scope styles locally by default. There are two major mechanisms for achieving that in vanilla web development.

Prefixed selectors

For custom elements that don't have a shadow DOM we can prefix the styles with the tag of the custom element. For example, here's a simple web component that uses prefixed selectors to create a local scope:

Shadow DOM import

Custom elements that use a shadow DOM start out unstyled with a local scope, and all styles must be explicitly imported into them. Here is the prefixed example reworked to use a shadow DOM instead.

To reuse styles from the surrounding page inside the shadow DOM, consider these options:

#

Replacing CSS modules

The local scoping feature of CSS modules can be replaced by one of the scoping approaches describe above. For example, let's take the canonical example of CSS modules from the Next.JS documentation:

As a vanilla web component, this is what that looks like:

Because the shadow DOM does not inherit the page's styles, the styles.css must first import the styles that are shared between the page and the shadowed web component.

#

Replacing PostCSS

Let's go over the main page of PostCSS to review its feature set.

Add vendor prefixes to CSS rules using values from Can I Use.
Vendor prefixes are no longer needed for most use cases. The :fullscreen pseudo-class shown in the example now works across browsers unprefixed.
Convert modern CSS into something most browsers can understand.
The modern CSS you want to use is most likely already supported. The color: oklch() rule shown in the example now works across browsers.
CSS Modules
See the alternatives described in the previous section.
Enforce consistent conventions and avoid errors in your stylesheets with stylelint.
The vscode-stylelint extension can be added into Visual Studio Code to get the same linting at develop time, without needing it to be baked into a build step.

Bottom line: Microsoft's dropping of support for IE11 combined with the continuing improvements of evergreen browsers has made PostCSS largely unnecessary.

#

Replacing SASS

Similarly to PostCSS, let's go over the main feature set of SASS:

Variables
Replaced by CSS custom properties.
Nesting
CSS nesting is newly available across major browsers, which may be good enough depending on your browser support needs.
Modules
Can be approximated by a combination of @import, CSS variables, and the scoping approaches described above.
Mixins
Regrettably the CSS mixins feature that will replace this is still in specification.
Operators
In many cases can be replaced by the built-in calc() feature.

Bottom-line: SASS is a lot more powerful than PostCSS, and while many of its features have a vanilla alternative it is not as easy to replace entirely. YMMV whether the added complexity of the SASS preprocessor is worth the additional abilities.


Up next

Learn about making vanilla sites with web components.