Skip to content
Gx
GitHub

Starlight shell

The Starlight docs shell: fixed header, version label, search, social icons, theme select, sidebar, table of contents, hero, page links and 404.

<starlight.Header site={starlight.Config{
  Title:    "Deedbox",
  Version:  "latest",
  Links:    []starlight.Link{{Label: "GitHub", Href: gx.URL("https://example.com/gx"), Icon: "github"}},
  EditBase: "https://example.com/edit/main/content/docs/",
}} />

Installation

Run the command in the app module.

Terminal
gx add starlight-shell

The command also installs docs and starlight.

The command writes these files.

File Path in the app
Header.fixtures.go ui/starlight-shell/Header.fixtures.go
Header.gx ui/starlight-shell/Header.gx
Hero.fixtures.go ui/starlight-shell/Hero.fixtures.go
Hero.gx ui/starlight-shell/Hero.gx
MobileToc.fixtures.go ui/starlight-shell/MobileToc.fixtures.go
MobileToc.gx ui/starlight-shell/MobileToc.gx
NotFound.fixtures.go ui/starlight-shell/NotFound.fixtures.go
NotFound.gx ui/starlight-shell/NotFound.gx
PageFooter.fixtures.go ui/starlight-shell/PageFooter.fixtures.go
PageFooter.gx ui/starlight-shell/PageFooter.gx
Pagination.fixtures.go ui/starlight-shell/Pagination.fixtures.go
Pagination.gx ui/starlight-shell/Pagination.gx
SearchDialog.fixtures.go ui/starlight-shell/SearchDialog.fixtures.go
SearchDialog.gx ui/starlight-shell/SearchDialog.gx
Shell.fixtures.go ui/starlight-shell/Shell.fixtures.go
Shell.gx ui/starlight-shell/Shell.gx
Sidebar.fixtures.go ui/starlight-shell/Sidebar.fixtures.go
Sidebar.gx ui/starlight-shell/Sidebar.gx
SidebarItem.fixtures.go ui/starlight-shell/SidebarItem.fixtures.go
SidebarItem.gx ui/starlight-shell/SidebarItem.gx
SiteTitle.fixtures.go ui/starlight-shell/SiteTitle.fixtures.go
SiteTitle.gx ui/starlight-shell/SiteTitle.gx
SocialIcons.fixtures.go ui/starlight-shell/SocialIcons.fixtures.go
SocialIcons.gx ui/starlight-shell/SocialIcons.gx
ThemeSelect.fixtures.go ui/starlight-shell/ThemeSelect.fixtures.go
ThemeSelect.gx ui/starlight-shell/ThemeSelect.gx
Toc.fixtures.go ui/starlight-shell/Toc.fixtures.go
Toc.gx ui/starlight-shell/Toc.gx
fixtures.go ui/starlight-shell/fixtures.go
helpers.go ui/starlight-shell/helpers.go
nav.go ui/starlight-shell/nav.go

Install docs and starlight first.

Copy each file to its path in the app. Change each import of a registry package to the path of that package in the app.

ui/starlight-shell/Header.fixtures.go
package starlight

import "github.com/alternayte/gx"

var HeaderFixtures = gx.Fixtures[HeaderProps]{
	"Default": {Site: fixtureSite},
}
ui/starlight-shell/Header.gx
package starlight

props {
  // Site is the site-wide data: the title, the versions and the social links.
  Site Config
}

<header class="sl-header">
  <div class="sl-header-inner">
    <div class="sl-title-wrapper">
      <SiteTitle site={p.Site} />
    </div>
    <div class="sl-header-search">
      <button type="button" class="sl-search-button" data-gx-search-open aria-label="Search" aria-keyshortcuts="Control+K" aria-haspopup="dialog">
        {icon("magnifier", "")}
        <span aria-hidden="true">Search</span>
        <kbd><kbd data-gx-mod-key>Ctrl</kbd><kbd>K</kbd></kbd>
      </button>
    </div>
    <div class="sl-right-group">
      <SocialIcons links={p.Site.Links} />
      <ThemeSelect />
    </div>
  </div>
</header>
ui/starlight-shell/Hero.fixtures.go
package starlight

import "github.com/alternayte/gx"

var HeroFixtures = gx.Fixtures[HeroProps]{
	"Default": {Title: "Deedbox", Tagline: "Event-source part of your app."},
	"Title":   {Title: "Deedbox"},
}
ui/starlight-shell/Hero.gx
package starlight

props {
  // Title is the heading of the hero.
  Title   string
  // Tagline is the text below the title. Empty omits it.
  Tagline string = ""
  // Actions is the row of link buttons below the tagline. Nil omits it.
  Actions gx.Node = nil
  // Image is the picture beside the text. Nil omits it.
  Image   gx.Node = nil
}

<div class="sl-hero">
  if p.Image != nil {
    <div class="sl-hero-image">{p.Image}</div>
  }
  <div class="sl-stack">
    <div class="sl-copy">
      <h1 id="_top">{p.Title}</h1>
      if p.Tagline != "" {
        <div class="sl-tagline">{p.Tagline}</div>
      }
    </div>
    if p.Actions != nil {
      <div class="sl-actions">{p.Actions}</div>
    }
  </div>
