Skip to content

Evolving Cascade toward a typed CSS component system — feedback from ocaml.org #1

Description

@cuihtlauac

Context

This comes from an analysis of how Cascade could be used in ocaml.org, a Dream-based OCaml web application. The site uses Tailwind CSS heavily — utility classes are written inline in .eml (Dream HTML) templates, with some component-level styles extracted into CSS partials using @apply.

The analysis looked at whether Cascade could address the main maintenance pain points in the project, and what it would take. The short answer: Cascade's typed AST and Tailwind-aware optimizer are strong foundations, but the missing piece is a developer-facing abstraction layer that bridges style definitions to template usage.

The problem in ocaml.org (concrete data)

The site has 69 EML files totaling ~9,000 lines. The maintenance burden comes from:

  • Dense inline class strings. A typical element looks like:

    <a class="my-0 font-sans text-xl tracking-normal font-extrabold leading-7 changelog-title whitespace-normal break-all text-title dark:text-dark-title hover:underline">

    That's 163 characters of class names on a single attribute. The structural intent (an anchor tag) is buried in styling tokens.

  • Dark mode duplication. The pattern text-title dark:text-dark-title appears 25 times in home.eml alone. Every color reference needs both variants written out inline. This is pure noise — it carries no structural information.

  • Responsive variant noise. flex flex-col lg:flex-row-reverse, space-y-24 lg:space-y-32, py-3 lg:py-6 — each breakpoint adds another copy of the property inline.

  • No semantic abstraction for repeated patterns. The same long class strings appear verbatim multiple times (e.g., 7× for "mx-8 my-4 dark:bg-dark-white py-4 px-12 rounded-md"). There's no way to name a style combination once and reuse it with compile-time safety.

  • Three concerns in one file. EML interleaves OCaml logic (match, List.iter, conditionals) with HTML structure with inline styles. Each makes the others harder to read.

The project partially addresses this with CSS partials using @apply (e.g., .btn, .card, .changelog-title), but these are string-based — typos are silent, drift between CSS definitions and template usage is invisible, and refactoring is risky.

What Cascade provides today vs. what's needed

Cascade already offers:

  • A typed CSS AST with 100+ properties, modern selectors, colors, values
  • A parser, pretty-printer, and optimizer
  • Tailwind awareness in the optimizer (modifier-prefix depth, group/peer markers, variant sorting)
  • CSS variables, custom properties, @layer, media queries, container queries

What's missing for a project like ocaml.org is the layer between "I can construct CSS in OCaml" and "my templates use CSS safely":

  1. Named, reusable style units that can be referenced from templates
  2. Dark mode and responsive variants as authoring-time concepts (not just parseable CSS features)
  3. Compile-time checking that a style referenced in a template actually exists
  4. A migration path from Tailwind that doesn't require rewriting everything at once

Recommendations

1. Style registry — named, composable style bundles

The single most impactful addition would be a way to define named style bundles that emit CSS class names:

(* Define once *)
let changelog_title = Style.define "changelog-title"
  [ font_family Sans_serif
  ; font_size (Rem 1.25)
  ; font_weight (Int 800)
  ; line_height (Rem 1.75)
  ; white_space Normal
  ; word_break Break_all
  ; color (var title_color)
  ; hover [ text_decoration Underline ]
  ]

(* In a Dream EML template — compiler-checked reference *)
<a class="<%s Style.class_name changelog_title %>">

The key property: Style.class_name returns a string, but the binding is an OCaml value. A typo like changelg_title is a compile error. Today Cascade lets you construct CSS, but there's no concept of a reusable, named style unit that bridges the gap between OCaml code and HTML templates.

This is conceptually similar to CSS Modules or Elm's typed stylesheets, but native to OCaml.

2. First-class dark mode and responsive variants

The pattern text-title dark:text-dark-title repeated 25+ times per page could become:

let title_text = Style.define "title-text"
  [ color (hex "#333")
  ; dark [ color (hex "#e0e0e0") ]
  ; md [ font_size (Rem 1.5) ]
  ]

This is more than @media (prefers-color-scheme: dark) support (which Cascade already has at the CSS level). It's about making the authoring ergonomics match what Tailwind provides with dark: and md: prefixes, but with type safety and without repeating variants at every call site.

