Hygguile

cozy and professional user-interfaces for everyone

Hygguile is an opinionated, batteries-included UI framework written in Guile Scheme. It gives you a small, expressive domain-specific languageDSL for building cozy and professional interfaces from symbolic expressionsS-expressions, Scheme XML: XML written as S-expressionsSXML and modern Cascading Style SheetsCSS.

It is built on Olive CSS: a utility-class framework whose syntax will feel familiar to anyone who has used Tailwind, but which is plain Cascading Style SheetsCSS rather than JavaScript.

Every component renders to real HyperText Markup LanguageHTML on the server and is enhanced in the browser only where that helps. Pages stay fast, accessible, and fully usable without JavaScript.

Order

Typography

Type⌄

The scale is a handful of headings, a lighter subheading, body copy with its inline elements, and the two faces the framework sets code in.

Headings

h1 · text-4xl · bold

Cozy by default

h2 · text-3xl

Settings and preferences

h3 · text-2xl

A card title

h4 · text-xl

A quieter heading

Secondary and serif

sub-h3 · italic · light

A lighter secondary heading

h2 · font-serif

Read slowly

h3 · font-serif

An editorial note

a · underline

Read the API documentation for the details.

Body and inline elements

p · strong · em · mark

Cozy copy with importance, emphasis and a highlight.

kbd · samp · var · abbr · sub · sup

Press Ctrl then l, read ok, export GC_ROOT, meet HTML, or write H2O and E = mc2.

Quotations

blockquote · cite

Good design is as little design as possible.

Dieter Rams
a quotation that links its source

Simplicity is about subtracting the obvious and adding the meaningful.

Mono and code

mono

$ guix shell -m manifest.scm

code

Call (render page) from the request handler.

Preformatted

;;; A simple Guile Scheme HTTP server
(use-modules (web server))

(define (my-handler request request-body)
  (values '((content-type . (text/plain)))
          "Hello World!"))

(run-server my-handler)

Cards

Layout⌄

Header, body and footer

Card title

New

Cards group related content with a header, a body and a footer.

1,204Active users+12%

An expandable card

Opens in place

Layout⌄

The card this page is built out of: a native disclosure whose header is the control, so it opens and closes without a line of script.

Badges

A badge is a short label with a semantic tint; a card header is a natural home for one.

neutralinfosuccesswarningdanger

Containers

Layout⌄

first element

second element

third element

first element

second element

third element

Glass panels

A glass panel is a surface you can see through: a translucent tint over a backdrop blur, so what sits behind the panel is softened rather than hidden. Nothing in it is cut short by default -- a panel decides that with #:clip? -- and the ink of a line flush with the glass stays whole. The washes are the wallpaper trick -- a few oversized, blurred discs of colour thrown across the glass -- and they are optional, as is their drift.

Static washes

The discs are there, but nothing moves. This is the panel without #:drift? -- the washes are simply still.

Drifting washes

The same panel with #:drift? #t, and a pace and a distance of its own: #:pace and #:distance are the page's to choose, so these discs cross their corner in a few rems while the hero's wander for half a minute.

Frosted, not opaque

Any content can live behind the glass.

Colors

Foundations⌄

Hygguile offers colors in the 100, 300, 500, 700 and 900 variants. Some of these colors are essential for the hygguile components, however not all of them are.

Gradients

Foundations⌄

A gradient is a container whose background image you describe with colour steps and a direction. Stops accept any CSS colour, oklch with an alpha channel included, so a gradient can stay inside the same palette as the rest of the interface.

hygge morning
warm horizon
coastal dusk
linden light
tyrian velvet
dusk in onyx
mint frost
fjord morning
northern lights

Pass a list of stops and a direction and you have a panel; the same call is the slide behind a headline, or a hairline of colour (this page marks every section with one) — the container classes decide which.

hygge morning

Layout & utility

Layout⌄

Structural primitives that keep spacing and responsive behaviour consistent.

Stack

Account

Signed in as Ada.

Cluster

onetwothree

Center

A measured reading width, centred in its column.

Aspect ratio