</div>
ui/starlight-shell/MobileToc.fixtures.go
package starlight

import "github.com/alternayte/gx"

var MobileTocFixtures = gx.Fixtures[MobileTocProps]{
	"Default": {Headings: fixtureHeadings},
}
ui/starlight-shell/MobileToc.gx
package starlight

props {
  // Headings holds the entries of the table of contents, below the entry "Overview" for the
  // top of the page.
  Headings []Heading
}

<div class="sl-mobile-toc">
  <nav data-gx-toc aria-labelledby="sl-on-this-page-mobile">
    <details data-gx-toc-menu>
      <summary id="sl-on-this-page-mobile">
        <span class="sl-toggle">
          On this page
          {icon("right-caret", "sl-caret")}
        </span>
        <span class="sl-display-current" data-gx-toc-current></span>
      </summary>
      <div class="sl-dropdown">
        <ul class="sl-toc-list">
          <li>
            <a href="#_top" data-gx-toc-target="_top" data-level="2"><span>Overview</span></a>
          </li>
          for _, h := range p.Headings {
            <li>
              <a href={gx.URL("#" + h.ID)} data-gx-toc-target={h.ID} data-level={h.Level}><span>{h.Text}</span></a>
            </li>
          }
        </ul>
      </div>
    </details>
  </nav>
</div>
ui/starlight-shell/NotFound.fixtures.go
package starlight

import "github.com/alternayte/gx"

var NotFoundFixtures = gx.Fixtures[NotFoundProps]{
	"Default": {Site: fixtureSite},
}
ui/starlight-shell/NotFound.gx
package starlight

props {
  // Site is the site-wide data: the title, the versions and the social links.
  Site    Config
  // Title is the heading of the page.
  Title   string = "404"
  // Message is the text below the title.
  Message string = "Page not found. Check the URL or try using the search bar."
}

<Shell site={p.Site} nav={Nav{}} page={Page{Title: p.Title, Splash: true}} hero={Hero(HeroProps{Title: p.Title, Tagline: p.Message})} children={gx.Text("")} />
ui/starlight-shell/PageFooter.fixtures.go
package starlight

import "github.com/alternayte/gx"

var PageFooterFixtures = gx.Fixtures[PageFooterProps]{
	"Default": {Site: fixtureSite, Page: fixturePage},
}
ui/starlight-shell/PageFooter.gx
package starlight

props {
  // Site is the site-wide data. Its EditBase builds the edit link; empty hides the link.
  Site Config
  // Page is the data of the current page: its path, its date and its previous and next
  // items.
  Page Page
}

<footer class="sl-footer">
  <div class="sl-meta">
    if p.Site.EditURL(p.Page) != "" {
      <a class="sl-edit-link" href={gx.URL(p.Site.EditURL(p.Page))}>
        {icon("pencil", "")}
        Edit page
      </a>
    }
    if p.Page.Updated != "" {
      <p data-gx-updated>Last updated: <time>{p.Page.Updated}</time></p>
    }
  </div>
  <Pagination prev={p.Page.Prev} next={p.Page.Next} />
</footer>
ui/starlight-shell/Pagination.fixtures.go
package starlight

import "github.com/alternayte/gx"

var PaginationFixtures = gx.Fixtures[PaginationProps]{
	"Both": {Prev: fixturePrev, Next: fixtureNext},
	"Next": {Next: fixtureNext},
	"Prev": {Prev: fixturePrev},
}
ui/starlight-shell/Pagination.gx
package starlight

props {
  // Prev is the previous sidebar item. Nil omits the link.
  Prev *NavItem
  // Next is the next sidebar item. Nil omits the link.
  Next *NavItem
}

<div class="sl-pagination">
  if p.Prev != nil {
    <a href={p.Prev.Href} rel="prev">
      {icon("left-arrow", "")}
      <span>Previous<br /><span class="sl-link-title">{p.Prev.Label}</span></span>
    </a>
  }
  if p.Next != nil {
    <a href={p.Next.Href} rel="next">
      {icon("right-arrow", "")}
      <span>Next<br /><span class="sl-link-title">{p.Next.Label}</span></span>
    </a>
  }
</div>
ui/starlight-shell/SearchDialog.fixtures.go
package starlight

import "github.com/alternayte/gx"

var SearchDialogFixtures = gx.Fixtures[SearchDialogProps]{
	"Default": {},
}
ui/starlight-shell/SearchDialog.gx
package starlight

<dialog id="gx-search" class="sl-search-dialog" data-gx-search data-gx-search-src="/pagefind/pagefind.js" aria-label="Search">
  <div class="sl-search-frame">
    <form method="get" action="/search" class="sl-search-form" data-gx-search-form>
      <div class="sl-search-field">
        {icon("magnifier", "")}
        <input type="search" name="q" class="sl-search-input" placeholder="Search" aria-label="Search" data-gx-search-input autocomplete="off" />
      </div>
      <button type="button" class="sl-search-close" data-gx-search-close>Cancel</button>
    </form>
    <div class="sl-search-results" data-gx-search-results></div>
  </div>
