Replace one-off hover colors with hover-neutral/hover-destructive tokens so all interactive elements respond consistently to theme changes. Fix hover-neutral-bg token value (was identical to card surface, making hover invisible on card backgrounds) to a semi-transparent primary tint. Switch turn nav buttons to outline variant for visible hover feedback. Convert HP popover damage/heal to plain buttons to avoid ghost variant hover conflict with tailwind-merge. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
11 KiB
date, git_commit, branch, topic, tags, status
| date | git_commit | branch | topic | tags | status | |||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 2026-03-13T15:35:07.699570+00:00 | bd39808000 |
main | CSS class usage, button categorization, and hover effects across all components |
|
complete |
Research: CSS Class Usage, Button Categorization, and Hover Effects
Research Question
How are CSS classes used across all components? How are buttons categorized — are there primary and secondary buttons? What hover effects exist, and are they unified?
Summary
The project uses Tailwind CSS v4 with a custom dark theme defined in index.css via @theme. All class merging goes through a cn() utility (clsx + tailwind-merge). Buttons are built on a shared Button component using class-variance-authority (CVA) with three variants: default (primary), outline, and ghost. Hover effects are partially unified through semantic color tokens (hover-neutral, hover-action, hover-destructive) defined in the theme, but several components use one-off hardcoded hover colors that bypass the token system.
Detailed Findings
Theme System (index.css)
All colors are defined as CSS custom properties via Tailwind v4's @theme directive (index.css:3-26):
| Token | Value | Purpose |
|---|---|---|
--color-background |
#0f172a |
Page background |
--color-foreground |
#e2e8f0 |
Default text |
--color-muted |
#64748b |
Subdued elements |
--color-muted-foreground |
#94a3b8 |
Secondary text |
--color-card |
#1e293b |
Card/panel surfaces |
--color-border |
#334155 |
Borders |
--color-primary |
#3b82f6 |
Primary actions (blue) |
--color-accent |
#3b82f6 |
Accent (same as primary) |
--color-destructive |
#ef4444 |
Destructive actions (red) |
Hover tokens (semantic layer for hover states):
| Token | Resolves to | Usage |
|---|---|---|
hover-neutral |
primary (blue) |
Text color on neutral hover |
hover-action |
primary (blue) |
Text color on action hover |
hover-destructive |
destructive (red) |
Text color on destructive hover |
hover-neutral-bg |
card (slate) |
Background on neutral hover |
hover-action-bg |
muted |
Background on action hover |
hover-destructive-bg |
transparent |
Background on destructive hover |
Button Component (components/ui/button.tsx)
Uses CVA with three variants and three sizes:
Variants:
| Variant | Base styles | Hover |
|---|---|---|
default (primary) |
bg-primary text-primary-foreground |
hover:bg-primary/90 |
outline |
border border-border bg-transparent |
hover:bg-hover-neutral-bg hover:text-hover-neutral |
ghost |
(no background/border) | hover:bg-hover-neutral-bg hover:text-hover-neutral |
Sizes:
| Size | Classes |
|---|---|
default |
h-9 px-4 py-2 |
sm |
h-8 px-3 text-xs |
icon |
h-8 w-8 |
All variants share: rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary disabled:pointer-events-none disabled:opacity-50.
There is no "secondary" variant — the outline variant is the closest equivalent.
Composite Button Components
ConfirmButton (components/ui/confirm-button.tsx):
- Wraps
Button variant="ghost" size="icon" - Default state:
hover:text-hover-destructive(uses token) - Confirming state:
bg-destructive text-primary-foreground animate-confirm-pulse hover:bg-destructive hover:text-primary-foreground
OverflowMenu (components/ui/overflow-menu.tsx):
- Trigger:
Button variant="ghost" size="icon"withtext-muted-foreground hover:text-hover-neutral - Menu items: raw
<button>elements withhover:bg-muted/20(not using the token system)
Button Usage Across Components
| Component | Button type | Variant/Style |
|---|---|---|
action-bar.tsx:556 |
<Button type="submit"> |
default (primary) — "Add" |
action-bar.tsx:561 |
<Button type="button"> |
default (primary) — "Roll all" |
turn-navigation.tsx:25,54 |
<Button size="icon"> |
default — prev/next turn |
turn-navigation.tsx:47 |
<ConfirmButton> |
ghost+destructive — clear encounter |
source-fetch-prompt.tsx:91 |
<Button size="sm"> |
default — "Load" |
source-fetch-prompt.tsx:106 |
<Button size="sm" variant="outline"> |
outline — "Upload file" |
bulk-import-prompt.tsx:31,45,106 |
<Button size="sm"> |
default — "Done"/"Load All" |
source-manager.tsx:50 |
<Button size="sm" variant="outline"> |
outline — "Clear all" |
hp-adjust-popover.tsx:112 |
<Button variant="ghost" size="icon"> |
ghost + custom red — damage |
hp-adjust-popover.tsx:124 |
<Button variant="ghost" size="icon"> |
ghost + custom green — heal |
player-management.tsx:67 |
<Button> |
default — "Create first player" |
player-management.tsx:113 |
<Button variant="ghost"> |
ghost — "Add player" |
create-player-modal.tsx:177 |
<Button variant="ghost"> |
ghost — "Cancel" |
create-player-modal.tsx:180 |
<Button type="submit"> |
default — "Save"/"Create" |
combatant-row.tsx:625 |
<ConfirmButton> |
ghost+destructive — remove combatant |
Raw <button> elements (not using the Button component):
action-bar.tsx— suggestion items, count increment/decrement, browse toggle, custom add (all inline-styled)combatant-row.tsx— editable name, HP display, AC, initiative, concentration togglestat-block-panel.tsx— fold/close/pin/unpin buttonscondition-picker.tsx— condition itemscondition-tags.tsx— condition tags, add condition buttontoast.tsx— dismiss buttonplayer-management.tsx— close modal, edit playercreate-player-modal.tsx— close modalcolor-palette.tsx— color swatchesicon-grid.tsx— icon options
Hover Effects Inventory
Using semantic tokens (unified):
| Hover class | Meaning | Used in |
|---|---|---|
hover:bg-hover-neutral-bg |
Neutral background highlight | button.tsx (outline/ghost), action-bar.tsx, condition-picker.tsx, condition-tags.tsx |
hover:text-hover-neutral |
Text turns primary blue | button.tsx (outline/ghost), action-bar.tsx, combatant-row.tsx, stat-block-panel.tsx, ac-shield.tsx, toast.tsx, overflow-menu.tsx, condition-tags.tsx |
hover:text-hover-action |
Action text (same as neutral) | action-bar.tsx (overflow trigger) |
hover:text-hover-destructive |
Destructive text turns red | confirm-button.tsx, source-manager.tsx |
hover:bg-hover-destructive-bg |
Destructive background (transparent) | source-manager.tsx |
One-off / hardcoded hover colors (NOT using tokens):
| Hover class | Used in | Context |
|---|---|---|
hover:bg-primary/90 |
button.tsx (default variant) | Primary button darken |
hover:bg-accent/20 |
action-bar.tsx | Suggestion highlight, custom add |
hover:bg-accent/40 |
action-bar.tsx | Count +/- buttons, confirm queued |
hover:bg-muted/20 |
overflow-menu.tsx | Menu item highlight |
hover:bg-red-950 |
hp-adjust-popover.tsx | Damage button |
hover:text-red-300 |
hp-adjust-popover.tsx | Damage button text |
hover:bg-emerald-950 |
hp-adjust-popover.tsx | Heal button |
hover:text-emerald-300 |
hp-adjust-popover.tsx | Heal button text |
hover:text-foreground |
player-management.tsx, create-player-modal.tsx, icon-grid.tsx | Close/edit buttons |
hover:bg-background/50 |
player-management.tsx | Player row hover |
hover:bg-card |
icon-grid.tsx | Icon option hover |
hover:border-hover-destructive |
source-manager.tsx | Clear all button border |
hover:scale-110 |
color-palette.tsx | Color swatch enlarge |
hover:bg-destructive |
confirm-button.tsx (confirming state) | Maintain red bg on hover |
hover:text-primary-foreground |
confirm-button.tsx (confirming state) | Maintain white text on hover |
Hover unification assessment
The hover token system (hover-neutral, hover-action, hover-destructive) provides a consistent pattern for the most common interactions. The Button component's outline and ghost variants use these tokens, and many inline buttons in action-bar, combatant-row, stat-block-panel, and condition components also use them.
However, there are notable gaps:
- HP adjust popover uses hardcoded red/green colors (
red-950,emerald-950) instead of tokens - Overflow menu items use
hover:bg-muted/20instead ofhover:bg-hover-neutral-bg - Player management modals use
hover:text-foregroundandhover:bg-background/50instead of the semantic tokens - Action-bar suggestion items use
hover:bg-accent/20andhover:bg-accent/40— accent-specific patterns not in the token system - Icon grid and color palette use their own hover patterns (
hover:bg-card,hover:scale-110)
Code References
apps/web/src/index.css:3-26— Theme color definitions including hover tokensapps/web/src/components/ui/button.tsx:1-38— Button component with CVA variantsapps/web/src/components/ui/confirm-button.tsx:93-115— ConfirmButton with destructive hover statesapps/web/src/components/ui/overflow-menu.tsx:38-72— OverflowMenu with non-token hoverapps/web/src/components/hp-adjust-popover.tsx:117-129— Hardcoded red/green hover colorsapps/web/src/components/action-bar.tsx:80-188— Mixed token and accent-based hoversapps/web/src/components/combatant-row.tsx:147-629— Inline buttons with token hoversapps/web/src/components/player-management.tsx:58-98— Non-token hover patternsapps/web/src/components/stat-block-panel.tsx:55-109— Consistent token usageapps/web/src/lib/utils.ts:1-5—cn()utility (clsx + twMerge)
Architecture Documentation
The styling architecture follows this pattern:
- Theme layer:
index.cssdefines all color tokens via@theme, including semantic hover tokens - Component layer:
Button(CVA) provides the shared button abstraction with three variants - Composite layer:
ConfirmButtonandOverflowMenuwrapButtonwith additional behavior - Usage layer: Components use either
Buttoncomponent or raw<button>elements with inline Tailwind classes
The cn() utility from lib/utils.ts is used in 9+ components for conditional class merging.
Custom animations are defined in index.css via @keyframes + @utility pairs: slide-in-right, confirm-pulse, settle-to-bottom, rise-to-center, slide-down-in, slide-up-out, concentration-pulse.
Open Questions
- The
hover-actionandhover-action-bgtokens are defined but rarely used —hover-actionappears only once inaction-bar.tsx:565. Is this intentional or an incomplete migration? - The
accentcolor (#3b82f6) is identical toprimary— are they intended to diverge in the future, or is this redundancy? - Should the hardcoded HP adjust colors (red/emerald) be promoted to theme tokens (e.g.,
hover-damage,hover-heal)?