Styling
Tailwind with no node, theme tokens, variants, class merge, icons and transitions.
Gx uses Tailwind CSS v4 classes as you write them. It does not need node: gx downloads the Tailwind standalone binary for your computer, pins its version and its hash in gx.lock, and runs it.
How the stylesheet is built
- The compiler writes each class of the app to
.gx/classes.txt. It reads the staticclassvalues, theclass:directives, and the string literals in the Go files of a package that has.gxfiles. gx devandgx buildrun Tailwind overapp/theme.cssand that list.- The result is the Go file
gxstyles/styles_gx.go. The app serves it at/_gx/app.css, and the binary holds it.
Tailwind sees only a class that is a full static string in the source. A class that the program makes later is the diagnostic GX5003.
Theme tokens
app/theme.css holds the tokens in an @theme block. The token names are those of shadcn/ui: --background, --foreground, --primary, --muted, --border, --radius and the others. A theme from the shadcn/ui theme page works when you paste it.
Dark mode follows the .dark class and the system setting.
Variants
A variant is a Go type with constants. gx.Enum is a map from each constant to a static class string. gx lint reports a constant with no entry as GX5001.
gx.Cx joins class strings with the rules of tailwind-merge. A later class removes an earlier class that sets the same CSS property, so a caller can replace one class of a component.
// Package tag is a small label.
package tag
import "github.com/alternayte/gx"
// Tone is the colour of a tag.
type Tone string
// The tones of a tag.
const (
Neutral Tone = "neutral"
Success Tone = "success"
Danger Tone = "danger"
)
var toneClass = gx.Enum[Tone]{
Neutral: "bg-muted text-foreground",
Success: "bg-primary text-primary-foreground",
Danger: "bg-destructive text-white",
}
// class returns the classes of one tag. The class of the caller comes
// last, so it wins.
func (p TagProps) class() string {
return gx.Cx("inline-flex items-center rounded-md px-2 py-0.5 text-xs font-medium", toneClass[p.Tone], p.Class)
}package tag
props {
// Tone is the colour.
Tone Tone = Neutral
// Class adds classes to the element. A class here replaces a class of
// the tag that sets the same property.
Class string = ""
// Children is the text.
Children gx.Node
}
<span class={p.class()}>{p.Children}</span>package home
import "acme/ui/tag"
<gx.Head title="Tags" />
<div class="flex gap-2">
<tag.Tag>Draft</tag.Tag>
<tag.Tag tone={tag.Success}>Live</tag.Tag>
<tag.Tag tone={tag.Danger} class="px-4">Removed</tag.Tag>
</div>
<Counter label="Clicks" />bg-primary text-primary-foreground">Live
py-0.5 text-xs font-medium bg-destructive text-white px-4">RemovedThe third tag has px-4 and no px-2: the class of the caller replaced it.
Icons
go run ./cmd/gx icons pin lucide@1.0.0gx icons pin <set>@<version> reads an Iconify pack and writes one .gx component for each icon to ui/icons/<set>/. An icon is an inline SVG that uses currentColor. It has aria-hidden="true", and a label prop gives it role="img" and a name.
The binary holds only the icons that the app uses.
View transitions
gx.Transition[K](name) makes a typed transition name. transition={Hero(id)} on an element in two pages joins the two elements, and the browser moves one into the other.
package home
import "github.com/alternayte/gx"
// Hero joins the picture of an item in a list and on its page.
var Hero = gx.Transition[int]("hero")package home
props {
// ItemID is the item of the picture.
ItemID int
}
<img transition={Hero(p.ItemID)} src="/pictures/item.png" alt="Item" width="80" height="80" />- Two elements with one name in one template are the diagnostic GX5002.
- Navigation between pages with a shared layout runs in a view transition.
- An action runs its patch in a transition with
c.Patch(gx.ViewTransition, nodes...). - A reader who asks for reduced motion gets no transition.
Offline and mirrors
gx vendor stores the pinned downloads in .gx/vendor. A build then needs no network.
A [mirrors] table in gx.toml gives a different address for each download. Each download also follows HTTPS_PROXY.