</dialog>
ui/starlight-shell/Shell.fixtures.go
package starlight

import "github.com/alternayte/gx"

var ShellFixtures = gx.Fixtures[ShellProps]{
	"Default": {Site: fixtureSite, Nav: fixtureNav, Page: fixturePage, Children: gx.Text("Body.")},
	"Splash": {
		Site:     fixtureSite,
		Nav:      fixtureNav,
		Page:     Page{Title: "Deedbox", Path: "/", Splash: true},
		Hero:     Hero(HeroProps{Title: "Deedbox", Tagline: "Event-source part of your app."}),
		Children: gx.Text("Body."),
	},
}
ui/starlight-shell/Shell.gx
package starlight

props {
  // Site is the site-wide data: the title, the versions and the social links.
  Site     Config
  // Nav is the sidebar model: the groups and their items.
  Nav      Nav
  // Page is the data of the current page: its title, its path, its headings and its previous
  // and next items. A page with Splash has no sidebar and no table of contents.
  Page     Page
  // Hero is the hero of the page. It takes the place of the page title. Nil shows the title.
  Hero     gx.Node = nil
  // Children is the content of the page.
  Children gx.Node
}

<gx.Head htmlClass={RootClass(p.Page)} />
<a class="sl-skip-link" href="#_top">Skip to content</a>
<div class="sl-page">
  <Header site={p.Site} />
  if !p.Page.Splash {
    <Sidebar site={p.Site} nav={p.Nav} path={p.Page.Path} />
  }
  <div class="sl-main-frame">
    <div class="sl-two-column">
      if len(p.Page.ShownTOC()) > 0 || !p.Page.Splash {
        <aside class="sl-right-sidebar-container">
          <div class="sl-right-sidebar">
            <MobileToc headings={p.Page.ShownTOC()} />
            <div class="sl-right-sidebar-panel">
              <div class="sl-toc-container">
                <Toc headings={p.Page.ShownTOC()} />
              </div>
            </div>
          </div>
        </aside>
      }
      <div class="sl-main-pane">
        <main class="sl-main" data-pagefind-body>
          if p.Hero != nil {
            <div class="sl-content-panel">
              <div class="sl-container">
                {p.Hero}
                <div class="sl-markdown-content gx-content">{p.Children}</div>
                <PageFooter site={p.Site} page={p.Page} />
              </div>
            </div>
          } else {
            <div class="sl-content-panel">
              <div class="sl-container">
                <h1 id="_top" class="sl-page-title">{p.Page.Title}</h1>
              </div>
            </div>
            <div class="sl-content-panel">
              <div class="sl-container">
                <div class="sl-markdown-content gx-content">{p.Children}</div>
                <PageFooter site={p.Site} page={p.Page} />
              </div>
            </div>
          }
        </main>
      </div>
    </div>
  </div>
</div>
<SearchDialog />
ui/starlight-shell/Sidebar.fixtures.go
package starlight

import "github.com/alternayte/gx"

var SidebarFixtures = gx.Fixtures[SidebarProps]{
	"Default": {Site: fixtureSite, Nav: fixtureNav, Path: "/start/"},
}
ui/starlight-shell/Sidebar.gx
package starlight

props {
  // Site is the site-wide data. The mobile menu shows its social links.
  Site Config
  // Nav is the sidebar model: the groups and their items.
  Nav  Nav
  // Path is the site path of the current page. A closed group that holds the page starts
  // open.
  Path string
}

<nav class="sl-sidebar" aria-label="Main">
  <button type="button" class="sl-menu-button" data-gx-menu aria-expanded="false" aria-controls="gx-sidebar">
    {icon("bars", "sl-open-menu")}
    {icon("close", "sl-close-menu")}
    <span class="sr-only">Menu</span>
  </button>
  <div id="gx-sidebar" class="sl-sidebar-pane" data-gx-sidebar="50rem">
    <div class="sl-sidebar-content">
      <ul class="sl-top-level">
        for _, g := range p.Nav.Groups {
          <li>
            <details open={GroupOpen(p.Path, g.Collapsed, g.Items)}>
              <summary>
                <span class="sl-group-label">
                  <span class="sl-large">{g.Label}</span>
                  if g.Badge != "" {
                    <span class="sl-badge">{g.Badge}</span>
                  }
                </span>
                {icon("right-caret", "sl-caret")}
              </summary>
              <ul>
                for _, it := range g.Items {
                  <SidebarItem item={it} path={p.Path} />
                }
              </ul>
            </details>
          </li>
        }
      </ul>
      <div class="sl-mobile-preferences">
        <SocialIcons links={p.Site.Links} />
        <ThemeSelect />
      </div>
    </div>
  </div>
</nav>
ui/starlight-shell/SidebarItem.fixtures.go
package starlight

import "github.com/alternayte/gx"

