Generator (scripts/generate-htmx-css.ts): track `viaVariants` per slot so
slots that compose another component's variant system (e.g. ToggleGroupItem
via toggleVariants) inherit the referenced CVA's base + variant rules under
their own selector. Previously toggle-group-item's CSS contained only its
override classes, shipping with no padding/height/hover/active state.
Toggle (components/ui/toggle.tsx):
- data-[state=on] now uses bg-primary (orange) instead of bg-accent (grey),
matching every other "commit" affordance in the palette.
- Horizontal padding aligned with Button: px-4/px-3/px-6 per size, plus
has-[>svg]:px-* for icon-only toggles.
ToggleGroup (components/ui/toggle-group.tsx): drop min-w-0 flex-1 shrink-0
from the item override. Items now size to content instead of being clamped
into equal narrow columns where longer labels overflowed the bg box.
Showcase: add ToggleGroup section to the React page (component-matrix.tsx)
and 1:1 HTMX mirror (public/htmx.html) with a new JS bridge branch for
single/multi-select. compare-all.sh extended with the new section; 22/22
pass at ≥99.97%.
Docs: GAPS.md captures the generator gap, overflow root cause, color
rationale, and padding parity with before/after numbers.
7.5 KiB
HTMX CSS Generator — Known Gaps & Fixes
Running log of edge cases discovered while consuming dist/greyhaven.htmx.css
in framework-agnostic (HTMX / Go html/template / etc.) projects. Each entry
captures: the bug, its root cause, and the fix. Keep this file updated when
new gaps surface so consumers have a single place to check.
Fixed
2026-04-24 — toggle-group-item missing Toggle base styles
Symptom (consumer-side): data-slot="toggle-group-item" renders as
unstyled inline text — no padding, no height, no border, and data-state="on"
produces no visual change.
Root cause: In React, ToggleGroupItem composes toggleVariants() with a
small set of segmented-group overrides:
className={cn(
toggleVariants({ variant, size }), // base + variant + size classes
'min-w-0 flex-1 shrink-0 rounded-none ...'
)}
The generator's extractSlot only captured the first string-literal arg of
cn(...) — the segmented overrides — and never followed the
toggleVariants(...) call to pull in Toggle's base, variant, and size rules.
As a result, the emitted [data-slot="toggle-group-item"] had no padding,
hover, disabled, or — critically — the data-[state=on]:bg-accent data-[state=on]:text-accent-foreground rule that drives the pressed state.
Fix (scripts/generate-htmx-css.ts): SlotExtract now carries a
viaVariants: string[] field. When processing className={cn(xVariants(...), 'literal')}, the extractor records every *Variants call it sees. The
emitter then merges each referenced CVA's base + variant rules under the
slot's own selector, so a slot that composes another component's variant
system inherits its full rule set.
Generated output before:
[data-slot="toggle-group-item"] { /* only overrides */
min-w-0 flex-1 ... rounded-none first:rounded-l-md ...
}
Generated output after:
[data-slot="toggle-group-item"] { /* full Toggle base + item overrides */
inline-flex items-center ... data-[state=on]:bg-accent ...
min-w-0 flex-1 ... rounded-none first:rounded-l-md ...
}
[data-slot="toggle-group-item"][data-variant="outline"] { /* inherited variant */ }
[data-slot="toggle-group-item"][data-size="sm"] { /* inherited size */ }
Consumer impact: existing HTMX demos do not use toggle-group-item, so no
screenshot-diff regression risk. Consumers previously working around this by
using data-slot="toggle" on each item (with manual rounded-none / border
overrides) can switch to the clean toggle-group-item form.
2026-04-24 — Toggle active state uses primary (orange), not accent (grey)
Symptom: Design review flagged the active-state grey (bg-accent) as too
muted — users expected the brand orange for selected items in a segmented
control, matching every other "selected" affordance in the system (primary
button, active nav link, focus ring).
Fix: components/ui/toggle.tsx — toggleVariants base class swapped from
data-[state=on]:bg-accent data-[state=on]:text-accent-foreground to
data-[state=on]:bg-primary data-[state=on]:text-primary-foreground. This
propagates to ToggleGroupItem automatically (composes toggleVariants) and
to the generated [data-slot="toggle"] and [data-slot="toggle-group-item"]
rules after pnpm htmx-css:build && pnpm htmx-demo:build.
Rationale: Greyhaven's palette reserves accent for hover hints and
subtle surface shifts; primary is the single brand-accent color, used
wherever a choice is committed. Selected-state for Toggle/ToggleGroup now
matches that convention.
2026-04-24 — ToggleGroupItem overflow: text escapes the bg box
Symptom: With segmented labels of uneven length ("System" / "Light" / "Dark"), the longer label's text rendered outside its button's background rectangle. Selected-state highlight appeared narrower than the text it was supposed to cover, and hover state was clipped for longer labels.
Root cause: ToggleGroupItem layered 'min-w-0 flex-1 shrink-0' on top
of toggleVariants' size-based min-w-*. Because tailwind-merge keeps the
later min-w-0, each item was allowed to shrink below its content. Combined
with flex: 1 1 0% and w-fit on the group, items ended up forced to equal
narrow columns sized to container_width / N — which for the "System" case
was ~37px, well below the text's ~45px intrinsic width. whitespace-nowrap
then let the text bleed out of the button's layout box.
Fix: components/ui/toggle-group.tsx — dropped min-w-0 flex-1 shrink-0
from ToggleGroupItem's override string. Items now size to their content
(respecting the min-w-* floor from toggleVariants), the group's w-fit
sums them up, and padding is symmetric. Unequal-width items are the
intentional result (a segmented "System/Light/Dark" now shows "System" wider
than "Light"/"Dark"); if a consumer specifically wants equal-width columns,
they can re-apply flex-1 basis-0 on each item via className.
Consumer impact: no regressions in the existing 21 sections; the new
toggle-group section passes at 99.98%.
2026-04-24 — Toggle horizontal padding aligned with Button
Symptom: Side-by-side, a Toggle (or ToggleGroupItem) looked cramped
compared to a regular Button of the same size — the active-state orange
bg sat tight against the label, while Button had comfortable breathing room
on both sides. Perceived as a visual-rhythm bug across any UI that mixed the
two in the same view.
Root cause: Toggle's cva had roughly half the horizontal padding of
Button per size:
| size | Button | Toggle (old) |
|---|---|---|
| default | px-4 (16px) |
px-2 (8px) |
| sm | px-3 (12px) |
px-1.5 (6px) |
| lg | px-6 (24px) |
px-2.5 (10px) |
Fix: components/ui/toggle.tsx — toggleVariants.size.* horizontal
padding now matches Button exactly. Also added has-[>svg]:px-* for
icon-only toggles, mirroring Button's affordance. min-w-* kept as a floor
so very short labels (A, B) still render as balanced pills.
size: {
default: 'h-9 px-4 min-w-9 has-[>svg]:px-3',
sm: 'h-8 px-3 min-w-8 has-[>svg]:px-2.5',
lg: 'h-10 px-6 min-w-10 has-[>svg]:px-4',
},
Known (not yet fixed)
(Add entries here as they are discovered. Template:
### YYYY-MM-DD — <slot or behavior>
**Symptom:** ...
**Root cause:** ...
**Workaround:** ...
**Proposed fix:** ...
)
Radix primitive-only components (AlertDialogAction, AlertDialogCancel)
Symptom: These components have no static data-slot in their JSX — they
wrap <AlertDialogPrimitive.Action> which sets data-slot at runtime. The
AST extractor never sees them, so no CSS is emitted.
Workaround: Consumers should add data-slot="button" on the
corresponding HTML element. The visual contract matches Button exactly
(cn(buttonVariants(), className) in React).
Proposed fix: none yet — requires either parsing the Radix primitive
source, or adding explicit data-slot attributes upstream.
Interactive components without a built-in JS bridge
Symptom: data-slot="select-trigger", data-slot="tooltip-content",
data-slot="dialog-content", data-slot="popover-content" emit the CSS for
their open/closed states, but nothing drives them.
Workaround: Consumers must implement open/close + positioning themselves
(Alpine.js, plain JS, HTMX swap). public/htmx.html ships a ~60-line vanilla
bridge for checkbox / switch / tabs; select / tooltip / dialog remain
consumer concerns.
Proposed fix: ship an opt-in greyhaven.htmx.js companion with pointer
positioning (e.g. via Floating UI) and focus management.