16 / 9

Grid

A cell

Another cell

Split

Start

End, two thirds

Utility & composition

Clipboard

Density

Truncation

The quick brown fox jumps over the lazy dog, and then keeps going for long enough that a card would grow a second and a third line if nothing stopped it.

Motion

Fade in

Reads the motion tokens, so reduced-motion turns it off.

RTL and language

مرحبا بالعالم

Responsive

Resize the window: only one of these badges is shown.

Narrow screens only

Buttons

Controls⌄

Every variant is a native <button>: the fill and the shadow are utilities, the label and the focus ring come from the element itself.

Variants

Form buttons

Icon and label

Tonally deep fills

Form fields

Controls⌄

A field is a native control with a bound label, helper copy and an error slot; the whole form below is server-rendered markup, not a widget.

Wired by the form-field abstraction.

The reveal control is a progressive enhancement, not the field.

  • SSR

A native number input with explicit bounds.

€/ month

The prefix and the suffix are written beside the value, not inside it.

Enter the code we sent you.

21 °C

The number beside the field is the value, written out.

How was your stay?
Average score

A read-only rating is one image with one accessible name.

Range
View
Stay

Forms

Controls⌄

Browsers and password managers can fill this in.

A select states its own selection: exactly one option carries the selected attribute, written from the value the server rendered. The rows here are built with symbols and compared with strings, which used to lose the selection between the two; an option whose value the menu does not have falls back to the first row a reader can actually pick, and rows grouped under an optgroup are still rows -- the headings are the browser's own, and the group a reader cannot choose never holds the selection.

A form is the wrapper for the most common case of all: the browser serialises the fields and reloads the page with the answers. The labels, the native validation and the submit button are all markup, so the form still works with JavaScript disabled.

The example posts nothing: this showcase is a static page, so it uses a harmless GET. A real signup would post to your own endpoint.

Subscribe to the newsletter

Used once, to confirm the subscription.

You can change this later, at any time.

Validation

Controls⌄

A field is in one of three states, and says which: nothing yet, wrong, or right. An error is announced and named (aria-errormessage as well as aria-describedby); a success is announced politely; and the control's own border follows the state, so a reader can find the one field that needs them without reading every line.

Looks good.

The form above checks itself in the browser too: #:validate? borrows the browser's verdict and writes it into the field's error slot, so the page's own sentence is the one a reader reads. Nothing about it needs a script to be safe -- with scripting off the browser still refuses the form, with its own bubble.

No state yet: a field is not wrong before it is filled in.

Looks good.

Range

Disclosure

Controls⌄
Native details element

Built on the browser's own disclosure widget.

Zero JavaScript

Accordions keep working with scripting disabled.

Open a CSS-only modal

Hover or focus for a tooltip:

Enter a reproducible development shellguix shell

Detail lists

Data⌄
foo:
bar
abcdefghij:
1234567890
some key:
some value

Data tables

Data⌄

Tabular data deserves real table markup: columns are declared with <th> and an explicit scope, the rows live in <tbody>, and the totals sit in <tfoot>, so the header that describes a cell is never guessed by a screen reader.

A wide table is also the classic way to break a phone layout. Every table therefore arrives wrapped in a region you can scroll sideways, named for assistive technology and reachable with the keyboard.

Café takings, week of 5 October 2026
DayCoffeesPastriesTakings
Monday4218€ 248.50
Tuesday5122€ 301.75
Wednesday4719€ 274.20
Thursday5826€ 349.90
Friday7331€ 438.05
Week271116€ 1 612.40
Plans
PlanPrice
Cozy9
Calm14
Total23
Sorting without JavaScript

Sorting is not a rendering trick, it is a question a page asks the server: in which order? A sortable header cell is therefore an ordinary link that carries the order in the query string, and the cell announces the state it is in with aria-sort. Clicking a column is a plain navigation: it works with JavaScript disabled, a reload reproduces the order, and the URL can be shared or bookmarked.