var SidebarItemFixtures = gx.Fixtures[SidebarItemProps]{
	"Link":   {Item: NavItem{Label: "Introduction", Href: gx.URL("/start/"), Badge: "New"}, Path: "/start/"},
	"Group":  {Item: NavItem{Label: "Errors", Items: []NavItem{{Label: "GX1000", Href: gx.URL("/errors/GX1000/")}}}},
	"Closed": {Item: NavItem{Label: "Errors", Collapsed: true, Items: []NavItem{{Label: "GX1000", Href: gx.URL("/errors/GX1000/")}}}},
}
ui/starlight-shell/SidebarItem.gx
package starlight

props {
  // Item is the sidebar link. An item with nested items shows as a group that opens and
  // closes.
  Item NavItem
  // Path is the site path of the current page. A closed group that holds the page starts
  // open.
  Path string
}

<li>
  if len(p.Item.Items) > 0 {
    <details open={GroupOpen(p.Path, p.Item.Collapsed, p.Item.Items)}>
      <summary>
        <span class="sl-group-label">
          <span class="sl-large">{p.Item.Label}</span>
          if p.Item.Badge != "" {
            <span class="sl-badge">{p.Item.Badge}</span>
          }
        </span>
        {icon("right-caret", "sl-caret")}
      </summary>
      <ul>
        for _, child := range p.Item.Items {
          <SidebarItem item={child} path={p.Path} />
        }
      </ul>
    </details>
  } else {
    <a class="sl-nav-link" href={p.Item.Href} active="page">
      <span>{p.Item.Label}</span>
      if p.Item.Badge != "" {
        <span class="sl-badge">{p.Item.Badge}</span>
      }
    </a>
  }
</li>
ui/starlight-shell/SiteTitle.fixtures.go
package starlight

import "github.com/alternayte/gx"

var SiteTitleFixtures = gx.Fixtures[SiteTitleProps]{
	"Label":    {Site: fixtureSite},
	"Versions": {Site: fixtureVersions},
	"NoLabel":  {Site: Config{Title: "Deedbox"}},
}
ui/starlight-shell/SiteTitle.gx
package starlight

props {
  // Site is the site-wide data. With two or more Versions, a select shows after the title;
  // with fewer, the Version label shows.
  Site Config
}

<div class="sl-title-row">
  <a class="sl-site-title" href={gx.URL("/")}>
    <span translate="no">{p.Site.Title}</span>
  </a>
  if len(p.Site.Versions) > 1 {
    <select class="sl-version-select" aria-label="Documentation version" data-gx-goto autocomplete="off">
      for _, v := range p.Site.Versions {
        <option value={string(v.Href)} selected={v.Label == p.Site.Version}>{v.Label}</option>
      }
    </select>
  } else if p.Site.Version != "" {
    <span class="sl-version" data-gx-version>{p.Site.Version}</span>
  }
</div>
ui/starlight-shell/SocialIcons.fixtures.go
package starlight

import "github.com/alternayte/gx"

var SocialIconsFixtures = gx.Fixtures[SocialIconsProps]{
	"Default": {Links: fixtureSite.Links},
}
ui/starlight-shell/SocialIcons.gx
package starlight

props {
  // Links holds the social links. Each one shows the built-in icon that its Icon names.
  Links []Link
}

<div class="sl-social-icons">
  for _, l := range p.Links {
    <a class="sl-social-link" href={l.Href} rel="me">
      <span class="sr-only">{l.Label}</span>
      {icon(l.Icon, "")}
    </a>
  }
</div>
ui/starlight-shell/ThemeSelect.fixtures.go
package starlight

import "github.com/alternayte/gx"

var ThemeSelectFixtures = gx.Fixtures[ThemeSelectProps]{
	"Default": {},
}
ui/starlight-shell/ThemeSelect.gx
package starlight

<label class="sl-select">
  <span class="sr-only">Select theme</span>
  {icon("laptop", "sl-select-icon")}
  <select data-gx-theme="select" autocomplete="off">
    <option value="dark">Dark</option>
    <option value="light">Light</option>
    <option value="auto" selected>Auto</option>
  </select>
  {icon("down-caret", "sl-select-caret")}
</label>
ui/starlight-shell/Toc.fixtures.go
package starlight

import "github.com/alternayte/gx"

var TocFixtures = gx.Fixtures[TocProps]{
	"Default": {Headings: fixtureHeadings},
}
ui/starlight-shell/Toc.gx
package starlight

props {
  // Headings holds the entries of the table of contents, below the entry "Overview" for the
  // top of the page.
  Headings []Heading
}

<nav class="sl-toc" data-gx-toc aria-labelledby="sl-on-this-page">
  <h2 id="sl-on-this-page">On this page</h2>
  <ul class="sl-toc-list">
    <li>
      <a href="#_top" data-gx-toc-target="_top" data-level="2"><span>Overview</span></a>
    </li>
    for _, h := range p.Headings {
      <li>
        <a href={gx.URL("#" + h.ID)} data-gx-toc-target={h.ID} data-level={h.Level}><span>{h.Text}</span></a>
      </li>
    }
  </ul>
