Devlog · Phase 0 · 6/7
Verre, Aubia's Design System
Verre replaces aubia.dev's former design and will also serve aubia.cloud and the desktop application: two themes in neutral grays, gray glass, Geist and components drawn from a registry.
by Mike, founder of Aubia Published on Updated on 9 min read
- design-system
- verre
- tailwind
- motion



The dark background on aubia.dev is an almost-black gray, #0E0F12, without the blue tint it had when this article was first published. The light theme, "dawn", is based on a very pale gray, #EDEFF2. Both come from Verre, Aubia's new visual identity, whose mockup I approved on October 5, 2026.
In August, this article described a different site: two complete Catppuccin themes, glass that let the background's pools of color show through, and Chakra Petch, an angular typeface reserved for aubia.dev's headings. Each Aubia app defined its own colors and its own components, and the desktop application planned four theme variants while the site offered two. Verre gives the three apps a single identity, and the site is the first to apply it.
From Luma, the shadcn/ui component style, I took the border radii, spacing, elevation and state animations. The material, colors, typography and icons are Verre's own.
A shadcn Registry Supplies the Components
Verre's components are in a private shadcn registry, served from GitHub. An app doesn't install them as a dependency: the shadcn add command copies their source code into its repository, alongside the rest of its code. I ruled out a versioned npm package: it would have shipped compiled code in place of these sources, and I would have had to publish and secure one more private npm registry.
I installed 91 registry items in the site. A manifest records the SHA-256 hash of every copied file, and a test checks that the copy stays identical to the source. For that check to hold, a copy is never edited in place: any style adjustment goes in a separate stylesheet, imported after the registry's stylesheets.
These components are built on Base UI 1.9.0, which shadcn now recommends instead of Radix. Base UI handles focus, keyboard navigation and ARIA roles. A link styled as a button goes through a separate ButtonLink component, because the Button primitive would force the button role onto a link.
The icons come from Hugeicons, in the Stroke Rounded style, under the MIT license. I chose them over Font Awesome Pro: it requires a paid license, and the approved mockup is drawn with Hugeicons.
Two Themes, Following the System by Default
Verre defines a light theme, "dawn", and a dark theme, "Night". The dark theme uses neutral grays for surfaces, lines and text. From Catppuccin, the old site's palette, Verre keeps the accents: Latte's peach in the light theme, #FE640B, Mocha's in the dark theme, #FAB387, and, on the home page, the teal and lavender of the section headings and the gradient of the main heading. Peach is used for links, bullets and the keyboard focus outline.
The colors no longer form a complete palette exposed as classes. They are named by role, such as background, glass, text or accent, and every role gets a value in each of the two themes. An @theme inline block maps these names to shadcn's and to Tailwind's utilities: a component uses bg-background or text-muted-foreground, and the value follows the displayed theme.
I kept the mockup's colors as they are, even where some text falls below the WCAG AA contrast thresholds, in both themes: the design system flags these low contrasts without correcting them. The other accessibility requirements remain: keyboard navigation, visible focus, touch targets of at least 44 px on screens narrower than 768 px, a skip link.
The former design opened the site in the dark theme, whatever the visitor's preference. I made the system theme the default: you've already told your OS what you prefer, and the same choice will apply in all three apps. The switcher still offers "Light", "Dark" and "System".
The Theme Is Known Before the First Render
A script in the <head>, placed before the stylesheets, reads the choice saved under the aubia-theme key and the system preference. It sets data-theme for an explicit choice and data-scheme for the preference: the CSS applies the right theme without waiting for React. Without JavaScript, the prefers-color-scheme media query applies the dark theme when the system asks for it. If local storage is blocked, your choice only lasts for the current visit.
The theme change itself is animated. After a click on the switcher, the page waits 200 ms, long enough for the switcher's indicator to slide onto the new choice, and then the new theme is revealed over 650 ms in a circle expanding from the button, using the View Transition API. In "System" mode, a change in the preference fades the whole page. When the system asks for reduced motion, the theme switches at once.
The CSS for "System" mode reads data-scheme rather than the media query. Chromium already shows the new preference's colors when it fires the change event: if the page followed the media query, it would have changed before the script captured the transition's starting state.
The Cards Reuse the Bar's Gray Glass
The glass effect comes from the backdrop-filter property: it blurs whatever passes behind an element, and a semi-opaque background tints the result. In the former design, this filter also saturated the colors, and those of the background's pools came through the cards. The design system's regular glass, used by select menus and notifications, still saturates, at 160%, with a 24 px blur. The navigation bar's glass blurs just as much but removes all color:
@utility surface-bar {
background: var(--bar-glass);
border: 1px solid var(--glass-line);
backdrop-filter: var(--bar-blur);
}
--bar-blur is blur(24px) grayscale(100%), and --bar-glass is white at 80% opacity in the light theme and gray at 70% in the dark theme: the background's halos of light no longer tint this glass. On the site, the cards, the FAQ questions and, in the articles, the cover and the tables reuse it, with no sheen when the pointer passes over them.
The Language Menu Renders Outside the Bar
When an ancestor already has a backdrop-filter, it establishes a backdrop root: a descendant's filter can no longer see what lies behind that ancestor. The bar's language menu, a Base UI menu, is rendered in a portal: the bar is no longer its ancestor, and the menu applies its own blur. The bar's glass is defined on the document root so that this menu can reach it too.
This menu mounts only on the client, so its links are missing from the initial HTML. The footer therefore repeats these links in a native <details> element: they appear in the HTML produced by server-side rendering, and opening it works without JavaScript. React adds arrow-key navigation and closing with Escape or an outside click, and moves focus to the current language when it opens.
Halos and a Star Pattern Under the Content
The page background is made of two fixed layers. The first draws an ellipse of light in each corner, in the color the logo's mark takes in that same corner: violet at the top left, pink at the top right, coral at the bottom left, light peach at the bottom right, more subdued in the dark theme than in the light one. The second is a pattern of 7 px shapes, sun-stars in the light theme and stars in the dark theme, revealed by the light of the halos. Its spacing, close to 32 px, divides the content width exactly: a column of shapes lines up with each of its edges.
Across the site, every 0.3 to 0.9 seconds, a shape grows and takes on one of the theme's hues, up to six at a time, and a magnifier lights up the shapes near the pointer. The animation loop stops when the tab is hidden.
Geist for Text and Code Alike
I removed Chakra Petch. Geist is now used for all text, the home page's main heading included, and Geist Mono for code, paths and commands. Both fonts are variable: a single Latin WOFF2 file per font covers weights from 100 to 900.
They are self-hosted. The fonts plugin in laravel-vite-plugin reads the files from the installed Fontsource packages and generates the @font-face declarations at build time; the Blade @fonts directive inserts them, along with their preloads, into every page. No request goes out to a font provider.
With display: 'swap', text first appears in a fallback font. For Geist, the plugin adjusts that fallback's metrics, and the text barely reflows when the font arrives. Geist Mono keeps a plain monospace fallback: the plugin looks in the file for the font's category, monospace or sans-serif, finds neither and would base the fallback on Arial.
Motion, and a Single Rule to Reduce It
Durations and easing curves come from CSS variables, --d-* and --ease-*, read by the stylesheets and the scripts alike. The micro-interaction curve, --ease-out, is cubic-bezier(.2, .8, .2, 1).
On load, no visible text starts at zero opacity, because Chromium excludes an invisible element from LCP. The home page's main heading enters with a 14 px slide. Fading is reserved for text revealed on scroll, further down the page, and for decorations.
Before Verre, the reduced-motion rules targeted certain animations and let the other transitions play. Verre replaces them with a single one, for the whole site:
@media (prefers-reduced-motion: reduce) {
*,
*::before,
*::after {
transition: none !important;
animation: none !important;
}
}
Every state then appears immediately. Colors still change, all at once; movements on hover and on press go through Tailwind's motion-safe: variant, which no longer applies.
Script-driven animations, such as the home page's tagline, whose phrases appear character by character, fall outside this rule. They read the preference through the registry's useReducedMotion hook, built on useSyncExternalStore: it subscribes to matchMedia's change event, and changing the setting during a visit immediately stops the running animation. The server doesn't know this preference. The hook therefore returns false for server rendering and during hydration, then reads the actual value, as described in React's documentation for useSyncExternalStore. The server HTML already contains the last phrase, and the tagline keeps it under reduced motion.
Code in the Articles Keeps Catppuccin's Colors
Code blocks on this blog are highlighted on the server by Phiki 2.2, at build time, with the Catppuccin Latte theme for light and Mocha for dark. A PHP class copies these two themes and replaces their neutral colors, those of the background, text, comments and punctuation, with Verre CSS variables; it fails if a Phiki upgrade removes one of the targeted color rules, rather than shipping an incomplete theme. A code block therefore takes the background of the displayed theme, and only its syntax colors still come from Catppuccin.
Aubia's beta is free, with no time limit. Sign up to receive your invitation.
Join the waitlist