Install
Same three ways as every jen pack. The pack only writes CSS files: no build step, no runtime, nothing to install in your project.
Globally
$ npm install -g @codejen/jen @codejen/pack-web $ jen web:cube --primary=#5b5bd6 --dir=src/css
Locally
$ npm install --save-dev @codejen/jen @codejen/pack-web $ npx jen web:cube --primary=#5b5bd6 --dir=src/css
Not at all
Ask for a web: generator without the pack installed and jen fetches @codejen/pack-web into a throwaway directory, runs it once and removes it again (first-party @codejen scope only; JEN_NO_FETCH=1 turns it off).
$ npx @codejen/jen web:cube --primary=#5b5bd6 --dir=src/css
Where files go
Generators write into the folder you run them in, or --dir. Everything meets in one entry point: styles.css, made by web:base or web:cube. It declares the cascade layers in CUBE CSS order – reset, tokens, base, compositions, utilities, blocks, exceptions – so a later layer wins over an earlier one whatever the selectors' specificity, and it ends with a /* jen:imports */ marker. Every other generator adds its @import above that marker, with the path relative to the entry point, so running one from a subfolder still wires it in right.
Link styles.css from your page, or import './css/styles.css' in a Vite project. The home page has the general rules.
The boilerplate the generators write lives as plain .css, .html and .js files in the pack's templates/ folder, so it's easy to read and adapt; a few have {{placeholders}} for the parts the generators fill in. Palettes, fluid scales and utilities are computed, so they're made in code.
Starter
web:cube
The whole starter at once: web:base, web:fluid, web:compositions and web:utilities, plus web:palette and web:button when you pass --primary. It takes every flag of web:palette and web:fluid.
$ jen web:cube --primary=#5b5bd6 --secondary=330 --dir=src/css web:cube (pack @codejen/pack-web) – create a CUBE CSS starter: base, fluid scales, compositions, utilities (+ palette and buttons with --primary) + src/css/styles.css + src/css/reset.css + src/css/base.css + src/css/palette.css + src/css/fluid.css + src/css/compositions.css + src/css/utilities.css + src/css/button.css ✓ 8 written
Params
primary
brand color – adds the palette and buttons: #rrggbb, #rgb, oklch(L C H) or a hue in degrees
secondary
a second brand color, same formats as --primary
tertiary
a third brand color, same formats as --primary
neutral
the gray's tint (default: the primary's hue)
success
overrides the built-in success color
warning
overrides the built-in warning color
danger
overrides the built-in danger color
info
overrides the built-in info color
contrast
WCAG level the role tokens meet: aaa, or aa (shades 3 apart)
tailwind
also map the scales into Tailwind's text-* and spacing
minWidth
smallest viewport width in rem, where sizes stop shrinking
maxWidth
largest viewport width in rem, where sizes stop growing
minSize
base font size (step 0) in rem at --minWidth
maxSize
base font size (step 0) in rem at --maxWidth
minRatio
how much each type step grows over the last, at --minWidth
maxRatio
how much each type step grows over the last, at --maxWidth
Tokens
web:palette
Six shades per color, from your colors, as custom properties: --color-primary-1 (lightest) to --color-primary-6 (darkest). The shades aren't copied from your input but placed by luminance, so every color's shade 3 is equally light – and contrast depends only on how many shades apart two colors are, across colors too:
- 2 apart: at least 3:1 – borders, icons, large text
- 3 apart: at least 4.5:1 – WCAG AA for text
- 4 apart: at least 7:1 – WCAG AAA for text
So --color-primary-5 text on --color-danger-1 is AAA, without looking anything up. Besides your colors you get a neutral gray tinted toward the primary hue, and success, warning, danger and info.
$ jen web:palette --primary=#5b5bd6 --secondary=330 --dir=src/css web:palette (pack @codejen/pack-web) – create a contrast-checked color palette (CSS custom properties) + src/css/palette.css ~ src/css/styles.css + @import "./palette.css"; ✓ 2 written
--primary=#5b5bd6 --secondary=330: labels sit at least 3 shades away from their swatchParams
primary
brand color: #rrggbb, #rgb, oklch(L C H) or a hue in degrees
secondary
a second brand color, same formats as --primary
tertiary
a third brand color, same formats as --primary
neutral
the gray's tint (default: the primary's hue)
success
overrides the built-in success color
warning
overrides the built-in warning color
danger
overrides the built-in danger color
info
overrides the built-in info color
contrast
WCAG level the role tokens meet: aaa, or aa (shades 3 apart)
tailwind
write Tailwind's @theme instead – on by itself in a Tailwind project
Role tokens and dark mode
On top of the shades come role tokens that switch between light and dark mode with light-dark(), following color-scheme. Browsers without light-dark() get the same through prefers-color-scheme, and data-theme="light" or "dark" on <html> forces a mode.
- Neutral:
--color-surface,--color-surface-2(cards, sidebars),--color-text,--color-text-muted,--color-border,--color-focus - Per color (shown for primary):
--color-primary,--color-primary-strong(hover, and colored text on tints),--color-primary-subtle(tinted background),--color-on-primary(text on the color),--color-primary-large(large text only)
--color-surface-2 and the -subtle tints sit between shades, so they stand out from the page while text on them keeps the full level. By default every text pair is AAA. Before the file is written, each pair is measured on the final hex values in both modes; if one ever missed, the generator would fail rather than write it.
Large text
Large text – at least 1.5rem, or 1.1667rem bold – needs less contrast for the same level: 4.5:1 is AAA. The -large roles use that and sit one shade closer to the background, so headings and heroes can be more colorful and still be AAA:
h1 {
color: var(--color-primary-large);
}
.hero {
background: var(--color-primary-large);
color: var(--color-on-primary);
font-size: 2.5rem;
}
web:fluid
Fluid type and space in the style of Utopia: every size grows from its value at --minWidth to its value at --maxWidth with clamp(). Type steps run from --step--2 to --step-5; space from --space-3xs to --space-3xl, plus one-up pairs like --space-s-m that grow more. All lengths are rem, and the rem part of each formula keeps text zoomable.
$ jen web:fluid --dir=src/css web:fluid (pack @codejen/pack-web) – create fluid type and space scales (Utopia-style clamp() tokens) + src/css/fluid.css ~ src/css/styles.css + @import "./fluid.css"; ✓ 2 written
A size that would grow more than 2.5× across the range is refused: browser zoom couldn't reach 200% text size anymore (WCAG 1.4.4).
$ jen web:fluid --maxSize=3
web:fluid (pack @codejen/pack-web) – create fluid type and space scales (Utopia-style clamp() tokens)
web:fluid: --step--1 grows from 0.9375rem to 2.4rem, more than 2.5× – zoom couldn't reach 200% text size (WCAG 1.4.4); pick closer sizes or ratios
Params
minWidth
smallest viewport width in rem, where sizes stop shrinking
maxWidth
largest viewport width in rem, where sizes stop growing
minSize
base font size (step 0) in rem at --minWidth
maxSize
base font size (step 0) in rem at --maxWidth
minRatio
how much each type step grows over the last, at --minWidth
maxRatio
how much each type step grows over the last, at --maxWidth
tailwind
also map the scales into Tailwind's text-* and spacing
CUBE CSS
web:base
The entry point, a modern reset and base styles for plain elements: a stable scrollbar gutter, balanced headings and pretty paragraphs, a visible focus ring, transition tokens that drop to zero under prefers-reduced-motion, visited links, <li data-icon="✅"> list markers, and # heading anchors with a highlight when you jump to them. Colors fall back to system colors until there's a palette; font sizes use the fluid steps when they exist.
$ jen web:base --dir=src/css web:base (pack @codejen/pack-web) – create the stylesheet entry point (CUBE CSS layers), a modern reset and base styles + src/css/styles.css + src/css/reset.css + src/css/base.css ✓ 3 written
web:compositions
Layout primitives after Every Layout and SmolCSS, in the compositions layer. Each is tuned with custom properties that fall back to the fluid space tokens, then to fixed values. --only=flow,cluster picks some.
$ jen web:compositions --dir=src/css web:compositions (pack @codejen/pack-web) – create CUBE CSS compositions (flow, cluster, sidebar, grid, wrapper, breakout, reel, …) + src/css/compositions.css ~ src/css/styles.css + @import "./compositions.css"; ✓ 2 written
.grid and .wrapper
.grid fits as many columns as there's room for, each at least --grid-min-item-size; a few items stretch to fill the row. .wrapper is a centered column up to --wrapper-max-width that never touches the viewport edges.
.flow, .cluster and .repel
.flow puts --flow-space between siblings. .cluster wraps items as a group, like tags or nav links. .repel pushes two groups to either end.
.sidebar and .switcher
.sidebar puts its first child beside the content, and stacks once the content would get narrower than --sidebar-content-min; data-direction="end" puts the sidebar last. .switcher lays its children side by side above --switcher-threshold and stacks them below.
.breakout
A text column (--breakout-max-width) whose children can break out with data-breakout="wide" or data-breakout="full".
.center, .overlay, .reel and .gallery
.center centers in both directions. .overlay layers its children in one grid cell, like text over a picture. .reel is a scrolling row that snaps to its items. .gallery crops media to --gallery-ratio in wrapping rows.
Params
only
comma-separated compositions to pick, e.g. flow,cluster (default: all)
web:utilities
Utility classes from your tokens, in the utilities layer: .color-* and .bg-* for every role token – not the raw shades, so you stay on checked pairs – .step-* font sizes, .flow-space-* and .gutter-* for every space size, .pad-fluid, .unbreakable and .visually-hidden. Run it after the tokens, and again with --force when they change.
$ jen web:utilities --dir=src/css web:utilities (pack @codejen/pack-web) – create utility classes from the palette and fluid tokens + src/css/utilities.css ~ src/css/styles.css + @import "./utilities.css"; ✓ 2 written
Blocks
web:button
A .button block with BEM modifiers: --secondary, --tertiary and --danger for the colors the palette has, --outline, --ghost, --pill, --small and --large. Text on its fill, on its hover fill and on the tinted hover of --outline, the fill against the page, and the focus ring all keep the palette's checked contrast. Your own variant sets the --button-* properties in a modifier.
$ jen web:button --dir=src/css web:button (pack @codejen/pack-web) – create a contrast-checked .button block on the palette tokens + src/css/button.css ~ src/css/styles.css + @import "./button.css"; ✓ 2 written
It needs a palette, and says so:
$ jen web:button
web:button (pack @codejen/pack-web) – create a contrast-checked .button block on the palette tokens
web:button needs a palette – run `jen web:palette --primary=<color>` first
web:card
A composable .card after SmolCSS: any children get its padding, media first or last goes edge to edge, and the last child sits at the bottom, so footers line up across a row of cards.
$ jen web:card --dir=src/css web:card (pack @codejen/pack-web) – create a composable .card block on the palette tokens + src/css/card.css ~ src/css/styles.css + @import "./card.css"; ✓ 2 written
web:avatars
Overlapping round avatars that move apart on hover or focus. Each step is at least 2.75rem wide, the WCAG AAA target size, for avatars that are links.
$ jen web:avatars --dir=src/css web:avatars (pack @codejen/pack-web) – create an overlapping .avatars list block + src/css/avatars.css ~ src/css/styles.css + @import "./avatars.css"; ✓ 2 written
Pages and platform features
Modern HTML and CSS that used to need JavaScript, each as progressive enhancement: where a browser lacks a feature, the page still works – it just doesn't animate or anchor. Generators that come with markup take --into=<page.html> and put it in before </body> – or before the line --line names: a number like --line=8, or text on that line like --line="</nav>" to put a menu inside the nav. Either way the page needs no marker, the markup is indented to fit, and running a generator twice duplicates nothing. Script tags they need still go before </body>.
web:page
A new page after Manuel Matuzović's HTML boilerplate: lang, a no-js/js class switched by a module script, the viewport, a title of the form "Page – Site", description and Open Graph tags. On top, it fits the page to your project: the stylesheet link points at the entry point, relative to the page; theme-color follows the palette's page colors in light and dark mode; and icons and the manifest are linked only if those files exist, at the root or in public/. Canonical, og:url and og:image need your site's address, so they come with --url.
$ jen web:page --name=about --site="My Site" --lang=en-GB --url=https://example.com --dir=pages web:page (pack @codejen/pack-web) – create an HTML page (after matuzo.at's boilerplate), linked to the stylesheet, palette and icons + pages/about.html ✓ 1 written
Params
name
file name: about makes about.html
title
page title (default: from --name, e.g. About)
site
site name, appended to the title: "title – site"
description
for search results and link previews
lang
page language; with a region (en-GB), it also sets og:locale
url
the site's address, for link previews
image
image that link previews show
imageAlt
alt text for --image
script
a module script to load at the end of the body
web:manifest
Makes the site installable with a manifest.webmanifest. It goes into public/ in a Vite project or any project with that folder, else where you run jen. The colors come from the palette – theme_color from primary, background_color from surface – or from --themeColor and --backgroundColor (#hex, oklch() or a hue), and white without either. They're written as hex, which every browser reads. The manifest lists only the icons that exist, at the root or in public/. Next to a favicon.svg, it also writes icon-maskable.svg: the same icon on the background color, shrunk into the safe zone that round and rounded-square icon masks leave alone. --into=<page.html> links the manifest and adds theme-color to the page's head, one per color scheme when there's a palette. Pages you make later with web:page link the manifest by themselves.
$ jen web:manifest --name="My Site" --into=index.html web:manifest (pack @codejen/pack-web) – create a web app manifest with the palette's colors and the icons the project has (--into links it from a page) + manifest.webmanifest + icon-maskable.svg ~ index.html + <link rel="manifest" href="./manifest.webmanifest"> = index.html already present: <meta name="theme-color" content="#f8f9ff" media="(prefers-color-scheme: light)"> = index.html already present: <meta name="theme-color" content="#17181e" media="(prefers-color-scheme: dark)"> ✓ 3 written
$ cat manifest.webmanifest
{
"name": "My Site",
"short_name": "My Site",
"start_url": "./",
"scope": "./",
"display": "standalone",
"theme_color": "#383396",
"background_color": "#f8f9ff",
"icons": [
{
"src": "favicon.svg",
"type": "image/svg+xml",
"sizes": "any",
"purpose": "any"
},
{
"src": "icon-maskable.svg",
"type": "image/svg+xml",
"sizes": "any",
"purpose": "maskable"
}
],
"lang": "en",
"dir": "ltr"
}
Some browsers, and iOS for the home screen, still want PNGs. --png renders icon-192.png, icon-512.png and apple-touch-icon.png from favicon.svg and lists them. The apple-touch icon goes on the background color with some room around it, because iOS fills transparent corners with black. jen bundles no image library: --png uses rsvg-convert (librsvg) or ImageMagick, whichever is installed, and stops with a message if neither is.
$ jen web:manifest --name="My Site" --png --into=index.html --force web:manifest (pack @codejen/pack-web) – create a web app manifest with the palette's colors and the icons the project has (--into links it from a page) ! manifest.webmanifest will be overwritten ! icon-maskable.svg will be overwritten + icon-192.png + icon-512.png + apple-touch-icon.png ~ index.html + <link rel="apple-touch-icon" href="./apple-touch-icon.png"> = index.html already present: <link rel="manifest" href="./manifest.webmanifest"> … (+2 more already present) ✓ 6 written
Params
name
the app name the install prompt shows
shortName
name under the icon (default: the name, or its first word if it is longer than 12 characters)
display
fullscreen, standalone, minimal-ui or browser
themeColor
browser toolbar color: #hex, oklch() or a hue (default: the palette's primary, else white)
backgroundColor
splash screen color: #hex, oklch() or a hue (default: the palette's surface, else white)
lang
app language; also sets the text direction (rtl for Arabic, Hebrew, …)
png
also render PNG icons from favicon.svg, with rsvg-convert or ImageMagick
into
a page to link the manifest from (and with --png, the apple-touch icon)
web:sw
A service worker without dependencies, short enough to read and own, with the caching strategies of Jake Archibald's Offline Cookbook. Pages go network first: fresh when online, from the cache when not, and the offline page for anything never visited. Everything else is cache first, or stale-while-revalidate with --strategy=swr: served from the cache right away, and refreshed in the background for next time. On install, it caches the pages next to it and the entry stylesheet with everything it imports, or what --precache=app.css,logo.svg lists. --offline adds an offline.html, made from web:page's template. The cache name carries a version: bump VERSION in sw.js and activating the new worker removes the old caches.
register-sw.js registers the worker. Like pack-js, it comes in the project's style: TypeScript, JavaScript with JSDoc types, or plain JavaScript, by --types or else the nearest tsconfig.json or jsconfig.json. The service worker itself is always plain JavaScript, since browsers load it as it is. When a new version is waiting, sw-update fires on window, and event.detail.apply() switches to it and reloads the page – ask first, or apply it right away. In a Vite project, sw.js goes into public/ and registers at Vite's base URL, and only in production builds, so the dev server's modules never end up in a cache. There, Vite builds the stylesheets, so they're cached as they load rather than on install.
$ jen web:sw --offline --into=index.html web:sw (pack @codejen/pack-web) – create a service worker (network-first pages, cached assets, an offline page) and a module that registers it + sw.js + offline.html + register-sw.js ~ index.html + <script type="module" src="./register-sw.js"></script> ✓ 4 written
A live preview is no use for a service worker, so here's the start of the file it writes:
$ head -12 sw.js
// Caching strategies after Jake Archibald's Offline Cookbook: https://web.dev/articles/offline-cookbook
// Bump the version to start over: activate removes the caches of older versions.
const VERSION = 1;
const PREFIX = 'my-site-';
const CACHE = `${PREFIX}v${VERSION}`;
const OFFLINE_PAGE = './offline.html';
const PRECACHE = [
'./',
'./styles.css',
'./reset.css',
Params
precache
comma-separated files to cache on install, relative to sw.js (default: the pages and stylesheets)
strategy
for assets: cache-first, or swr (stale-while-revalidate); pages stay network-first
offline
also write offline.html, shown for pages that aren't cached
into
a page (script tag) or module (import line) that loads register-sw
types
ts, jsdoc or none (default: from the nearest tsconfig.json or jsconfig.json)
web:dialog
A modal .dialog that fades and rises in and out (@starting-style) above a dimmed backdrop. The markup opens it with the command API – <button commandfor="confirm" command="show-modal"> – so no JavaScript is needed; commands.js stands in for browsers that don't have the command API yet. closedby="any" lets a click outside close it.
$ jen web:dialog --name=confirm-delete --into=index.html --dir=css web:dialog (pack @codejen/pack-web) – create a modal .dialog block (and its markup, opened via the command API, with --into) + css/dialog.css ~ css/styles.css + @import "./dialog.css"; ~ index.html + <button class="button" commandfor="confirm-delete" command="show-modal">Confirm delete</button> (+8 more) + commands.js ~ index.html + <script type="module" src="./commands.js"></script> ✓ 4 written
Params
name
id, and the title from it: settings makes "Settings"
into
a page to put the markup into, before </body>
line
with --into: put it before this line instead – a number, or text on that line like </nav>
web:popover
A .popover anchored below its trigger with CSS anchor positioning, flipping above or to the other side when there's no room. Without anchor positioning it opens centered, like any popover. The markup uses popovertarget and ties trigger and popover together with an anchor name.
$ jen web:popover --name=menu --into=index.html --dir=css web:popover (pack @codejen/pack-web) – create an anchored .popover block (and its markup with --into) + css/popover.css ~ css/styles.css + @import "./popover.css"; ~ index.html + <button class="button" popovertarget="menu" style="anchor-name: --menu">Menu</button> (+3 more) ✓ 3 written
Params
name
id, and the label from it: user-menu makes "User menu"
into
a page to put the markup into, before </body>
line
with --into: put it before this line instead – a number, or text on that line like </nav>
web:scroll
Scroll-driven animations, no JavaScript: a .reading-progress bar that follows the page's scroll position, .reveal for elements that fade in as they scroll into view, and .parallax, shifted by --parallax-shift. Without support or with reduced motion, nothing is hidden and nothing moves.
$ jen web:scroll --into=index.html --dir=css web:scroll (pack @codejen/pack-web) – create scroll-driven animations: reading progress, reveal, parallax + css/scroll.css ~ css/styles.css + @import "./scroll.css"; ~ index.html + <div class="reading-progress" aria-hidden="true"></div> ✓ 3 written
Params
into
a page to put the markup into, before </body>
line
with --into: put it before this line instead – a number, or text on that line like </nav>
web:transitions
Cross-document view transitions between the pages of a site, as the browser's crossfade or --style=slide – off with reduced motion, and simply absent where unsupported. --spa adds view-transition.js for same-document transitions: transition(() => updateTheDom()), a plain update where unsupported.
$ jen web:transitions --style=slide --spa --dir=css web:transitions (pack @codejen/pack-web) – create view transitions between pages (and a same-document helper with --spa) + css/transitions.css ~ css/styles.css + @import "./transitions.css"; + css/view-transition.js ✓ 3 written
Params
style
fade (the browser's crossfade) or slide
spa
also add view-transition.js for same-document transitions in a single-page app
Tailwind v4
web:tailwind
Sets up the entry point for Tailwind: @import "tailwindcss", with Tailwind's layers fitted into the CUBE CSS order. From then on, web:palette writes its tokens into @theme under the same names – so Tailwind makes bg-surface, text-on-primary, bg-primary-subtle, border-border – and drops Tailwind's own colors, leaving only checked ones. web:fluid maps its scales to text-step-1, p-s-m and friends, and web:cube leaves the utilities to Tailwind. jen doesn't install anything: add tailwindcss and its build integration (e.g. @tailwindcss/vite) yourself.
$ jen web:tailwind --dir=src/css web:tailwind (pack @codejen/pack-web) – set up the stylesheet entry point for Tailwind v4 (layers in CUBE CSS order) + src/css/styles.css ✓ 1 written $ jen web:cube --primary=#5b5bd6 --dir=src/css web:cube (pack @codejen/pack-web) – create a CUBE CSS starter: base, fluid scales, compositions, utilities (+ palette and buttons with --primary) + src/css/reset.css + src/css/base.css + src/css/palette.css + src/css/fluid.css + src/css/compositions.css + src/css/button.css ~ src/css/styles.css + @import "./reset.css"; … ✓ 7 written
Credits
Every file credits the work it builds on, in a comment at the top of the part it's about:
- CUBE CSS by Andy Bell: the layer order, and the Repel composition
- Every Layout by Heydon Pickering and Andy Bell: the flow (Stack), cluster, sidebar, switcher and reel compositions
- SmolCSS by Stephanie Eckles: the grid, wrapper (intrinsic container), center, overlay, breakout, reel (scroll snap) and gallery compositions, the card and avatar list,
.pad-fluidand.unbreakable, and the transition tokens, visited links, heading anchors and list markers in the base styles - A (more) Modern CSS Reset by Andy Bell and a modern CSS reset by Josh W. Comeau: the reset
- Utopia by James Gilyead and Trys Mudford: the fluid type and space scales
- Reasonable Colors by Matthew Howell: the idea of shade distances that guarantee contrast
- OKLab by Björn Ottosson: the color math behind the shades
- HTML boilerplate by Manuel Matuzović:
web:page - How to Favicon in 2021 by Andrey Sitnik (Evil Martians): the icon set
web:pagelinks andweb:manifestlists - Web app manifest reference by MDN:
web:manifest - The Offline Cookbook by Jake Archibald: the caching strategies in
web:sw
The code is written fresh for this pack, not copied. The pack is ISC licensed.