Get Started

Start developing a Hugo theme with the Keystone UI.

Introduction

Keystone UI brings the component architecture of Shadcn UI (Opens in a new tab) to the Hugo ecosystem.

It is not a rigid library or a black-box dependency. Instead, Keystone is a collection of accessible components designed to be copied into your project and customized to fit your needs. You own the code.

We built Keystone to be the foundation for high-quality Hugo themes, adhering strictly to three core pillars:

  • Accessibility: Components are implemented to meet WCAG 2.2 standards.
  • Security: All logic is compatible with strict Content Security Policies (CSP).
  • Performance: A zero-bloat architecture and no negative impact on Core Web Vitals.

Installation

The frictionless path to start developing with Keystone UI is using our starter template.

  1. Press the Use this template button on GitHub (Opens in a new tab) to setup a new repository.
  2. Clone it locally.
  3. Run npm install.

Alternatively, if you prefer not to use GitHub templates, run:

1npx degit oxypteros/keystone-ui my-new-theme
2cd my-new-theme
3npm install

If you are integrating Keystone into an existing Hugo project, follow these steps to set up Tailwind and the Alpine.js library.

1. CSS & Tailwind Setup

Keystone uses a CSS variable theme system compatible and based upon Shadcn UI.

Install Tailwind

Follow the official Hugo Tailwind Documentation (Opens in a new tab) to set up the cli.

Define Base Variables

Create a base CSS file to define your theme color palette (Default: neutral).

Tailwind CSS assets/base/_neutral.css
 1:root {
 2  --radius: 0.625rem;
 3  --background: oklch(1 0 0);
 4  --foreground: oklch(0.145 0 0);
 5  --card: oklch(1 0 0);
 6  --card-foreground: oklch(0.145 0 0);
 7  --popover: oklch(1 0 0);
 8  --popover-foreground: oklch(0.145 0 0);
 9  --primary: oklch(0.205 0 0);
10  --primary-foreground: oklch(0.985 0 0);
11  --secondary: oklch(0.97 0 0);
12  --secondary-foreground: oklch(0.205 0 0);
13  --muted: oklch(0.97 0 0);
14  --muted-foreground: oklch(0.4386 0 0);
15  --accent: oklch(0.97 0 0);
16  --accent-foreground: oklch(0.205 0 0);
17  --destructive: oklch(0.577 0.245 27.325);
18  --border: oklch(0.922 0 0);
19  --input: oklch(0.922 0 0);
20  --ring: oklch(0.708 0 0);
21  --chart-1: oklch(0.646 0.222 41.116);
22  --chart-2: oklch(0.6 0.118 184.704);
23  --chart-3: oklch(0.398 0.07 227.392);
24  --chart-4: oklch(0.828 0.189 84.429);
25  --chart-5: oklch(0.769 0.188 70.08);
26  --sidebar: oklch(0.985 0 0);
27  --sidebar-foreground: oklch(0.145 0 0);
28  --sidebar-primary: oklch(0.205 0 0);
29  --sidebar-primary-foreground: oklch(0.985 0 0);
30  --sidebar-accent: oklch(0.97 0 0);
31  --sidebar-accent-foreground: oklch(0.205 0 0);
32  --sidebar-border: oklch(0.922 0 0);
33  --sidebar-ring: oklch(0.708 0 0);
34}
35
36.dark {
37  --background: oklch(0.145 0 0);
38  --foreground: oklch(0.985 0 0);
39  --card: oklch(0.205 0 0);
40  --card-foreground: oklch(0.985 0 0);
41  --popover: oklch(0.205 0 0);
42  --popover-foreground: oklch(0.985 0 0);
43  --primary: oklch(0.922 0 0);
44  --primary-foreground: oklch(0.205 0 0);
45  --secondary: oklch(0.269 0 0);
46  --secondary-foreground: oklch(0.985 0 0);
47  --muted: oklch(0.269 0 0);
48  --muted-foreground: oklch(0.7668 0 0);
49  --accent: oklch(0.269 0 0);
50  --accent-foreground: oklch(0.985 0 0);
51  --destructive: oklch(0.704 0.191 22.216);
52  --border: oklch(1 0 0 / 10%);
53  --input: oklch(1 0 0 / 15%);
54  --ring: oklch(0.556 0 0);
55  --chart-1: oklch(0.488 0.243 264.376);
56  --chart-2: oklch(0.696 0.17 162.48);
57  --chart-3: oklch(0.769 0.188 70.08);
58  --chart-4: oklch(0.627 0.265 303.9);
59  --chart-5: oklch(0.645 0.246 16.439);
60  --sidebar: oklch(0.205 0 0);
61  --sidebar-foreground: oklch(0.985 0 0);
62  --sidebar-primary: oklch(0.488 0.243 264.376);
63  --sidebar-primary-foreground: oklch(0.985 0 0);
64  --sidebar-accent: oklch(0.269 0 0);
65  --sidebar-accent-foreground: oklch(0.985 0 0);
66  --sidebar-border: oklch(1 0 0 / 10%);
67  --sidebar-ring: oklch(0.556 0 0);
68}

Configure Main CSS

Replace your default assets/main.css with the following.

Tailwind CSS /assets/main.css
 1@import 'tailwindcss';
 2@source 'hugo_stats.json';
 3
 4/* Theme Variables */
 5@import './base/_neutral.css';
 6
 7/* Components */
 8/** (Import Keystone components CSS here) */
 9
10@theme {
11  --radius: var(--radius);
12  --color-background: var(--background);
13  --color-foreground: var(--foreground);
14  --color-card: var(--card);
15  --color-card-foreground: var(--card-foreground);
16  --color-popover: var(--popover);
17  --color-popover-foreground: var(--popover-foreground);
18  --color-primary: var(--primary);
19  --color-primary-foreground: var(--primary-foreground);
20  --color-secondary: var(--secondary);
21  --color-secondary-foreground: var(--secondary-foreground);
22  --color-muted: var(--muted);
23  --color-muted-foreground: var(--muted-foreground);
24  --color-accent: var(--accent);
25  --color-accent-foreground: var(--accent-foreground);
26  --color-destructive: var(--destructive);
27  --color-border: var(--border);
28  --color-input: var(--input);
29  --color-ring: var(--ring);
30  --color-chart-1: var(--chart-1);
31  --color-chart-2: var(--chart-2);
32  --color-chart-3: var(--chart-3);
33  --color-chart-4: var(--chart-4);
34  --color-chart-5: var(--chart-5);
35  --color-sidebar: var(--sidebar);
36  --color-sidebar-foreground: var(--sidebar-foreground);
37  --color-sidebar-primary: var(--sidebar-primary);
38  --color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
39  --color-sidebar-accent: var(--sidebar-accent);
40  --color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
41  --color-sidebar-border: var(--sidebar-border);
42  --color-sidebar-ring: var(--sidebar-ring);
43}
44
45@layer base {
46  /* Set default border and outline color */
47  * {
48    @apply border-border outline-ring/50;
49  }
50  /* Set cursor-pointer for Buttons */
51  button:not(:disabled),
52  [role='button']:not(:disabled) {
53    @apply cursor-pointer;
54  }
55}
56
57/* Alpine Styles */
58[x-cloak] {
59  display: none !important;
60}

2. JavaScript & Alpine setup

Keystone uses Alpine.js (CSP build) for interactivity.

Install Dependencies

1npm install @alpinejs/csp  @alpinejs/anchor @alpinejs/collapse @alpinejs/focus

Create entry point

Create the main JavaScript entry file.

Alpine Js /assets/js/main.js
 1import Alpine from '@alpinejs/csp';
 2import focus from '@alpinejs/focus';
 3import anchor from '@alpinejs/anchor';
 4import collapse from '@alpinejs/collapse';
 5
 6// REGISTER PLUGINS
 7Alpine.plugin(focus);
 8Alpine.plugin(anchor);
 9Alpine.plugin(collapse);
10
11// INITIALIZE STORES
12// (Add stores here)
13
14// IMPORT MODULES
15// (Add modules here)
16
17// INITIALIZE MODULES
18// (Init modules here)
19
20// START
21window.Alpine = Alpine;
22Alpine.start();

Create Hugo Bundle Pipeline

Create a partial to build and fingerprint the JavaScript.

Go/HTML template _partials/ui/head/js.html
 1{{- with resources.Get "js/main.js" }}
 2  {{- $opts := dict
 3    "minify" (not hugo.IsDevelopment)
 4    "sourceMap" (cond hugo.IsDevelopment "external" "")
 5    "targetPath" "js/main.js"
 6    "format" "esm"
 7  }}
 8  {{- with . | js.Build $opts }}
 9    {{- if hugo.IsDevelopment }}
10      <script type="module" src="{{ .RelPermalink }}"></script>
11    {{- else }}
12      {{- with . | fingerprint }}
13        <script
14          type="module"
15          src="{{ .RelPermalink }}"
16          integrity="{{ .Data.Integrity }}"
17          crossorigin="anonymous"
18        ></script>
19      {{- end }}
20    {{- end }}
21  {{- end }}
22{{- else }}
23  <script>
24    console.error('Keystone UI: main.js not found');
25  </script>
26{{- end }}

Inject into Head

Finally, include the partial in your site’s <head> tag.

HTML
1<head>
2  <!-- ... other tags ... -->
3  {{- partial "ui/head/js.html" . }}
4  <!-- ... more tags ... -->
5</head>