Skip to content

theme.extend accepts only colors/screens/fontFamily in practice: no type, spacing, radius, shadow or keyframes scale, so a real app hand-writes 46 rules and 30 font sizes #20

Description

@glennmichael123

Evidence from loghq, a production SSR app on stx + crosswind (33 routes, 6.5k lines of templates). It ships three hand-written stylesheets next to the generated one, and they exist because the engine could not express what is in them.

Measured in the app

count
Hand-written rules in public/app-chrome.css 46
— pseudo-element rules 7
— attribute-state selectors ([data-on], [data-sel], [data-t]) 9
@keyframes 5
Custom properties in public/tokens.css 42
Distinct font sizes across the stylesheets ~30

config/crosswind.ts registers theme.extend with colors, screens and fontFamily only. Everything dimensional — type, spacing, radius, shadow — is hand-typed per element as an arbitrary value, so there is no scale to be consistent with. The result is 30 font sizes in an app with maybe eight real roles.

The gaps, each behind a concrete workaround

  1. No type / spacing / radius / shadow scale. theme.extend.fontSize, .spacing, .borderRadius, .boxShadow are not usable, so every size is text-[13px] / rounded-[10px] / shadow-[0_24px_60px_rgba(0,0,0,0.22)]. The app has exactly three shadows, all hand-written, none theme-aware.

  2. No custom @keyframes / animation tokens. Five @keyframes (dz-fade, dz-pop, loghq-pulse, …) live in a hand-written sheet because there is no theme.keyframes / theme.animation. Animations are then applied with arbitrary properties: [animation:loghq-pulse_1.4s_ease-in-out_infinite].

  3. No plugin API that can emit a selector. The public rules entry cannot produce a pseudo-element or an attribute-state selector, so .tgl::after, .tgl[data-on='1'], .seg[data-sel='1'], .theme-btn icon swapping — all state-driven UI — are stranded in hand-written CSS.

  4. Dynamic class names are silently dropped. class="badge-{{ c.type }}" emits badge-slack / badge-discord and generates nothing. There is no safelist pattern (regex/glob) to cover a class assembled at render time. This shipped as an unstyled badge in production and was invisible to review, because the class name does not exist anywhere to grep for.

  5. Arbitrary property + variant does not combine. dark:[background-image:…] for a themed select chevron had to be written by hand.

  6. Arbitrary/custom max-width variants (max-[900px]:, max-bp-md:) are unavailable, so the marketing sheet keeps its own media queries — and the app surface ended up with zero width media queries at all.

Why it compounds

Because the scale is not registered, nothing enforces it — the string hover:border-[color-mix(in_srgb,var(--accent)_45%,var(--border))] appears 20 times verbatim across the views. A registered theme plus a working plugin API would delete most of app-chrome.css and let the design system be checked rather than retyped.

Related: #19 (responsive variants dropped on SPA fragment swap) is the same class of problem seen from the delivery side.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions