Once UI in practice: how a backend engineer finally builds frontends he wants to look at
I was never a designer. Tables, forms, service layers, fine. But the moment it came to spacing, hierarchy, and colour, I started guessing. With Once UI that guessing stopped overnight.I was never a designer. Seven years of Java, Spring, databases, and architecture: anything that lives behind a surface, I could do. The moment it became about the surface, the awkward guessing started: are 16 pixels too many here? Should the button be rounded or square? Which grey doesn’t look cheap? The frontends I built in my first six years worked, but I rarely enjoyed looking at them.
Three weeks ago I migrated this site from a Vite + Tailwind combination to Next.js 16 with Once UI. Only then did I realise what I had been missing all along: not an eye for design, but a system that pre-decided the calls I’m not competent to make.
What Once UI is, and what it isn’t
Once UI is a React/Next.js component library from the Once Magazine team. It ships geometric primitives (<Column>, <Row>, <Grid>), content components (<Heading>, <Text>, <Button>, <Card>), and a theming system built on CSS custom properties. It’s actively developed. I moved from 1.6 to 1.7 mid-migration and nothing broke.
Important to understand: Once UI is not a page builder and not an all-in-one kit like Material UI. If you need filterable tables, datepicker dialogs, or complex forms, you’re building them yourself or pairing with a second library. What Once UI gives you is exactly what I had been missing, a grammar for the surface.
What makes the difference as a backend engineer
Spacing tokens. Spacing in Once UI is a discrete scale: 0, 1, 2, 4, 8, 12, 16, 20, 24, 32, 40, 48, 56, 64, 80, 104, 128, 160. Write gap="6" and you get a TypeScript error: doesn’t exist. At first this strictness annoyed me. Then I realised what it actually does: it removes the only call I always lost on when building frontends: which number is the right one. The answer is: one of the allowed ones. Done.
Theming as a data attribute. A single data-brand and data-accent attribute on <html> swaps the entire colour palette. The two colours of this site (blue as brand, cyan as accent) are defined once in the configuration file and referenced throughout the codebase via --brand-on-background-strong, --brand-alpha-weak, and similar custom properties. Want the whole site green tomorrow? One-line change.
The layout primitives. Once UI skips Tailwind-style utility classes. Instead I write markup like this:
That’s no longer flex flex-col items-stretch gap-6 p-8 max-w-2xl, it’s structured content. After three days I’m writing frontends I can read without opening a stylesheet next to them.
Where it still chafed
Honestly, because anyone else migrating will hit the same bumps:
- Responsive props aren’t reactive everywhere. The hide prop and similar conditional props work, but at breakpoints that didn’t always match my expectations. For the header switch between desktop and mobile, I ended up writing plain CSS media queries. Faster than reverse-engineering the library internals.
- Theme init without FOUC takes care. To stop dark mode from flashing light for 200 ms, a tiny inline script must run before hydration. The recommended place is the root layout, not the nested locale layout I tried first. A full walkthrough of that stumble is in my migration post (linked below).
- Learn the spacing scale. For three days I checked the TypeScript error list every other gap value to remember which step was next. Now the scale lives in my head.
The big win: I trust the result
Before Once UI I built frontends I hoped someone would like, but rarely felt confident about. Today I send mockups and don’t think “is this too much padding?” or “do these colours look cheap?”. The guardrails are built in. A layout built in Once UI with the standard token scale doesn’t look like a hobby build.
For a backend engineer that’s a release. I can focus on what I can do (architecture, data, logic) and the frontend carries itself, without me simulating designer intuition for two days per section.
Who Once UI is for
Exactly people like me: backend or full-stack engineers who want to build a marketing site, a portfolio, or a product surface without drifting into the designer role. It saves the mechanical work (layout, spacing, theme) without forcing you into a visual identity that isn’t yours.
The site you’re reading is the reference. If you stop scrolling for a moment (hero, audiences, pricing) you’re looking at Once UI components and their token system, since version 2.0 without any SCSS modules of my own. That’s all it is, and for someone without a design background it’s the most honest tool I’ve ever worked with.
If you want the concrete migration story (what actually broke when moving from Vite + Tailwind to Next.js 16 with Once UI) head to the migration post. That covers the how. This one was about the why.
Update, June 2026: now on 1.7.10
I wrote this post in May. The 1.7 line has developed steadily since then, and I am now on version 1.7.10. The updates have been pleasantly unspectacular: heavyweights like Recharts and PrismJS became optional dependencies, new components arrived, and not one of those updates broke my build. That is exactly what I want from a design system I rely on: small, continuous updates I can adopt without turning each one into its own migration project.
The real stress test, though, was not this portfolio. It was Aveiro: a complete AI publishing platform that I have built entirely on Once UI since the original post. The whole dashboard, the visual editor, every toolbar and panel, spread across roughly 960 files. That a small marketing site is pleasant to build this way was expected. That a product this size runs stably on it is the real proof. And it does.
Update, October 2026: Once UI 2.0 and no more SCSS
This site now runs on Once UI 2.0. I used the upgrade to remove my own SCSS modules entirely: the header with its mega menu, every homepage section, the blog, and the legal pages are now built from Once UI components only. What remains is a small global stylesheet for the font mapping and the skip link for keyboard users.
Moving from 1.7 to 2.0 took some work, but not much. A few examples from the migration: RevealFx delays are now given in milliseconds, the LayoutProvider comes from its own Next.js entry point, and some props were renamed, such as active instead of isActive. TypeScript pointed out each of these before it could show up in the browser.