struts: a CSS and JS starter for web projects
A starting point you copy and tailor, not a library you install. Opinionated, and shaped by the sites we build.
Approach
- 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>,commandforandcolor-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.
- Components in one folder
- A component keeps its markup, styles and script together, with an example that the components page renders.
- Written down
- Each pattern's header comment is its API, and each choice has a short decision doc explaining why.
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.
Colour
Surfaces set roles
Each palette colour gives a surface-* class: background, foreground,
color-scheme and the role tokens it sets. Buttons, links, tags, rules and focus rings
read the roles, so they follow the surface with no overrides.
-
surface-neutral
-
surface-neutral-600
-
surface-white
-
surface-lilac
-
surface-blue
-
surface-red
-
surface-background
On a colour
on-* sets the foreground and roles without a background, for text over an image or
a background set some other way.
on-blue
Inline link example and a button
Dark scheme
Roles switch with the OS setting, or with data-scheme on any element. Kite Co.'s dark
scheme is the blue. This box is always dark:
Text, an inline link example and buttons inside [data-scheme=dark].
Type
Type and flow
This block is prose: flow spacing, a reading measure and list styling. Each
element declares the space above it with --flow-space, so a paragraph after a
heading sits closer than a heading after a paragraph.
A third-level heading
Followed straight away by a fourth
Headings are IBM Plex Serif, body is IBM Plex Sans, and labels are IBM Plex Mono. Sizes are fluid,
built at compile time with fluid(min, max): resize the window to see them scale
between the 400 and 1440 pixel frames.
- List items get markers back inside prose
-
And spacing between them
- Nested lists indent further
- Ordered lists use numbers
- Like this
Kite Co. brought both deep technical expertise and genuine care to every stage.
The client
- not-prose
- leaves
- this list
- alone
- type-h1 · Heading 1
- Cultural organisations
- type-h2 · Heading 2
- Cultural organisations
- type-h3 · Heading 3
- Cultural organisations
- type-h4 · Heading 4
- Cultural organisations
- type-base · Body
- Better technology and better ways to communicate.
- type-label · Surtitle
- Announcement
- type-label-sm · tag
- CMS & Content Systems
- type-meta · Caption
stack-8
One fixed gap between every child,
whatever the element.
flow
Each child sets its own space.
Layout
Tracks and roles
Children of layout-grid sit in the content track unless they ask for another. Kite
Co.'s content runs the width of the frame, inset from the keyline, so text sits left.
Outside the grid
Grids
- grid-simple
- cols-2
- md:cols-4
- 4
- grid-auto
- fits
- as many
- as it can
- at 250px
- flex-grid
- cols-3
- centres
- the last
- row
Behaviour
Native first
Disclosure
Hidden with hidden and inert. Press Escape inside to close it.
Focusable link example.
Opened from a span with role="button".
For a simple accordion, native <details name> needs no script:
First question
Only one in the group stays open.
Second question
Opening this closes the first.
Dialog
This content came from a template, cloned into the slot when the dialog opened.
This opener asked for a different template with data-dialog-template.
Animate on scroll
Fades in
one after
another
No trigger, so it plays itself
Played by the box opposite, sliding up
Outer trigger’s item
Inner
trigger’s
items
Components
One folder each
Patterns style things that appear everywhere. A component is a whole piece of UI, like the header at the top of this page, with its markup, styles and behaviour kept together:
components/site-header/
styles.css @utility blocks; the header comment is the API
scripts.ts behaviour, registered with define()
example.html reference markup, one <template> per example
A project ports each example.html to whatever renders its pages, such as an Astro
component or a PHP template. The components page shows every example in a frame you can resize.