Only a server can answer that link, and this showcase is a static page, so what you see below is the markup it produces: each header carries ?hygguile-table-sort= and ?hygguile-table-dir=, and the current column says which way it is going. The names are prefixed so that a sort or a dir parameter of the page's own is never read as the table's state. examples/sortable-table.scm runs the other half -- a server that reads the order back with parse-sort and re-renders the rows in it.

Plans, sorted by seats
PlanSeatsRevenue
Cozy3€ 27
Calm14€ 196
Roomy9€ 81
Plans
PlanSeats
Cozy3
Calm14

Data display

Data⌄
SchemeDraft×Published
ABCDEFGH+1
  1. Build started

  2. Tests passed

  3. Deployed

Carousel

Media⌄

Media

Media⌄
Hygguile logo
figure + img + figcaption
WikiMusic
picture with source + fallback img
HG

avatar with initials fallback

ByggstegLucidPlanStatic Site Starter

Navigation

Navigation⌄
Hygguile

Tabs are pure CSS: no JavaScript required.

Selecting a label reveals the matching panel.

Fly-out

Navigation⌄

Overlays

Application⌄

Drawers and the command palette open from the URL; popovers and dropdowns use the browser's own overlay machinery. The menu here starts open so you can see it.

Triggers

Hover meA hover card

Richer than a tooltip.

An open menu

Actions

Settings

Application⌄

The theme toggle rides at the end of the section rail at the top of the page: the theme is a class on the document, remembered in local storage, and a setting for the whole document belongs with the page's own navigation rather than inside one of its cards.

The notification tray below is the other setting worth trying: a plain list that slides in from the corner. Both are ordinary HTML and CSS with a little script on top, so nothing blocks the first paint.

Feedback & states

Application⌄

Alerts

Information

Alerts carry a semantic color and an ARIA role.

Success

Your changes were saved.

Warning

This action is irreversible.

Progress, skeleton and spinner

Progress65%
Loading

The spinner is a CSS ring and the bars are a quiet placeholder.

Empty, error and loading shapes

Nothing here yet

An empty list is not an error.

Could not load

The request failed.

Try again
68%Importing
Working
Loading

A toast

Rendered by the server, queued by the script.

Why server-side rendering?

Overview⌄

Hygguile renders complete HTML on the server. The browser receives a finished document — headings, links, labels and controls included — and a little JavaScript enhances it afterwards.

"Old school" server rendering is not nostalgia. For the kinds of software most of us ship — content, forms, dashboards, admin panels — rendering on the server keeps the fast path fast and the simple path simple.

First paint without the framework tax
The HTML is ready to paint. There is no framework bundle to download, parse and execute before the first pixel, so pages feel immediate even on slow connections and modest hardware.
No hydration, no double work
Nothing is re-rendered on the client just to attach behaviour. The server has already produced the final tree, so there is no hydration step that can disagree with the markup.
It works when JavaScript does not
Links navigate and forms submit. Disclosure, tabs, the modal and tooltips keep working with scripting disabled; the theme toggle, carousel arrows and live notifications simply step in when they can.
Accessible and discoverable by default
Real headings, landmarks, labels and form controls are present in the first byte, so assistive technology and crawlers never wait on a client-side rendering race.
One source of truth
State lives in the database and in the URL, not in a second client-side store. There is no model to duplicate, no cache to invalidate, and no stale-UI bug class caused by the two drifting apart.
Cacheable and cheap to serve
A page is a pure function of its request, which makes responses easy to cache, compress and proxy. Payloads stay small: mostly text, styled by one stylesheet the browser already holds.
A smaller, calmer stack
No client router, no global store, no build step that has to agree with the server. The mental model is the one the web began with: request a URL, receive a document, follow a link.

Single-page applications earn their complexity when you genuinely need offline support or a deeply interactive workspace. Even then it is a spectrum rather than a binary: ship mostly-static HTML and add interactivity only where it repays its cost. That is exactly how Hygguile is built — a document first, with small islands of behaviour.

;; A page is a pure function of its data
(define (page request)
  `((h1 "Posts")
    (ul ,@(map render-post (posts-for request)))))

;; Render it once, on the server, and send text to the browser