@skewedaspect/nuxt-ui-sleekspace
| Install | |
|---|---|
npm install @skewedaspect/nuxt-ui-sleekspace |
|
| Latest Version: | 0.7.0 |
| Vue: | 3 |
| License: | MIT |
| Last Updated: | Oct 3, 2026 |
| Links: | GitLab · npm |
SleekSpace Theme for NuxtUI
Restyles Nuxt UI v4 with the SleekSpace design system: OKLCH color ramps, beveled corners, glow effects, colored 1px borders over same-hue fills darkened toward black, and Titillium Web.
Live demo: every themed component, with a scheme switcher.
Features
- Standard aliases -
primary,secondary,info,success,warning,error, andneutral, mapped to the SleekSpace ramps. Every Nuxt UI component accepts them. - Schemes - switch the entire palette at runtime with
useSleekScheme(). Four ship with the theme, and adding your own is one CSS file. - Per-component tinting - components derive their look from a single
--sleek-colorcustom property. Set it to retint a card or a panel. - Extra components -
SleekSpinnerand theSleekToolbarfamily cover two SleekSpace controls Nuxt UI lacks. - Self-hosted font - Titillium Web ships in the package, so nothing is fetched from Google.
- Dark only - SleekSpace is a dark theme by design. The color mode switch is ignored:
dark:utilities apply whatever mode is set, so a light toggle changes nothing.
Installation
npm install @skewedaspect/nuxt-ui-sleekspace
Requires Node 24 or newer, in a Vue app with Nuxt UI already set up.
Quick Start
Use the theme's Vite plugin in place of Nuxt UI's:
// vite.config.ts
import { defineConfig } from 'vite';
import vue from '@vitejs/plugin-vue';
import { sleekspace } from '@skewedaspect/nuxt-ui-sleekspace/vite';
export default defineConfig({
plugins: [
vue(),
sleekspace(),
],
});
sleekspace() adds Nuxt UI's Vite plugin with the theme applied, so don't also add ui() from @nuxt/ui/vite. It
takes the same options as Nuxt UI's plugin, and anything under ui merges over the theme's component defaults.
It also keeps this package out of Vite's dependency pre-bundling, adding to any optimizeDeps.exclude your config
already has. Nuxt UI keeps itself out of pre-bundling and loads reka-ui straight from node_modules; pre-bundling
this package would inline a second copy of reka-ui into it, and the two copies do not share contexts.
Import the stylesheet:
/* src/assets/main.css */
@import "@skewedaspect/nuxt-ui-sleekspace/css";
No @source line and no path into node_modules: the theme declares its own source, relative to its own
stylesheet. The stylesheet also declares the font, so there is no font link to add.
Then use Nuxt UI components as usual, with any of the theme's color aliases:
<UButton color="primary">Launch</UButton>
<UButton color="secondary" variant="outline">Abort</UButton>
<UAlert color="warning" title="Hull integrity at 40%" />
Nuxt
Nuxt is not this package's target, but a Nuxt app with Nuxt UI takes the same entries. List the package in
build.transpile: its components import a .vue file from Nuxt UI, and Nuxt otherwise leaves node_modules
packages for Node to load when it renders on the server, which Node cannot do with a .vue file.
// nuxt.config.ts
export default defineNuxtConfig({
modules: [ '@nuxt/ui' ],
css: [ '~/assets/css/main.css' ],
build: {
transpile: [ '@skewedaspect/nuxt-ui-sleekspace' ],
},
});
Hand the theme object to Nuxt UI through app config instead of a Vite plugin, and import the stylesheet from
app/assets/css/main.css the same way as above:
// app/app.config.ts
import { sleekspaceUI } from '@skewedaspect/nuxt-ui-sleekspace';
export default defineAppConfig({
ui: sleekspaceUI,
});
The Sleek components' icons render after hydration there, not in the server-rendered HTML.
Entry points
| Import | What it is |
|---|---|
@skewedaspect/nuxt-ui-sleekspace/vite |
sleekspace(), the Vite plugin |
@skewedaspect/nuxt-ui-sleekspace |
sleekspaceUI, the theme's component defaults, and the scheme list |
@skewedaspect/nuxt-ui-sleekspace/components |
the Sleek components and useSleekScheme() |
@skewedaspect/nuxt-ui-sleekspace/css |
the theme stylesheet, fonts included |
The plugin and the components each have an entry of their own. The plugin only runs in Node, and a Nuxt app bundles
the main entry into the browser through its app config. The components import a .vue file that only a bundler can
load, so they stay out of anything a Vite config imports.
Fonts
Titillium Web ships inside the package: the woff2 files, and the @font-face rules that point at them, under the
SIL Open Font License (src/assets/fonts/OFL.txt). Every face the family has is declared, split into latin and
latin-ext; a browser downloads a file only when the page renders that weight, style, and range, so the faces you
never use cost nothing.
The family has no 500, so the theme sets font-medium, which Nuxt UI uses throughout its stock component theme, to
400. That keeps a fallback font from drawing medium text heavier than the Titillium face that replaces it.
Icons
Nuxt UI bundles its own icons, and fetches any other icon from the Iconify API the first time it renders. To bundle the icons your source uses instead, have the Vite plugin scan for them:
sleekspace({
icon: {
clientBundle: { scan: true },
},
}),
The theme leaves this to you, since bundling affects your whole app rather than the theme.
Schemes
A scheme re-points the color aliases at different palettes with a single [data-scheme='name'] CSS block; nothing
else in the theme changes. Four ship with the theme:
greyscale(default) - grey surfaces, blue primary, orange secondarycolorful- blue-tinted surfaces, orange primary, blue secondarystranded- industrial military green with amber accents, green-tinted surfaces (ported from RFI: Stranded)sage- brand red primary, gold secondary, charcoal surfaces (ported from SAGE)
The useSleekScheme() composable returns the active scheme as a writable ref plus the built-in scheme list, ready
to bind to a select:
<template>
<USelect v-model="scheme" :items="schemes" />
</template>
<script setup lang="ts">
import { useSleekScheme } from '@skewedaspect/nuxt-ui-sleekspace/components';
const { scheme, schemes } = useSleekScheme();
</script>
The choice persists in localStorage under sleek-scheme, and the composable writes it to data-scheme on
<html>.
Adding a scheme
A scheme is a CSS file that sets the alias variables inside a [data-scheme] block. Two patterns work.
Derive ramps from base colors. Set one base color per alias and let CSS relative color syntax generate the
full 50-950 ramp. Copy src/assets/css/schemes/stranded.css and change the
base colors:
[data-scheme='mars'] {
--scheme-primary: oklch(0.55 0.12 40);
--ui-color-primary-50: oklch(from var(--scheme-primary) calc(l * 1.334) calc(c * 0.323) h);
--ui-color-primary-100: oklch(from var(--scheme-primary) calc(l * 1.29) calc(c * 0.417) h);
/* ...through -950, repeated for each alias */
}
Any shade can be hand-tuned instead of derived, since a scheme is just CSS variables.
sage.css writes its neutral ramp as explicit hex values so the 900 shade lands
exactly on the brand charcoal.
Re-point the built-in ramps. Point the aliases at the ten SleekSpace ramps that ship with the theme, as
colorful.css does:
[data-scheme='matrix'] {
--ui-color-primary-50: var(--color-sleek-green-50);
--ui-color-primary-100: var(--color-sleek-green-100);
/* ...through -950, plus any other aliases you want to re-point */
}
The ramps are --color-sleek-blue-*, cyan, gray, green, mint, orange, pink, purple, red, and
yellow, each in shades 50 through 950.
Import the file after the theme's stylesheet, so its block wins over the theme's defaults:
@import "@skewedaspect/nuxt-ui-sleekspace/css";
@import "./schemes/matrix.css";
Then offer it in your picker. scheme accepts any value, so the list is the only thing to extend:
import { sleekspaceSchemes, useSleekScheme } from '@skewedaspect/nuxt-ui-sleekspace/components';
const { scheme } = useSleekScheme();
const schemes = [ ...sleekspaceSchemes, { label: 'Matrix', value: 'matrix' } ];
Custom Colors
Components derive their fills, borders, text, and glow from one --sleek-color custom property. Setting it retints
the whole component. With Tailwind's arbitrary-property syntax:
<UCard class="[--sleek-color:var(--ui-secondary)]">
Tinted with the secondary color.
</UCard>
<UCard class="[--sleek-color:var(--color-sleek-purple-500)]">
Tinted with a specific ramp shade.
</UCard>
Corner Flags
A corner flag is a solid triangle filling one of an element's beveled corners. Its long edge runs parallel to the
bevel with a small gap between them, so the element reads as a rectangle with a colored corner. Use it as a status
indicator on a button, in place of wrapping the button in UChip.
sleek-flag-tl draws the flag in the top-left corner and sleek-flag-br in the bottom-right, the two corners where
a button is cut. Toggle it with a class binding:
<UButton
size="sm"
icon="i-lucide-scroll"
:aria-label="needsAttention ? 'Missions (needs attention)' : 'Missions'"
:class="{ 'sleek-flag-tl sleek-flag-warning': needsAttention }"
/>
The flag is visual only, like UChip's dot, so give screen readers the status another way, as the aria-label
above does. It can't show a count; for a number, use UChip's text prop.
A flag is primary unless a color class sets it: sleek-flag-primary, sleek-flag-secondary, sleek-flag-info,
sleek-flag-success, sleek-flag-warning, sleek-flag-error, or sleek-flag-neutral. sleek-flag-neutral uses
the same lightened neutral as a neutral button's border.
Add sleek-flag-breathe next to a flag's classes, as in sleek-flag-tl sleek-flag-warning sleek-flag-breathe, to make
it pulse. The triangle gets a 3px glow in the flag's color and eases down to 40% opacity and back on a 3-second cycle.
When the user's system asks for reduced motion (prefers-reduced-motion: reduce), the flag doesn't pulse and keeps its
glow at full strength.
Tailwind generates a utility only for a class name it finds written out in full in your source, so a class built at
runtime such as sleek-flag-${ color } has no styles. Write each name out, for example in an object that maps colors
to classes.
Like the bevels, the flag is drawn with corner-shape; see Browser Support.
Flags on other elements
The triangle takes its size from the element's --sleek-cut custom property, which holds the bevel depth. Every
UButton size sets it, so a button needs nothing more. Any other beveled element can take a flag once it sets
--sleek-cut to its own bevel depth, e.g. [--sleek-cut:16px]. The property inherits: an element that doesn't set
it picks up an ancestor's value (cards set 16px), and its flag comes out the wrong size.
The flag classes also set position: relative on the element, and they position the triangle for a 1px border,
which every button has.
Components
Nuxt UI has no equivalent for two SleekSpace controls, so the theme ships them. Import them from
@skewedaspect/nuxt-ui-sleekspace/components. Their styles come from the theme stylesheet, so there is no second
CSS import to remember.
SleekSpinner
A loading indicator: dual counter-rotating arcs around a pulsing core. The dots variant renders three pulsing
beveled squares, and crosshair renders rotating targeting brackets.
| Prop | Values | Default |
|---|---|---|
size |
xs, sm, md, lg, xl |
md |
color |
the seven color aliases | primary |
variant |
circular, dots, crosshair |
circular |
label |
screen-reader text | Loading |
<SleekSpinner />
<SleekSpinner variant="dots" color="secondary" />
<SleekSpinner variant="crosshair" size="xl" color="error" />
SleekToolbar
A toolbar family wrapping reka-ui's Toolbar primitives: SleekToolbar, SleekToolbarButton,
SleekToolbarSeparator, SleekToolbarToggleGroup, and SleekToolbarToggleItem.
SleekToolbar takes color (default neutral), orientation (horizontal or vertical), and corners, an
array of top-left, top-right, bottom-right, and bottom-left naming which corners get the bevel cut (all
four by default). Buttons take icon and disabled. Toggle groups take a v-model, type (single or
multiple), and disabled; toggle items take value, icon, and disabled.
<template>
<SleekToolbar color="primary">
<SleekToolbarToggleGroup v-model="formatting" type="multiple">
<SleekToolbarToggleItem value="bold" icon="i-lucide-bold" aria-label="Bold" />
<SleekToolbarToggleItem value="italic" icon="i-lucide-italic" aria-label="Italic" />
</SleekToolbarToggleGroup>
<SleekToolbarSeparator />
<SleekToolbarButton icon="i-lucide-undo" aria-label="Undo" />
</SleekToolbar>
</template>
<script setup lang="ts">
import {
SleekToolbar,
SleekToolbarButton,
SleekToolbarSeparator,
SleekToolbarToggleGroup,
SleekToolbarToggleItem,
} from '@skewedaspect/nuxt-ui-sleekspace/components';
import { ref } from 'vue';
const formatting = ref<string[]>([]);
</script>
Browser Support
Beveled corners use CSS corner-shape: bevel, currently shipped in Chromium. Safari and Firefox have implementations
in their nightlies. Browsers without it render rounded corners instead.
Development
npm install # install dependencies
npm run dev # playground at localhost:3040
npm run lint # check style and correctness
npm test # resolve the theme and check it against snapshots
npm run build:lib # build dist/
The tests resolve every src/theme.ts override against Nuxt UI's stock theme the way the components do, then
snapshot the classes that come out. Shared helpers feed dozens of entries, so this is what catches a change to one
turning up somewhere nobody was looking. Update them with npx vitest -u once you have checked the diff. npm test
first writes the stock themes, which Nuxt UI's Vite plugin generates when its config loads.
dist/ is built by prepack, so npm pack and npm publish produce it without anyone remembering to.
node scripts/consumerSmokeTest.ts packs the result and builds apps against the tarball, which is the only check
that looks at what consumers actually download.
The playground is a Vite app with a page for each group of themed components: elements, forms, data, navigation, overlays, site, shell, dashboard, chat, and editor.
Releases are cut with npm run release -- <patch|minor|major>; see docs/releasing.md.
License
MIT © Christopher S. Case
Related Packages
tsp.tools design-system theme as a Nuxt layer (Nuxt UI v4 + Tailwind v4): amber-...
The MageObsidian JS build engine for Magento 2 frontends: theme inheritance reso...
Create, validate, type, build, package, and locally preview independently deploy...
Version History
| Version | Released | License |
|---|---|---|
| 0.7.0 | MIT | |
| 0.6.4 | MIT | |
| 0.6.3 | MIT | |
| 0.6.2 | MIT | |
| 0.6.1 | MIT | |
| 0.6.0 | MIT | |
| 0.5.0 | MIT | |
| 0.4.1 | MIT | |
| 0.4.0 | MIT | |
| 0.3.0 | MIT | |
| 0.2.0 | MIT | |
| 0.1.1 | MIT | |
| 0.1.0 | MIT |