@skewedaspect/nuxt-ui-sleekspace

Restyles Nuxt UI v4 with the SleekSpace design system: beveled corners, OKLCH ramps, glows, self-hosted Titillium Web, and runtime color schemes.
2,329
Install
npm install @skewedaspect/nuxt-ui-sleekspace
Latest Version:0.7.0
Vue:3
License:MIT
Last Updated:Oct 3, 2026
Links: GitLab  ·  npm
Maintainer: morgul

SleekSpace Theme for NuxtUI

npm

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, and neutral, 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-color custom property. Set it to retint a card or a panel.
  • Extra components - SleekSpinner and the SleekToolbar family 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 secondary
  • colorful - blue-tinted surfaces, orange primary, blue secondary
  • stranded - 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

tailwindcss-themeable

multiple themes support for tailwindcss, with Dracula built-in

7,097 59
@thespielplatz/tsp-tools-theme

tsp.tools design-system theme as a Nuxt layer (Nuxt UI v4 + Tailwind v4): amber-...

3,240
mage-obsidian

The MageObsidian JS build engine for Magento 2 frontends: theme inheritance reso...

4,758 1
@bopli/theme-cli

Create, validate, type, build, package, and locally preview independently deploy...

1,912 0
@wyxos/vibe

An initializable Vue 3 media feed with virtualized masonry and reel layouts.

29,315 2

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