Skip to content
Back to skills

Go Bulma

BSecurity

Scaffold the Bulma CSS framework into a Go web app with embedded assets for zero-dependency binaries. Internal skill used by golang-pro agent when building HTML UIs with a classes-based CSS framework and no Node toolchain.

  • 16 stars
  • 0 votes
  • 0 copies
  • 10 views
  • Added May 28, 2026
developmentjavascriptgojavabashreactvueangularnodegitapi

Works with

  • cli
  • api

Security analysis

B84/100
  • mediumUses curl or wget to download content
  • mediumInstalls packages at runtime which could introduce malicious dependencies

Pro shows the line behind each finding and how to fix it

Scanned May 28, 2026

npx -y skills add sgaunet/claude-plugins --skill go-bulma --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Go Bulma?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Go Bulma
[![Security: B — Skills Directory](https://www.skillsdirectory.com/api/skills/sgaunet-go-bulma/badge)](https://www.skillsdirectory.com/skills/sgaunet-go-bulma)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
name: go-bulma
description: Scaffold the Bulma CSS framework into a Go web app with embedded assets for zero-dependency binaries. Internal skill used by golang-pro agent when building HTML UIs with a classes-based CSS framework and no Node toolchain.
user-invocable: false
allowed-tools: Read, Write, Edit, Glob, Grep, Bash(go:*), Bash(curl:*), Bash(mkdir:*), Bash(shasum:*), Bash(sha256sum:*)
---

# Bulma for Go

Scaffold [Bulma](https://bulma.io/documentation/) — a modern, classes-based, CSS-only framework (MIT, no JavaScript, no jQuery) — into a Go web app. Assets are **always** downloaded at setup time and embedded into the binary with `//go:embed`. The running binary never fetches anything from the network.

Bulma styles components through utility and component classes (`class="button is-primary"`, `class="column is-half"`, `class="navbar"`). Bulma 1.0+ (released Feb 2024) ships **CSS custom properties** for theming, so colors, radii, fonts, and spacing can be customized without Sass or a Node build step.

Bulma intentionally ships **zero JavaScript**, so interactive components (navbar burger, modal, dropdown, tabs) need a small amount of vanilla JS to actually work. This skill scaffolds a self-contained `app.js` covering those patterns.

## When to Use

- User asks for a polished classes-based HTML UI in a Go app without React/Vue/Angular
- User mentions "Bulma", "no Node", "no Sass", or wants a Bootstrap-like experience with a cleaner class system
- `html/template`, `templ`, or `*.gohtml` files exist but no CSS framework is present
- `bulma.min.css` is already in the repo and needs wiring or upgrading

## Prerequisites

1. **go.mod exists**: The project must be a Go module. Abort otherwise.
2. **Go ≥ 1.16**: `//go:embed` requires Go 1.16+. Read the `go` directive in `go.mod` to verify.
3. **HTTP server present or planned**: Bulma renders HTML served over HTTP. Confirm with the user if unclear.

## Core Principle

**Zero runtime dependencies.** The binary must run offline after `go build`. Never reference `cdn.jsdelivr.net`, `unpkg.com`, or `cdnjs.cloudflare.com` from rendered HTML. Download once at setup time, embed, commit, ship.

## Asset Source

Use the pinned npm-on-jsdelivr URL — it is immutable per version and fetched only at setup:

```
https://cdn.jsdelivr.net/npm/bulma@<VERSION>/css/bulma.min.css
```

Alternative (also acceptable, also pinned): the GitHub release zip:

```
https://github.com/jgthms/bulma/releases/download/<VERSION>/bulma-<VERSION>.zip
```

The zip contains `css/bulma.min.css` and the unminified sources. For a Go workflow the single `bulma.min.css` from jsdelivr is simpler.

**Do not** use:
- `unpkg.com/bulma/...` without a version — resolves to `latest`, not reproducible
- `cdnjs.cloudflare.com/.../latest/...` — same problem
- Bulma's GitHub `master` branch raw URLs — moving target, not a release artifact

To find the current version: `curl -s https://api.github.com/repos/jgthms/bulma/releases/latest | grep tag_name`

## Workflow: Scaffold Bulma into a Go project

### Step 1: Choose asset layout

| Project shape | Recommended layout |
|---|---|
| Flat app (`main.go` at root) | `web/static/bulma.min.css`, `web/static/app.js`, `web/embed.go` |
| `internal/` + layered (`cmd/...`, `internal/...`) | `internal/ui/static/bulma.min.css`, `internal/ui/embed.go` |
| Library with optional web UI | `web/ui/static/...` in a separate package |

Prefer `internal/ui/` when there is already an `internal/` tree. Otherwise use `web/`.

### Step 2: Download pinned assets

```bash
VERSION=1.0.4  # confirm latest via the release API before running
DEST=internal/ui/static
mkdir -p "$DEST"
curl -sSLo "$DEST/bulma.min.css" "https://cdn.jsdelivr.net/npm/bulma@${VERSION}/css/bulma.min.css"
```

The jsdelivr npm path uses a bare version like `1.0.4` (no `v` prefix); GitHub release tags happen to match (also `1.0.4`, no `v`).

### Step 3: Record the version and checksums

Create a `BULMA_VERSION` file (or header comment in `embed.go`) so upgrades are traceable:

```
bulma 1.0.4
bulma.min.css sha256: <output of shasum -a 256 bulma.min.css>
app.js        sha256: <output of shasum -a 256 app.js>
```

### Step 4: Create the embed package

```go
// internal/ui/embed.go
package ui

import "embed"

// Static holds Bulma's CSS and the vanilla JS helpers.
// See ../../BULMA_VERSION for the pinned release.
//
//go:embed static/bulma.min.css static/app.js
var Static embed.FS
```

If a `theme.css` is added later (see Theming), extend the directive:

```go
//go:embed static/bulma.min.css static/app.js static/theme.css
```

### Step 5: Wire the static file handler

```go
// in route setup (main.go or internal/server/routes.go)
import (
    "io/fs"
    "net/http"

    "yourmod/internal/ui"
)

func registerStatic(mux *http.ServeMux) {
    sub, err := fs.Sub(ui.Static, "static")
    if err != nil {
        panic(err) // compile-time constant; impossible at runtime
    }
    mux.Handle("/static/", http.StripPrefix("/static/", http.FileServer(http.FS(sub))))
}
```

### Step 6: Create a minimal Bulma-styled template

```html
<!-- internal/ui/templates/index.html -->
<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width,initial-scale=1">
  <title>{{.Title}}</title>
  <link rel="stylesheet" href="/static/bulma.min.css">
  <script src="/static/app.js" defer></script>
</head>
<body>
  <nav class="navbar is-primary" role="navigation" aria-label="main navigation">
    <div class="navbar-brand">
      <a class="navbar-item" href="/"><strong>{{.Title}}</strong></a>
      <a role="button" class="navbar-burger" aria-label="menu" aria-expanded="false" data-target="mainMenu">
        <span aria-hidden="true"></span>
        <span aria-hidden="true"></span>
        <span aria-hidden="true"></span>
      </a>
    </div>
    <div id="mainMenu" class="navbar-menu">
      <div class="navbar-end">
        <a class="navbar-item" href="/about">About</a>
      </div>
    </div>
  </nav>

  <section class="section">
    <div class="container">
      <h1 class="title">{{.Title}}</h1>
      <form method="post" action="/save">
        <div class="field">
          <label class="label">Name</label>
          <div class="control">
            <input class="input" name="name" required>
          </div>
        </div>
        <div class="field">
          <label class="label">Email</label>
          <div class="control">
            <input class="input" type="email" name="email">
          </div>
        </div>
        <div class="field">
          <div class="control">
            <button class="button is-primary" type="submit">Save</button>
            <button class="button" type="button" data-target="aboutModal">About</button>
          </div>
        </div>
      </form>
    </div>
  </section>

  <div id="aboutModal" class="modal">
    <div class="modal-background"></div>
    <div class="modal-card">
      <header class="modal-card-head">
        <p class="modal-card-title">About</p>
        <button class="delete" aria-label="close"></button>
      </header>
      <section class="modal-card-body">
        <p>Styled by Bulma — embedded in the binary, served offline.</p>
      </section>
    </div>
  </div>
</body>
</html>
```

Embed templates alongside assets:

```go
//go:embed templates/*.html
var Templates embed.FS
```

Parse at startup with `template.ParseFS(ui.Templates, "templates/*.html")`.

### Step 7: Verify zero-dependency build

```bash
go build -o app ./...
./app &
curl -sI http://localhost:PORT/static/bulma.min.css | head -1   # expect 200
curl -sI http://localhost:PORT/static/app.js | head -1          # expect 200
# kill the app, disable network, rerun — it still works
```

## Workflow: Upgrade Bulma

1. Check the latest release: `curl -s https://api.github.com/repos/jgthms/bulma/releases/latest | grep tag_name`
2. Re-run Step 2 with the new version
3. Update `BULMA_VERSION` and checksums (Step 3)
4. Run the app; review the [Bulma changelog](https://github.com/jgthms/bulma/blob/master/CHANGELOG.md) for breaking changes (Bulma 1.x is stable; minor releases are additive but always read the changelog)
5. Commit as a single changeset so the bump is traceable

## Pattern: Vanilla JS helpers (`app.js`)

Write this verbatim to `static/app.js`. It covers navbar burger, modal open/close, dropdown toggle, and tabs — Bulma's four interactive patterns. Zero dependencies, plain DOM API.

```javascript
// internal/ui/static/app.js
// Vanilla JS helpers for Bulma's interactive components.
// Bulma ships zero JS by design; this file wires the standard idioms
// documented at https://bulma.io/documentation/.

(() => {
  // Navbar burger: toggle .is-active on burger + matching menu.
  document.querySelectorAll('.navbar-burger').forEach((burger) => {
    burger.addEventListener('click', () => {
      const target = document.getElementById(burger.dataset.target);
      burger.classList.toggle('is-active');
      if (target) target.classList.toggle('is-active');
    });
  });

  // Modal: any element with data-target="modal-id" opens that modal.
  // .modal-background, .modal-close, .delete (inside .modal), and Escape close it.
  const openModal = (el) => el.classList.add('is-active');
  const closeModal = (el) => el.classList.remove('is-active');
  const closeAllModals = () =>
    document.querySelectorAll('.modal.is-active').forEach(closeModal);

  document.querySelectorAll('[data-target]').forEach((trigger) => {
    const target = document.getElementById(trigger.dataset.target);
    if (target && target.classList.contains('modal')) {
      trigger.addEventListener('click', () => openModal(target));
    }
  });

  document
    .querySelectorAll('.modal-background, .modal-close, .modal-card-head .delete, .modal .delete')
    .forEach((el) => {
      const modal = el.closest('.modal');
      if (modal) el.addEventListener('click', () => closeModal(modal));
    });

  document.addEventListener('keydown', (e) => {
    if (e.key === 'Escape') closeAllModals();
  });

  // Dropdown: click .dropdown-trigger to toggle, click outside to close.
  document.querySelectorAll('.dropdown:not(.is-hoverable)').forEach((dropdown) => {
    const trigger = dropdown.querySelector('.dropdown-trigger');
    if (!trigger) return;
    trigger.addEventListener('click', (e) => {
      e.stopPropagation();
      dropdown.classList.toggle('is-active');
    });
  });
  document.addEventListener('click', () => {
    document
      .querySelectorAll('.dropdown.is-active:not(.is-hoverable)')
      .forEach((d) => d.classList.remove('is-active'));
  });

  // Tabs: <div class="tabs"><ul><li data-tab="panel-id">...</li></ul></div>
  // Panels are any elements with id matching data-tab; only the active one shows.
  document.querySelectorAll('.tabs').forEach((tabs) => {
    const items = tabs.querySelectorAll('li[data-tab]');
    items.forEach((item) => {
      item.addEventListener('click', () => {
        items.forEach((i) => i.classList.remove('is-active'));
        item.classList.add('is-active');
        items.forEach((i) => {
          const panel = document.getElementById(i.dataset.tab);
          if (panel) panel.hidden = i !== item;
        });
      });
    });
  });
})();
```

## Pattern: Pair with templ

If `**/*.templ` files exist, render via templ components instead of `html/template`. The embed and handler steps are unchanged — only the rendering layer differs.

```go
// internal/ui/layout.templ
package ui

templ Layout(title string) {
    <!doctype html>
    <html lang="en">
    <head>
        <meta charset="utf-8">
        <meta name="viewport" content="width=device-width,initial-scale=1">
        <title>{ title }</title>
        <link rel="stylesheet" href="/static/bulma.min.css">
        <script src="/static/app.js" defer></script>
    </head>
    <body>
        <section class="section">
            <div class="container">
                { children... }
            </div>
        </section>
    </body>
    </html>
}
```

If templ is not yet set up in the project, defer to the **go-tool** skill to add it via `go get -tool github.com/a-h/templ/cmd/templ`, then return here.

## Pattern: Theming via CSS custom properties

Bulma 1.0+ exposes its design tokens as CSS custom properties (HSL components for colors, plus radii, fonts, spacing). Override them in a sibling `theme.css` loaded **after** `bulma.min.css`:

```css
/* internal/ui/static/theme.css */
:root {
  --bulma-primary-h: 217;
  --bulma-primary-s: 71%;
  --bulma-primary-l: 53%;

  --bulma-link-h: 217;
  --bulma-link-s: 71%;
  --bulma-link-l: 53%;

  --bulma-radius: 4px;
  --bulma-radius-small: 2px;
  --bulma-radius-large: 6px;

  --bulma-family-primary: system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
}
```

```html
<link rel="stylesheet" href="/static/bulma.min.css">
<link rel="stylesheet" href="/static/theme.css">
```

Add `theme.css` to the `//go:embed` directive. For the full list of custom properties, search `bulma.min.css` for `--bulma-` or see the [customize colors docs](https://bulma.io/documentation/features/customize-colors/).

Deeper customization (new component variants, removing unused modules to shrink the bundle) requires Sass and Node — **out of scope for this skill**. If the user asks for that, push back: the value proposition here is "no Node toolchain", and tree-shaking saves at most ~150KB on an already-cached file.

## Anti-Patterns

| Don't | Do |
|---|---|
| `<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bulma/...">` in rendered HTML | Download once at setup, embed, serve from `/static/` |
| `npm install bulma` + a Sass build step | Pure Go workflow — download `bulma.min.css`, write `app.js`, embed, done |
| Shipping unminified `bulma.css` in the binary | Always use `bulma.min.css` |
| Inline `<style>` blocks duplicating Bulma utilities | Use Bulma helper classes: `.has-text-centered`, `.mt-4`, `.is-flex`, etc. |
| Mixing Bootstrap and Bulma classes in the same page | Pick one. For Bootstrap, defer to the `html-first-frontend` agent |
| Forgetting `defer` on `<script src="/static/app.js">` | `defer` is required — the helpers query the DOM at load time |
| Reaching for jQuery or Alpine for navbar/modal | The scaffolded `app.js` already covers Bulma's four interactive patterns |

## Reference Layout

A completed setup looks like:

```
.
├── go.mod
├── main.go
├── BULMA_VERSION
└── internal/
    └── ui/
        ├── embed.go
        ├── render.go
        ├── static/
        │   ├── bulma.min.css
        │   ├── app.js
        │   └── theme.css          (optional)
        └── templates/
            ├── layout.html
            └── index.html
```

## Error Handling

| Condition | Action |
|---|---|
| No `go.mod` | Abort: "Not a Go module — run `go mod init` first." |
| Go version `< 1.16` | Abort: "`//go:embed` requires Go 1.16+. Update the `go` directive in go.mod." |
| `curl` returns non-200 for `bulma.min.css` | Verify the version exists on npm: `https://www.npmjs.com/package/bulma`. The release tag and the npm version should match. |
| User insists on CDN at runtime | Push back: the skill's reason for existing is offline, zero-dep, fully standalone binaries. If they still want CDN, decline and let them wire it manually without this skill. |
| Bulma assets already present in the repo | Skip download; verify checksums against `BULMA_VERSION` and proceed to wire the handler, write `app.js` if missing, and add the template. |

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…

Newsletter

Get the weekly list of safe new skills, and the ones we pulled.