Skip to content
Gx
GitHub

App Shell

An app page frame with a sidebar, a header and a main area.

<appshell.AppShell title="Acme">
  <:nav>
    <sidebar.SidebarGroup>
      <sidebar.SidebarGroupLabel>Menu</sidebar.SidebarGroupLabel>
      <sidebar.SidebarGroupContent>
        <sidebar.SidebarMenu>
          <sidebar.SidebarMenuItem>
            <sidebar.SidebarMenuButton href={gx.URL("/")} active><span>Home</span></sidebar.SidebarMenuButton>
          </sidebar.SidebarMenuItem>
          <sidebar.SidebarMenuItem>
            <sidebar.SidebarMenuButton href={gx.URL("/docs")}><span>Docs</span></sidebar.SidebarMenuButton>
          </sidebar.SidebarMenuItem>
        </sidebar.SidebarMenu>
      </sidebar.SidebarGroupContent>
    </sidebar.SidebarGroup>
  </:nav>
  <:footer><span class="px-2 text-xs text-sidebar-foreground/70">v0.1.0</span></:footer>
  Page content.
</appshell.AppShell>

Installation

Run the command in the app module.

Terminal
gx add app-shell

The command also installs sidebar.

The command writes these files.

File Path in the app
AppShell.gx ui/app-shell/AppShell.gx
AppShell.fixtures.go ui/app-shell/AppShell.fixtures.go

Install sidebar 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/app-shell/AppShell.gx
package appshell

import "github.com/alternayte/gx/registry/sidebar"

props {
  // Title is the app name. It shows in the sidebar header and in the page header.
  Title    string
  // Nav is the primary navigation. It renders in the sidebar content.
  Nav      gx.Node
  // Footer is the content of the sidebar footer. A nil value renders no footer.
  Footer   gx.Node = nil
  // Children is the content of the main area.
  Children gx.Node
}

<div class="flex min-h-screen">
  <sidebar.Sidebar id="gx-sidebar">
    <sidebar.SidebarHeader>
      <span class="px-2 py-1 text-sm font-semibold">{p.Title}</span>
    </sidebar.SidebarHeader>
    <sidebar.SidebarContent>
      {p.Nav}
    </sidebar.SidebarContent>
    if p.Footer != nil {
      <sidebar.SidebarFooter>{p.Footer}</sidebar.SidebarFooter>
    }
  </sidebar.Sidebar>
  <div class="flex min-w-0 flex-1 flex-col">
    <header class="flex h-14 items-center gap-2 border-b border-border px-4">
      <sidebar.SidebarTrigger class="-ml-1 lg:hidden" />
      <h1 class="text-sm font-semibold">{p.Title}</h1>
    </header>
    <main class="flex-1 p-6">{p.Children}</main>
  </div>
</div>
ui/app-shell/AppShell.fixtures.go
package appshell

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

var AppShellFixtures = gx.Fixtures[AppShellProps]{
	"Default": {
		Title: "Acme",
		Nav: sidebar.SidebarGroup(sidebar.SidebarGroupProps{Children: gx.Frag(
			sidebar.SidebarGroupLabel(sidebar.SidebarGroupLabelProps{Children: gx.Text("Menu")}),
			sidebar.SidebarGroupContent(sidebar.SidebarGroupContentProps{Children: sidebar.SidebarMenu(sidebar.SidebarMenuProps{Children: gx.Frag(
				sidebar.SidebarMenuItem(sidebar.SidebarMenuItemProps{Children: sidebar.SidebarMenuButton(sidebar.SidebarMenuButtonProps{Href: gx.URL("/"), Active: true, Children: gx.El("span", nil, gx.Text("Home"))})}),
				sidebar.SidebarMenuItem(sidebar.SidebarMenuItemProps{Children: sidebar.SidebarMenuButton(sidebar.SidebarMenuButtonProps{Href: gx.URL("/docs"), Children: gx.El("span", nil, gx.Text("Docs"))})}),
			)})}),
		)}),
		Footer:   gx.El("span", gx.Attrs{{Key: "class", Value: "px-2 text-xs text-sidebar-foreground/70"}}, gx.Text("v0.1.0")),
		Children: gx.Text("Page content."),
	},
}

The theme must define these tokens: --border, --sidebar, --sidebar-foreground.

Usage

<appshell.AppShell title="Acme" nav={<appshell.Nav />}>
  <p>Page content.</p>
</appshell.AppShell>

The block is copied source. Wrap it in a gx.Layout so every page shares the frame.

Examples

Default

<appshell.AppShell title="Acme">
  <:nav>
    <sidebar.SidebarGroup>
      <sidebar.SidebarGroupLabel>Menu</sidebar.SidebarGroupLabel>
      <sidebar.SidebarGroupContent>
        <sidebar.SidebarMenu>
          <sidebar.SidebarMenuItem>
            <sidebar.SidebarMenuButton href={gx.URL("/")} active><span>Home</span></sidebar.SidebarMenuButton>
          </sidebar.SidebarMenuItem>
          <sidebar.SidebarMenuItem>
            <sidebar.SidebarMenuButton href={gx.URL("/docs")}><span>Docs</span></sidebar.SidebarMenuButton>
          </sidebar.SidebarMenuItem>
        </sidebar.SidebarMenu>
      </sidebar.SidebarGroupContent>
    </sidebar.SidebarGroup>
  </:nav>
  <:footer><span class="px-2 text-xs text-sidebar-foreground/70">v0.1.0</span></:footer>
  Page content.
</appshell.AppShell>

API reference

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

appshell.AppShell

Prop Type Default Description
Title string Required Title is the app name. It shows in the sidebar header and in the page header.
Nav gx.Node Required Nav is the primary navigation. It renders in the sidebar content.
Footer gx.Node nil Footer is the content of the sidebar footer. A nil value renders no footer.
Children gx.Node Required Children is the content of the main area.

Do and do not

Do

  • Keep one page title in the header.
  • Put the primary navigation in Nav.

Don't

  • Do not add a second header inside the main area.
  • Do not nest an app shell in another app shell.

Keyboard

Key Action
Tab Moves to the menu button, then the sidebar links, then the page.
Enter, Space Opens or closes the sidebar on a narrow screen.