</nav>
ui/starlight-shell/fixtures.go
package starlight

import "github.com/alternayte/gx"

// fixtureNav is the sidebar model of the fixtures: two open groups, with a
// nested group that starts closed.
var fixtureNav = Nav{Groups: []NavGroup{
	{Label: "Start", Items: []NavItem{
		{Label: "Introduction", Href: gx.URL("/start/"), Badge: "New"},
		{Label: "Install", Href: gx.URL("/start/install/")},
	}},
	{Label: "Reference", Items: []NavItem{
		{Label: "Configuration", Href: gx.URL("/reference/configuration/")},
		{Label: "Errors", Collapsed: true, Items: []NavItem{
			{Label: "GX1000", Href: gx.URL("/errors/GX1000/")},
		}},
	}},
}}

// fixtureSite is the site config of the fixtures.
var fixtureSite = Config{
	Title:    "Deedbox",
	Version:  "latest",
	Links:    []Link{{Label: "GitHub", Href: gx.URL("https://example.com/gx"), Icon: "github"}},
	EditBase: "https://example.com/edit/main/content/docs/",
}

// fixtureVersions is the site config of the fixture with a version select.
var fixtureVersions = Config{
	Title:   "Deedbox",
	Version: "latest",
	Versions: []Version{
		{Label: "latest", Href: gx.URL("https://example.com/")},
		{Label: "0.4", Href: gx.URL("https://example.com/0.4/")},
	},
}

// fixtureNext is the next page of the fixtures.
var fixtureNext = &NavItem{Label: "Install", Href: gx.URL("/start/install/")}

// fixturePrev is the previous page of the fixtures.
var fixturePrev = &NavItem{Label: "Introduction", Href: gx.URL("/start/")}

// fixtureHeadings are the headings of the fixtures.
var fixtureHeadings = []Heading{
	{Level: 2, Text: "Install", ID: "install"},
	{Level: 3, Text: "First page", ID: "first-page"},
}

// fixturePage is the page data of the fixtures.
var fixturePage = Page{
	Title:   "Introduction",
	Path:    "/start/",
	Updated: "Oct 2, 2026",
	Next:    fixtureNext,
	TOC:     fixtureHeadings,
}
ui/starlight-shell/helpers.go
package starlight

import (
	"strings"

	"github.com/alternayte/gx"
	"github.com/alternayte/gx/registry/docs"
)

// icon renders one built-in icon of the docs kit at the size of the text.
func icon(name docs.IconName, class string) gx.Node {
	return gx.Icon(string(docs.IconBody(name)), gx.IconProps{Class: gx.Cx("sl-icon", class)})
}

// Active reports whether href is the current page.
func Active(path string, href string) bool {
	return href != "" && path == href
}

// ActiveItem reports whether item or one of its descendants is the current
// page.
func ActiveItem(path string, item NavItem) bool {
	if Active(path, string(item.Href)) {
		return true
	}
	for _, child := range item.Items {
		if ActiveItem(path, child) {
			return true
		}
	}
	return false
}

// GroupOpen reports whether a sidebar group starts open: it is not
// collapsed, or it holds the current page.
func GroupOpen(path string, collapsed bool, items []NavItem) bool {
	if !collapsed {
		return true
	}
	for _, item := range items {
		if ActiveItem(path, item) {
			return true
		}
	}
	return false
}

// TocDepth returns the heading depth limit of the page.
func (p Page) TocDepth() int {
	if p.Depth <= 0 {
		return 3
	}
	return p.Depth
}

// ShownTOC returns the headings that the table of contents shows. A splash
// page has none.
func (p Page) ShownTOC() []Heading {
	if p.Splash {
		return nil
	}
	depth := p.TocDepth()
	var out []Heading
	for _, h := range p.TOC {
		if h.Level >= 2 && h.Level <= depth && h.Text != "" {
			out = append(out, h)
		}
	}
	return out
}

// RootClass returns the classes of the html element. The theme sets the
// widths of the layout from them.
func RootClass(p Page) string {
	if p.Splash {
		return ""
	}
	// A page with a sidebar always has the entry "Overview" in its table
	// of contents.
	return "sl-has-sidebar sl-has-toc"
}

// EditURL returns the edit link of the page. The page path maps to its
// Markdown file: "/start/" to "start.md".
func (c Config) EditURL(p Page) string {
	if c.EditBase == "" || p.Path == "" {
		return ""
	}
	base := c.EditBase
	if base[len(base)-1] != '/' {
		base += "/"
	}
	path := strings.TrimPrefix(p.Path, "/")
	path = strings.TrimSuffix(path, "/")
	if path == "" {
		path = "index"
	}
	return base + path + ".md"
}
ui/starlight-shell/nav.go
// Package starlight is the starlight-shell registry block: the page
// structure of the default Starlight theme. It has a fixed header, a
// sidebar, a table of contents, a hero, page links and a search dialog.
// The `starlight` theme styles it; the `docs` kit holds its icons.
package starlight

import (
	"github.com/alternayte/gx"
	"github.com/alternayte/gx/registry/docs"
)

// Nav is the sidebar model.
type Nav struct {
	Groups []NavGroup
}

// NavGroup is one top-level sidebar section. A group with Collapsed starts
// closed, unless it holds the current page.
type NavGroup struct {
	Label     string
	Badge     string
	Collapsed bool
	Items     []NavItem
}

// NavItem is one sidebar link. An item with Items is a nested group; a
// nested group with Collapsed starts closed, unless it holds the current
// page.
type NavItem struct {
	Label     string
	Href      gx.URL
	Badge     string
	Collapsed bool
	Items     []NavItem
}

// Link is one social link of the header. Icon is the name of a built-in
// icon of the docs kit, for example "github".
type Link struct {
	Label string
	Href  gx.URL
	Icon  docs.IconName
}

// Version is one version of the docs: its label and the address of its
// site.
type Version struct {
	Label string
	Href  gx.URL
}

// Config is the site-wide shell data.
type Config struct {
	Title string
	// Version is the label of the version that this site shows. The header
	// shows it after the title. Empty hides it.
	Version string
	// Versions lists the versions of the docs. With two or more, the
	// header shows a select in place of the label, and a choice opens the
	// site of that version.
	Versions []Version
	Links    []Link
	// EditBase is the repository edit URL up to the file path, for example
	// https://github.com/acme/site/edit/main/content/docs/. Empty hides
	// the edit link.
	EditBase string
}

// Page is the shell data of one page.
type Page struct {
	Title string
	// Path is the site path of the page, for example /start/.
	Path string
	// Splash is true for a page with no sidebar and no table of contents,
	// such as a home page.
	Splash bool
	// Updated is the date of the last change. Empty hides it.
	Updated string
	// Prev and Next are the previous and next sidebar items.
	Prev *NavItem
	Next *NavItem
	// TOC holds the page headings, in document order.
	TOC []Heading
	// Depth limits the table of contents to headings at or below this
	// level. Zero means 3.
	Depth int
}

// Heading is one table-of-contents entry.
type Heading struct {
	Level int
	Text  string
	ID    string
}

The theme must define these tokens: --sl-color-bg, --sl-color-text, --sl-color-text-accent, --sl-nav-height, --sl-sidebar-width, --sl-content-width.

Usage

The page structure of the default Starlight theme. It has these parts:

  • A fixed header with the site title, a version label, the search button, social icons and a theme select.
  • A sidebar and a table of contents.
  • A hero for a splash page.
  • Page links, a search dialog and a 404 page.

The block needs the starlight theme for its look and the docs kit for its icons.

var Site = starlight.Config{
    Title:    "Deedbox",
    Version:  "latest",
    Links:    []starlight.Link{{Label: "GitHub", Href: gx.URL("https://github.com/acme/site"), Icon: "github"}},
    EditBase: "https://github.com/acme/site/edit/main/content/docs/",
}

func View(e gx.Entry[DocMeta]) gx.Node {
    return starlight.Shell(starlight.ShellProps{Site: Site, Nav: Nav(), Page: PageFor(e), Children: DocsBody(e)})
}

A splash page has no sidebar and no table of contents. Give it a hero:

starlight.Shell(starlight.ShellProps{
    Site: Site,
    Page: starlight.Page{Title: "Deedbox", Path: "/", Splash: true},
    Hero: starlight.Hero(starlight.HeroProps{
        Title:   "Deedbox",
        Tagline: "Event-source part of your app.",
        Actions: docs.LinkButton(docs.LinkButtonProps{Href: gx.URL("/start/"), Icon: "right-arrow", Children: gx.Text("Start")}),
    }),
    Children: DocsBody(e),
})

With two or more Versions in the config, the header shows a select. A choice opens the site of that version.

Examples

Header: Default

<starlight.Header site={starlight.Config{
  Title:    "Deedbox",
  Version:  "latest",
  Links:    []starlight.Link{{Label: "GitHub", Href: gx.URL("https://example.com/gx"), Icon: "github"}},
  EditBase: "https://example.com/edit/main/content/docs/",
}} />

Hero: Default

<starlight.Hero title="Deedbox" tagline="Event-source part of your app." />

Hero: Title

<starlight.Hero title="Deedbox" />

MobileToc: Default

<starlight.MobileToc headings={[]starlight.Heading{
  {Level: 2, Text: "Install", ID: "install"},
  {Level: 3, Text: "First page", ID: "first-page"},
}} />

NotFound: Default

<starlight.NotFound site={starlight.Config{
  Title:    "Deedbox",
  Version:  "latest",
  Links:    []starlight.Link{{Label: "GitHub", Href: gx.URL("https://example.com/gx"), Icon: "github"}},
  EditBase: "https://example.com/edit/main/content/docs/",
}} />
<starlight.PageFooter site={starlight.Config{
  Title:    "Deedbox",
  Version:  "latest",
  Links:    []starlight.Link{{Label: "GitHub", Href: gx.URL("https://example.com/gx"), Icon: "github"}},
  EditBase: "https://example.com/edit/main/content/docs/",
}} page={starlight.Page{
  Title:   "Introduction",
  Path:    "/start/",
  Updated: "Oct 2, 2026",
  Next:    &starlight.NavItem{Label: "Install", Href: gx.URL("/start/install/")},
  TOC: []starlight.Heading{
    {Level: 2, Text: "Install", ID: "install"},
    {Level: 3, Text: "First page", ID: "first-page"},
  },
}} />

