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.
- Kraken
-
A starting point for front-end projects. It includes a CSS reset, typography, a grid, and other conveniences.
- Pico CSS
-
A complete starter kit for styling of semantic HTML that includes a CSS reset.
- Tailwind
-
If you're going to be using Tailwind anyway, you may as well lean on its CSS reset.
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 theindex.html
but this quickly becomes unworkable if we have multiple HTML pages. Instead it is better to import them into theindex.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 itsh1
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:
- Common CSS files can be imported inside the shadow DOM using
<link>
tags or@import
. - CSS variables defined in the surrounding page can be referenced inside the shadow DOM's styles.
- For advanced shadow domination the
::part
pseudo-element can be used to expose an API for styling.
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.