Cascade already understands Tailwind modifier prefixes in its optimizer. Surfacing this as a first-class authoring API would be the natural next step.

3. Style composition and inheritance

ocaml.org has patterns like .btn, .btn-lg, .btn-secondary, .btn-ghost — a base style with variants. Cascade could support this as a first-class concept:

let btn = Style.define "btn"
  [ display Inline_flex
  ; border_radius (Rem 0.25)
  ; font_weight Normal
  ; font_size (Rem 1.125)
  ; color White
  ; white_space Nowrap
  ; transition_property Color
  ]

let btn_lg = Style.extend btn "btn-lg"
  [ padding_inline (Rem 1.75); height (Rem 4.) ]

let btn_secondary = Style.extend btn "btn-secondary"
  [ background_color (var secondary)
  ; border_color (var secondary)
  ]

Style.extend would generate CSS that relies on cascade ordering (or @layer) rather than duplicating all base declarations. This is how ocaml.org's CSS partials already work conceptually — Cascade could make it explicit and type-safe.

4. Tailwind compatibility and migration story

Cascade's optimizer already knows about Tailwind patterns. Building on that:

  • Parse @apply directives. A project migrating from Tailwind will have CSS partials full of @apply bg-primary border-primary text-white .... Being able to parse these and convert them to typed Cascade equivalents would be a powerful migration tool.

  • Ingest a Tailwind config. Projects like ocaml.org define their design tokens in tailwind.config.js (colors, spacing, breakpoints, fonts). A cascade CLI command that reads a Tailwind config and emits an OCaml module of typed design tokens would dramatically lower the migration barrier. In the case of ocaml.org, that config defines 30+ custom colors, 4 breakpoints, custom font stacks, and extended spacing — all of which would need to be manually transcribed otherwise.

  • Emit Tailwind-compatible class names (optionally). For incremental adoption — some pages migrated, some not — Cascade-generated CSS needs to coexist with Tailwind output without conflicts.

The migration story matters a lot: the realistic adoption path for an existing project is "use Cascade for new component styles, gradually migrate old ones" — not "rewrite everything."

5. Dream/EML integration (or generic HTML template integration)

A lightweight ppx or helper module for Dream's EML templates would close the loop:

(* Simple combinator approach *)
<div class="<%s Style.classes [changelog_title; container_fluid] %>">

(* Or a ppx for even tighter integration *)
<div class=[%css changelog_title; container_fluid]>

The value proposition isn't just "write CSS in OCaml." It's closing the loop between style definitions and style usage so the compiler can check both sides. Without this, there's a stringly-typed gap at the template boundary, and much of the type safety is lost. This doesn't have to be Dream-specific — any system that emits class name strings works.

6. Dead style detection

With a style registry, Cascade could provide a tool that scans EML/HTML templates and reports styles that are defined but never referenced — the equivalent of Tailwind's tree-shaking, but for named component classes. This would help keep the stylesheet lean as the site evolves.

7. CSS diffing in CI (immediate value, no migration needed)

The cssdiff tool already exists. Packaging it for easy CI integration (e.g., compare the CSS output of a PR branch against main and post a structured summary) would be immediately useful for projects like ocaml.org, even before any migration. This could be a "foot in the door" adoption path — use Cascade's tooling first, adopt its authoring API later.


The big picture

Cascade today is a CSS construction and processing toolkit. These recommendations would evolve it toward being a typed CSS component system for OCaml web applications — one that:

  1. Knows about design tokens, dark mode, and responsive variants as first-class authoring concepts (not just CSS features it can parse)
  2. Bridges the gap between style definitions and template usage with compile-time checking
  3. Offers a credible migration path from Tailwind (not "rewrite everything" but "adopt incrementally")
  4. Provides immediate value through tooling (diffing, dead style detection) even before full adoption

The underlying observation: the hard problem in OCaml web development isn't generating CSS — it's that styles are stringly-typed at the template boundary, duplicated across dark/responsive variants, and impossible to refactor safely. Cascade is uniquely positioned to solve this because it already has the typed AST and Tailwind awareness. What's missing is the developer-facing abstraction layer on top.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions