**Phase 2c: no more Nuxt UI, the same site on plain Tailwind** #5

Merged
pjotr merged 6 commits from feat/phase-2c-no-nuxt-ui into main 2026-10-28 13:33:22 +00:00
Owner

The site used Nuxt UI for six things: buttons, the header navigation, two menu icons, a card, a placeholder block and the gallery lightbox. To get those, it pulled in a UI kit with an icon runtime, a colour-mode module, Headless UI, VueUse and four Tailwind plugins. After this PR, each of the six is a small plain component with Tailwind classes, and Tailwind is a direct dependency. No page looks different. Nothing changes on the server: same Dockerfile, compose file, pipeline and Traefik labels.

Commits:

  1. Buttons (elements/button.vue). The header's Inschrijven button, the content's Link buttons and the 404 button are now an <a> through NuxtLink, or a <button> when the content gives no link. They use the classes the Nuxt UI button rendered. The download="false" attribute is gone, along with the other props Nuxt UI leaked onto the link as HTML attributes (as, type, exact, exactquery, exacthash).
  2. Header nav and menu icons (main/header.vue, svg/icon_menu.vue, svg/icon_close.vue). The four nav links are a plain list of NuxtLinks with the same classes. The active link is the router's, as before: only a page's own link, so on an atelier page no link is active. The mobile menu's open and close icons are inline SVG, so no icon runtime is needed.
  3. Card and placeholder (elements/course.vue, elements/person.vue). The aanbod card was a transparent box with no padding, ring or shadow, so it is now a plain div. The placeholder for a teacher without a photo is a green div. No teacher is missing a photo today, so it does not show anywhere.
  4. Gallery lightbox (elements/images.vue), now a native <dialog>. While it is open, focus stays in the dialog and the rest of the page is inert. Esc or a click outside the image closes it, focus goes back to where it was, and the page does not scroll behind it. It has aria-modal="true" and a label ("Vergrote afbeelding"). The fade and zoom use the same classes and timings as before.
  5. Dependencies. @nuxt/ui is removed. @nuxtjs/tailwindcss (the same 6.13.1 that Nuxt UI used, with the same content globs) and @tailwindcss/typography (the same 0.5.16, for the prose text) become direct dependencies. Tailwind itself stays 3.4.17, and the lockfile has only removals. The last primary/gray classes become main/black, which are the same colours. The Tailwind config keeps gray pointing at the black palette, as Nuxt UI did, because Tailwind's own defaults read it: without it, the footer's 4 px bottom border turned light grey. darkMode: 'class' stays, so nothing ever turns dark. main.css takes over Nuxt UI's two global rules, the green focus colour for links and the green text selection. app.config.ts held only the Nuxt UI colours, so it is removed.
  6. README. A new stack table, and an updated structure table and intro line.

Smaller:

main this branch
Direct dependencies 4 5 (@nuxt/ui out; @nuxtjs/tailwindcss and @tailwindcss/typography in, both already installed through it)
Installed packages (pnpm list --depth Infinity, unique) 1019 993
node_modules 306 MB 293 MB
.output 136.6 MB 134.7 MB (public/: 106.9 → 106.8 MB)
Client JS (.output/public/_nuxt/*.js) 1,190 kB (367 kB gzip) 1,067 kB (328 kB gzip), -10 %
Home page HTML 268 kB, of which 105 kB inline CSS 191 kB, of which 32 kB inline CSS, -29 %

Checked locally with Node 22.14.0 and pnpm 9.1.2 (Corepack). Both builds were made from a clean checkout, main (66920d6) against the branch, and served side by side. They were checked in headless Chromium with third-party requests blocked in both (map tiles, Vimeo, TimeTree, MailerLite) and the home banner's frame timer stopped in both, so the only thing that differs is the code:

  • A clean pnpm install --frozen-lockfile and pnpm build pass.
  • Pixels: all 22 routes at 375 and 1280, full page (5 main pages, 15 ateliers, /nope, /aanbod/x/y). At 1280, all 22 are identical, 0 pixels. At 375, each page differs in 2 pixels, by 1/255: the anti-aliasing of the menu icon, which was a CSS mask and is now inline SVG. Every page has the same height in both builds.
  • The open mobile menu (375) and the open lightbox (375 and 1280) are identical too, apart from the same icon pixels (6 at most, 1/255).
  • Computed styles were compared for the nav links (active and inactive), the Inschrijven button, all 14 content Link buttons, a download button and the 404 button, each at rest, on hover and with keyboard focus. They are identical: colour, background, focus ring, outline, padding, size, position, and the underline under the nav links. The same holds for the card, the footer border, the text-selection colour and the focus colour of plain links. The only differences are the attributes removed on purpose (download="false" and the leaked props).
  • Behaviour, the same in both builds:
    • Nav active state on all 22 routes.
    • Inschrijven opens /inschrijven.
    • The internal Link buttons navigate inside the app. The external ones keep target="_self" and rel="noopener noreferrer". The 4 Link buttons without a link stay <button>.
    • The download buttons keep the file name.
    • The 404 button goes to the home page.
    • The mobile menu opens and closes with a click, Enter and Space, and its links work.
  • Lightbox:
    • The same image and alt open, with the same size and position at both widths.
    • The page does not scroll behind it, and the rest of the page is inert.
    • A click on the image keeps it open. Esc closes it, and so does a click outside.
    • Focus ends where it started, and the page works afterwards.
    • The fade-in and fade-out, sampled every 60 ms, follow the same curve.
    • What differs, and is the point of the change: the dialog now has a label, and focus sits on the dialog. Before, focus stayed on the page body and Tab went to a hidden helper button.
  • CSS classes: 49 class names that main used on a page lose their CSS rule in the branch. All 49 are Nuxt UI's own classes on the replaced components (for example bg-primary-200, dark:*, disabled:*, before:*, the icon classes and the modal's wrapper classes). None of them is still in the branch's rendered pages or source. No class used on a branch page lacks a rule that main had. 794 class names disappear from the generated CSS, mostly Nuxt UI's safelist and the styles of its components. None of them is in the branch's pages or source. The one name the check flags, @container, is a CSS at-rule in main.css, not the removed plugin's class.
  • UI scanner (GL-028): the same result on both builds for / (pass) and an atelier page with a gallery (the same 2 findings and 6 advisories in both).

Not checked:

  • Docker and Woodpecker (unchanged by this PR).
  • 768 px.
  • Real third-party content (maps, Vimeo, TimeTree, MailerLite were blocked in both builds on purpose).
  • Each intermediate commit was not built on its own; only the branch tip was.
  • Browsers other than Chromium. The lightbox now relies on <dialog>, which Safari supports from 15.4 and Firefox from 98.

Found, not changed:

  • The gallery thumbnails cannot be reached with the keyboard (a div with a click handler), so the lightbox opens only by mouse or touch. This is the same as on main.
  • The active nav link announces "Current page:" in English to screen readers and has no aria-current. Pressing Enter on a nav link drops focus to the page (blur()). Both are kept as they were.
  • border-accent-800 in the footer and bg-background in the header are classes Tailwind does not know, so they do nothing (also on main).
The site used Nuxt UI for six things: buttons, the header navigation, two menu icons, a card, a placeholder block and the gallery lightbox. To get those, it pulled in a UI kit with an icon runtime, a colour-mode module, Headless UI, VueUse and four Tailwind plugins. After this PR, each of the six is a small plain component with Tailwind classes, and Tailwind is a direct dependency. **No page looks different.** Nothing changes on the server: same Dockerfile, compose file, pipeline and Traefik labels. Commits: 1. **Buttons** (`elements/button.vue`). The header's Inschrijven button, the content's Link buttons and the 404 button are now an `<a>` through NuxtLink, or a `<button>` when the content gives no link. They use the classes the Nuxt UI button rendered. The `download="false"` attribute is gone, along with the other props Nuxt UI leaked onto the link as HTML attributes (`as`, `type`, `exact`, `exactquery`, `exacthash`). 2. **Header nav and menu icons** (`main/header.vue`, `svg/icon_menu.vue`, `svg/icon_close.vue`). The four nav links are a plain list of NuxtLinks with the same classes. The active link is the router's, as before: only a page's own link, so on an atelier page no link is active. The mobile menu's open and close icons are inline SVG, so no icon runtime is needed. 3. **Card and placeholder** (`elements/course.vue`, `elements/person.vue`). The aanbod card was a transparent box with no padding, ring or shadow, so it is now a plain `div`. The placeholder for a teacher without a photo is a green `div`. No teacher is missing a photo today, so it does not show anywhere. 4. **Gallery lightbox** (`elements/images.vue`), now a native `<dialog>`. While it is open, focus stays in the dialog and the rest of the page is inert. Esc or a click outside the image closes it, focus goes back to where it was, and the page does not scroll behind it. It has `aria-modal="true"` and a label ("Vergrote afbeelding"). The fade and zoom use the same classes and timings as before. 5. **Dependencies.** `@nuxt/ui` is removed. `@nuxtjs/tailwindcss` (the same 6.13.1 that Nuxt UI used, with the same content globs) and `@tailwindcss/typography` (the same 0.5.16, for the `prose` text) become direct dependencies. Tailwind itself stays 3.4.17, and the lockfile has only removals. The last `primary`/`gray` classes become `main`/`black`, which are the same colours. The Tailwind config keeps `gray` pointing at the black palette, as Nuxt UI did, because Tailwind's own defaults read it: without it, the footer's 4 px bottom border turned light grey. `darkMode: 'class'` stays, so nothing ever turns dark. `main.css` takes over Nuxt UI's two global rules, the green focus colour for links and the green text selection. `app.config.ts` held only the Nuxt UI colours, so it is removed. 6. **README.** A new stack table, and an updated structure table and intro line. **Smaller:** | | main | this branch | | --- | --- | --- | | Direct dependencies | 4 | 5 (`@nuxt/ui` out; `@nuxtjs/tailwindcss` and `@tailwindcss/typography` in, both already installed through it) | | Installed packages (`pnpm list --depth Infinity`, unique) | 1019 | 993 | | `node_modules` | 306 MB | 293 MB | | `.output` | 136.6 MB | 134.7 MB (`public/`: 106.9 → 106.8 MB) | | Client JS (`.output/public/_nuxt/*.js`) | 1,190 kB (367 kB gzip) | 1,067 kB (328 kB gzip), **-10 %** | | Home page HTML | 268 kB, of which 105 kB inline CSS | 191 kB, of which 32 kB inline CSS, **-29 %** | Checked locally with Node 22.14.0 and pnpm 9.1.2 (Corepack). Both builds were made from a clean checkout, `main` (`66920d6`) against the branch, and served side by side. They were checked in headless Chromium with third-party requests blocked in both (map tiles, Vimeo, TimeTree, MailerLite) and the home banner's frame timer stopped in both, so the only thing that differs is the code: - **A clean `pnpm install --frozen-lockfile` and `pnpm build` pass.** - **Pixels: all 22 routes at 375 and 1280, full page** (5 main pages, 15 ateliers, `/nope`, `/aanbod/x/y`). At 1280, all 22 are identical, 0 pixels. At 375, each page differs in 2 pixels, by 1/255: the anti-aliasing of the menu icon, which was a CSS mask and is now inline SVG. Every page has the same height in both builds. - **The open mobile menu (375) and the open lightbox (375 and 1280)** are identical too, apart from the same icon pixels (6 at most, 1/255). - **Computed styles** were compared for the nav links (active and inactive), the Inschrijven button, all 14 content Link buttons, a download button and the 404 button, each at rest, on hover and with keyboard focus. They are identical: colour, background, focus ring, outline, padding, size, position, and the underline under the nav links. The same holds for the card, the footer border, the text-selection colour and the focus colour of plain links. The only differences are the attributes removed on purpose (`download="false"` and the leaked props). - **Behaviour, the same in both builds:** - Nav active state on all 22 routes. - Inschrijven opens `/inschrijven`. - The internal Link buttons navigate inside the app. The external ones keep `target="_self"` and `rel="noopener noreferrer"`. The 4 Link buttons without a link stay `<button>`. - The download buttons keep the file name. - The 404 button goes to the home page. - The mobile menu opens and closes with a click, Enter and Space, and its links work. - **Lightbox:** - The same image and alt open, with the same size and position at both widths. - The page does not scroll behind it, and the rest of the page is inert. - A click on the image keeps it open. Esc closes it, and so does a click outside. - Focus ends where it started, and the page works afterwards. - The fade-in and fade-out, sampled every 60 ms, follow the same curve. - What differs, and is the point of the change: the dialog now has a label, and focus sits on the dialog. Before, focus stayed on the page body and Tab went to a hidden helper button. - **CSS classes:** 49 class names that `main` used on a page lose their CSS rule in the branch. All 49 are Nuxt UI's own classes on the replaced components (for example `bg-primary-200`, `dark:*`, `disabled:*`, `before:*`, the icon classes and the modal's wrapper classes). None of them is still in the branch's rendered pages or source. No class used on a branch page lacks a rule that `main` had. 794 class names disappear from the generated CSS, mostly Nuxt UI's safelist and the styles of its components. None of them is in the branch's pages or source. The one name the check flags, `@container`, is a CSS at-rule in `main.css`, not the removed plugin's class. - **UI scanner (GL-028):** the same result on both builds for `/` (pass) and an atelier page with a gallery (the same 2 findings and 6 advisories in both). Not checked: - Docker and Woodpecker (unchanged by this PR). - 768 px. - Real third-party content (maps, Vimeo, TimeTree, MailerLite were blocked in both builds on purpose). - Each intermediate commit was not built on its own; only the branch tip was. - Browsers other than Chromium. The lightbox now relies on `<dialog>`, which Safari supports from 15.4 and Firefox from 98. Found, not changed: - The gallery thumbnails cannot be reached with the keyboard (a `div` with a click handler), so the lightbox opens only by mouse or touch. This is the same as on `main`. - The active nav link announces "Current page:" in English to screen readers and has no `aria-current`. Pressing Enter on a nav link drops focus to the page (`blur()`). Both are kept as they were. - `border-accent-800` in the footer and `bg-background` in the header are classes Tailwind does not know, so they do nothing (also on `main`).
elements/button.vue renders an <a> through NuxtLink (or a <button> when
there is no link) with the classes the Nuxt UI button rendered: the
header's Inschrijven, the content's Link buttons and the 404 button.
The download="false" attribute and the other props Nuxt UI leaked onto
the <a> (as, type, exact*) are gone.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The desktop nav is a plain list of NuxtLinks with the same classes and
the same active state (the router's isActive). The mobile menu's two
heroicons are inline SVG components, so no icon runtime is needed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The aanbod card (UCard) was a transparent box without padding, ring or
shadow; it is a plain div. The person placeholder (USkeleton) is a div
with the same green.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Replaces UModal: showModal() keeps focus in the dialog and makes the page
inert, Esc and a click outside the image close it, focus goes back where
it was, and the page does not scroll behind it. aria-modal and a label
are set. The fade and zoom use the same classes and timings as before.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@nuxt/ui goes, and with it @nuxt/icon, @nuxtjs/color-mode, @headlessui/vue,
the iconify packages, VueUse and four Tailwind plugins. @nuxtjs/tailwindcss
(same 6.13.1, same content globs) and @tailwindcss/typography (same 0.5.16,
for prose) become direct dependencies; tailwindcss stays 3.4.17.

The last primary/gray classes become main/black (the same colours). The
Tailwind config keeps gray pointed at the black palette, as Nuxt UI did,
because Tailwind's defaults read it (the default border colour, the
placeholder colour). darkMode stays 'class'. main.css takes over the two
global rules Nuxt UI added: the link focus colour and the selection
colour. app.config.ts held only the Nuxt UI colours and is removed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
pjotr merged commit 93b5696370 into main 2026-10-28 13:33:22 +00:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
sintlukas/sla-website!5
No description provided.