Skip to content

struts: a CSS and JS starter for web projects

by Kite Co.

A starting point you copy and tailor, not a library you install. Opinionated, and shaped by the sites we build.

Principles

Copy it, don't install it
Each project takes the parts it needs and changes them freely. There's no version to keep up with.
The platform first
<details>, <dialog>, commandfor, popover and color-mix() do most of the work. JS fills the gaps, and everything works without it.
Tailwind underneath, used sparingly
Tokens and utilities come from Tailwind v4. Patterns are BEM blocks with a small custom-property API, so markup stays readable.
Colour by context
Set a surface and everything inside follows it. No per-component overrides.
Written down
Each file's header comment is its API, and these pages show it. Each choice has a short decision doc explaining why.

Layers

struts is built up in layers, each with its own page. A layer can hold CSS, scripts or both: what decides the layer is what a thing is for, not what it's written in.

  1. Foundations

    css/tokens/ · css/base/ · colors.config.json

    Tokens, shared knobs and element defaults: what every page gets before a single class. Colour, type, spacing and bare HTML.

  2. Patterns

    css/patterns/ · js/

    Reusable blocks that work on any markup, each with a custom-property API. Some have a script, hung off data-* hooks: disclosure, dialog and animate on scroll.

  3. Utilities

    css/utilities/ · js/

    Single-purpose helpers: classes like stack-16 and rule-block-start, and functions like define() and debounce(). Tailwind's own utilities sit alongside them.

  4. Components

    components/

    Markup, styles and behaviour that only work together, one folder each, built from everything above.

css/index.css imports tokens, base, patterns, components and utilities, in that order. Element defaults sit in Tailwind's base layer, so any class beats them. Why: CSS layers and file order.

One more folder, css/adapters/, maps a platform's classes onto patterns (WordPress, for now). A project imports one if it needs it.

Naming

Ask one question of anything new: is it owned by one pattern, or read by several? The answer decides its name.

Global knobs
Single dashes, in Tailwind's shape: --color-accent, --gutter, --space-layout.
Pattern variables
--{block}[__{element}]--{property}[--{state}], with CSS property names: --btn--background-color--hover. Modifiers never own variables.
Classes
Patterns and components are BEM (btn btn--secondary). Utilities are Tailwind-shaped (stack-16). Colour contexts are surface-{colour} and on-{colour}.
JS hooks
Behaviour hangs off data-* attributes, never classes: data-disclosure, data-dialog.

The full rules: docs/naming.md.

Decisions

Why things are the way they are, one topic each.

  1. CSS layers and file order
  2. Colour contexts
  3. The custom-property API
  4. Spacing and fluid sizing
  5. Flow and rhythm
  6. Layout
  7. JS and dynamicElements
  8. Progressive enhancement
  9. Animate on scroll hooks
  10. Components
  11. The docs pages

Get started

Give your coding agent this prompt from your project's root:

Adopt struts (https://github.com/kite-co-code/struts) into this project. Clone it to a temporary folder and follow its ADOPT.md. Plan the work in phases that each leave a working build, starting with the foundation.

ADOPT.md is the full procedure, for agents and people alike.