Pagination: Both

<starlight.Pagination prev={&starlight.NavItem{Label: "Introduction", Href: gx.URL("/start/")}} next={&starlight.NavItem{Label: "Install", Href: gx.URL("/start/install/")}} />

Pagination: Next

<starlight.Pagination next={&starlight.NavItem{Label: "Install", Href: gx.URL("/start/install/")}} prev={(*starlight.NavItem)(nil)} />

Pagination: Prev

<starlight.Pagination prev={&starlight.NavItem{Label: "Introduction", Href: gx.URL("/start/")}} next={(*starlight.NavItem)(nil)} />

SearchDialog: Default

<starlight.SearchDialog />

Shell: Default

<starlight.Shell site={starlight.Config{
  Title:    "Deedbox",
  Version:  "latest",
  Links:    []starlight.Link{{Label: "GitHub", Href: gx.URL("https://example.com/gx"), Icon: "github"}},
  EditBase: "https://example.com/edit/main/content/docs/",
}} nav={starlight.Nav{Groups: []starlight.NavGroup{
  {Label: "Start", Items: []starlight.NavItem{
    {Label: "Introduction", Href: gx.URL("/start/"), Badge: "New"},
    {Label: "Install", Href: gx.URL("/start/install/")},
  }},
  {Label: "Reference", Items: []starlight.NavItem{
    {Label: "Configuration", Href: gx.URL("/reference/configuration/")},
    {Label: "Errors", Collapsed: true, Items: []starlight.NavItem{
      {Label: "GX1000", Href: gx.URL("/errors/GX1000/")},
    }},
  }},
}}} page={starlight.Page{
  Title:   "Introduction",
  Path:    "/start/",
  Updated: "Oct 2, 2026",
  Next:    &starlight.NavItem{Label: "Install", Href: gx.URL("/start/install/")},
  TOC: []starlight.Heading{
    {Level: 2, Text: "Install", ID: "install"},
    {Level: 3, Text: "First page", ID: "first-page"},
  },
}}>
  Body.
</starlight.Shell>

Shell: Splash

<starlight.Shell site={starlight.Config{
  Title:    "Deedbox",
  Version:  "latest",
  Links:    []starlight.Link{{Label: "GitHub", Href: gx.URL("https://example.com/gx"), Icon: "github"}},
  EditBase: "https://example.com/edit/main/content/docs/",
}} nav={starlight.Nav{Groups: []starlight.NavGroup{
  {Label: "Start", Items: []starlight.NavItem{
    {Label: "Introduction", Href: gx.URL("/start/"), Badge: "New"},
    {Label: "Install", Href: gx.URL("/start/install/")},
  }},
  {Label: "Reference", Items: []starlight.NavItem{
    {Label: "Configuration", Href: gx.URL("/reference/configuration/")},
    {Label: "Errors", Collapsed: true, Items: []starlight.NavItem{
      {Label: "GX1000", Href: gx.URL("/errors/GX1000/")},
    }},
  }},
}}} page={starlight.Page{Title: "Deedbox", Path: "/", Splash: true}}>
  <:hero><starlight.Hero title="Deedbox" tagline="Event-source part of your app." /></:hero>
  Body.
</starlight.Shell>
<starlight.Sidebar site={starlight.Config{
  Title:    "Deedbox",
  Version:  "latest",
  Links:    []starlight.Link{{Label: "GitHub", Href: gx.URL("https://example.com/gx"), Icon: "github"}},
  EditBase: "https://example.com/edit/main/content/docs/",
}} nav={starlight.Nav{Groups: []starlight.NavGroup{
  {Label: "Start", Items: []starlight.NavItem{
    {Label: "Introduction", Href: gx.URL("/start/"), Badge: "New"},
    {Label: "Install", Href: gx.URL("/start/install/")},
  }},
  {Label: "Reference", Items: []starlight.NavItem{
    {Label: "Configuration", Href: gx.URL("/reference/configuration/")},
    {Label: "Errors", Collapsed: true, Items: []starlight.NavItem{
      {Label: "GX1000", Href: gx.URL("/errors/GX1000/")},
    }},
  }},
}}} path="/start/" />
<starlight.SidebarItem item={starlight.NavItem{Label: "Introduction", Href: gx.URL("/start/"), Badge: "New"}} path="/start/" />

SidebarItem: Group

<starlight.SidebarItem item={starlight.NavItem{Label: "Errors", Items: []starlight.NavItem{{Label: "GX1000", Href: gx.URL("/errors/GX1000/")}}}} path="" />

SidebarItem: Closed

<starlight.SidebarItem item={starlight.NavItem{Label: "Errors", Collapsed: true, Items: []starlight.NavItem{{Label: "GX1000", Href: gx.URL("/errors/GX1000/")}}}} path="" />

