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.cssis the one entry your app imports.foundation.cssowns accessibility and behavior-critical mechanics such as hidden states, positioning, and interaction geometry.theme.cssowns semantic light/dark variables, including sidebar, status, overlay, contrast, chart, and radius tokens, plus the mode-independent--font-sans/--font-headingtypography pair.style.cssowns 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): yourh-12replaces the component's own height utility instead of conflicting with it. - Rules in
style.csslive 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:
- Change the CSS entry from
web/gsxui.csstoweb/gsxui/index.css. - Review the four-file diff, then run
gsxui init --overwrite. - Run
gsxui add <component> --overwritefor each vendored component you want to refresh. - Replace project
data-slotselectors with exact presence selectors such as[data-gsxui-slot-button].