Theming

A theme changes CSS variables only: recolor every component, light and dark, by editing one file. Component markup never changes.

The four CSS files

gsxui init vendors four CSS files into web/gsxui/:

@import "tailwindcss";
@import "tw-animate-css";
@import "./foundation.css";
@import "./theme.css";
@import "./style.css";
  • index.css is the one entry your app imports.
  • foundation.css owns accessibility and behavior-critical mechanics such as hidden states, positioning, and interaction geometry.
  • theme.css owns semantic light/dark variables, including sidebar, status, overlay, contrast, chart, and radius tokens, plus the mode-independent --font-sans/--font-headingtypography pair.
  • style.css owns the default style's component presentation rules: density, borders, shadows, typography.

To recolor the default style, edit or replace theme.css only. The other three files stay untouched.

Edit semantic variables

The variables are shadcn-compatible :root and .dark blocks — nothing else lives in the file. The theme editor imports and exports exactly this file, so you can build a theme visually and download the result.

/* web/gsxui/theme.css — semantic variables only */
:root {
  --radius: 0.5rem;                  /* squarer corners, every component */
  --primary: oklch(0.55 0.2 265);    /* brand blue instead of near-black */
  --primary-foreground: oklch(0.98 0 0);
}

.dark {
  --primary: oklch(0.72 0.15 265);
}

Stable component markers

To restyle a part from your own CSS, target its marker: every semantic part carries one baredata-gsxui-slot-<name> attribute. A composed part carries every role it plays — AlertDialog's action renders a Button whose element has both data-gsxui-slot-buttonand data-gsxui-slot-alert-dialog-action — so each role stays targetable:

/* Each semantic role has its own presence attribute. */
[data-gsxui-slot-button] {
  /* project-specific addition */
}

/* Composed parts forward each role as another attribute. */
[data-gsxui-slot-alert-dialog-action] {
  /* action-specific addition */
}

Match on bare presence only — value and operator forms are not part of the contract.

Caller utilities win

Pass an ordinary utility class and it wins — no !important:

<ui.Button class="h-12">Tall</ui.Button>

Two mechanisms make that hold:

  • Utilities a component carries in its own markup are settled by the vendored class merger (ui/merge/merge.go): your h-12 replaces the component's own height utility instead of conflicting with it.
  • Rules in style.css live in @layer components, while your class lands in Tailwind's later @layer utilities — the later layer wins regardless of specificity.

Fallthrough attributes carry ids, ARIA, data, and HTMX attributes to the rendered element the same way:

<ui.Button
	id="submit"
	aria-label="Submit the form"
	data-testid="submit-btn"
	hx-post="/submit"
	hx-target="#result"
>
	Submit
</ui.Button>

Behaviour roles are opt-in data attributes: any element becomes a dialog trigger by carrying data-gsxui-dialog-trigger. The family's own Trigger components need no role attribute — their slot marker implies it:

<ui.Dialog>
	<ui.Button variant="outline" data-gsxui-dialog-trigger>
		Open
	</ui.Button>
	<ui.DialogContent>
		...
	</ui.DialogContent>
</ui.Dialog>

Breaking migration

Only projects from before the four-file split — a single web/gsxui.css entry and data-slot markers — need this. There is no compatibility layer; migrate in one pass:

  1. Change the CSS entry from web/gsxui.css to web/gsxui/index.css.
  2. Review the four-file diff, then run gsxui init --overwrite.
  3. Run gsxui add <component> --overwrite for each vendored component you want to refresh.
  4. Replace project data-slot selectors with exact presence selectors such as [data-gsxui-slot-button].