SiteTitle: Label

<starlight.SiteTitle site={starlight.Config{
  Title:    "Deedbox",
  Version:  "latest",
  Links:    []starlight.Link{{Label: "GitHub", Href: gx.URL("https://example.com/gx"), Icon: "github"}},
  EditBase: "https://example.com/edit/main/content/docs/",
}} />

SiteTitle: Versions

<starlight.SiteTitle site={starlight.Config{
  Title:   "Deedbox",
  Version: "latest",
  Versions: []starlight.Version{
    {Label: "latest", Href: gx.URL("https://example.com/")},
    {Label: "0.4", Href: gx.URL("https://example.com/0.4/")},
  },
}} />

SiteTitle: No label

<starlight.SiteTitle site={starlight.Config{Title: "Deedbox"}} />

SocialIcons: Default

<starlight.SocialIcons links={starlight.Config{
  Title:    "Deedbox",
  Version:  "latest",
  Links:    []starlight.Link{{Label: "GitHub", Href: gx.URL("https://example.com/gx"), Icon: "github"}},
  EditBase: "https://example.com/edit/main/content/docs/",
}.Links} />

ThemeSelect: Default

<starlight.ThemeSelect />

Toc: Default

<starlight.Toc headings={[]starlight.Heading{
  {Level: 2, Text: "Install", ID: "install"},
  {Level: 3, Text: "First page", ID: "first-page"},
}} />

API reference

A tag sets a prop by its name with a lower-case first letter: Class is class.

starlight.Header

Prop Type Default Description
Site Config Required Site is the site-wide data: the title, the versions and the social links.

starlight.Hero

Prop Type Default Description
Title string Required Title is the heading of the hero.
Tagline string "" Tagline is the text below the title. Empty omits it.
Actions gx.Node nil Actions is the row of link buttons below the tagline. Nil omits it.
Image gx.Node nil Image is the picture beside the text. Nil omits it.

starlight.MobileToc

Prop Type Default Description
Headings []Heading Required Headings holds the entries of the table of contents, below the entry "Overview" for the top of the page.

starlight.NotFound

Prop Type Default Description
Site Config Required Site is the site-wide data: the title, the versions and the social links.
Title string "404" Title is the heading of the page.
Message string "Page not found. Check the URL or try using the search bar." Message is the text below the title.

starlight.PageFooter

Prop Type Default Description
Site Config Required Site is the site-wide data. Its EditBase builds the edit link; empty hides the link.
Page Page Required Page is the data of the current page: its path, its date and its previous and next items.

starlight.Pagination

Prop Type Default Description
Prev *NavItem Required Prev is the previous sidebar item. Nil omits the link.
Next *NavItem Required Next is the next sidebar item. Nil omits the link.

starlight.SearchDialog

The component has no props.

starlight.Shell

Prop Type Default Description
Site Config Required Site is the site-wide data: the title, the versions and the social links.
Nav Nav Required Nav is the sidebar model: the groups and their items.
Page Page Required Page is the data of the current page: its title, its path, its headings and its previous and next items. A page with Splash has no sidebar and no table of contents.
Hero gx.Node nil Hero is the hero of the page. It takes the place of the page title. Nil shows the title.
Children gx.Node Required Children is the content of the page.

starlight.Sidebar

Prop Type Default Description
Site Config Required Site is the site-wide data. The mobile menu shows its social links.
Nav Nav Required Nav is the sidebar model: the groups and their items.
Path string Required Path is the site path of the current page. A closed group that holds the page starts open.

starlight.SidebarItem

Prop Type Default Description
Item NavItem Required Item is the sidebar link. An item with nested items shows as a group that opens and closes.
Path string Required Path is the site path of the current page. A closed group that holds the page starts open.

starlight.SiteTitle

Prop Type Default Description
Site Config Required Site is the site-wide data. With two or more Versions, a select shows after the title; with fewer, the Version label shows.

starlight.SocialIcons

Prop Type Default Description
Links []Link Required Links holds the social links. Each one shows the built-in icon that its Icon names.

starlight.ThemeSelect

The component has no props.

starlight.Toc

Prop Type Default Description
Headings []Heading Required Headings holds the entries of the table of contents, below the entry "Overview" for the top of the page.

Do and do not

Do

  • Keep the page Path with a trailing slash. The sidebar marks the item with the same path.
  • Name a built-in icon of the docs kit for each social link, for example github.
  • Set Collapsed on a long nested group. The group opens when it holds the current page.

Don't

  • Do not use the block without the starlight theme. The markup has no utility classes.
  • Do not put a Toc or a Sidebar outside the Shell.

Keyboard

Key Action
Tab Moves through the skip link, the header, the sidebar, the content and the table of contents.
Enter, Space Opens the search dialog, the mobile menu, a sidebar group or the table of contents menu.
Ctrl/Cmd K Opens the search dialog.
Escape Closes the search dialog and the table of contents menu.
Arrow up, Arrow down Changes the theme or the version in its select.