Skip to main content
Nexus Design System
GitHub

Appearance

Appearance is one object that sets your app's whole theme: colors, density, corners, shadows, borders and font preferences.

@nxds/core turns that object into the theme's CSS. The examples use the React provider or the Vue plugin you copied in Install.

Fields

Color is one setting out of many. Density, corners, shadows, border width, type and motion are settings too, so "make this product denser" is a one-field change, not a refactor.

Every option ships in the stylesheet. The provider applies colors as CSS variables, mode as the .dark class, and density, corners, shadows and border width as data attributes. Switching an option needs no rebuild.

FieldValuesSets
mode'light', 'dark', 'system'Light or dark. system follows the operating system.
brandColorAny CSS color. Default #0a0a0a.The primary color.
surfaceTone'stone', 'neutral', 'zinc', 'slate', 'gray'. Default 'zinc'.The gray used for backgrounds.
lightContrast / darkContrast0 to 100. Default 50.How strong text and borders are, separately for each mode.
density'tight', 'compact', 'default', 'comfortable', 'relaxed', 'spacious'. Default 'default'.Spacing.
corners'square', 'subtle', 'smooth', 'round', 'extra-round'. Default 'subtle'.Corner radius.
elevation'flat', 'quiet', 'soft', 'standard', 'strong'. Default 'quiet'.Shadows.
stroke'fine', 'normal', 'strong'. Default 'normal'.Border width.
prefsNxdsAppearancePrefsFonts, font sizes, reduced motion, pointer cursors, font smoothing.

Fonts and font sizes apply to nxds components only. Reduced motion, pointer cursors and font smoothing apply to the whole page. All three are off until a user turns them on.

Change the theme at runtime

Read and update the appearance with useNxdsAppearance:

// components/mode-button.tsx
'use client';

import { useNxdsAppearance } from '@/components/nxds/components/appearance/provider';

export function ModeButton() {
  const { state, setState } = useNxdsAppearance();

  return (
    <button
      type="button"
      onClick={() =>
        setState((current) => ({
          ...current,
          mode: current.mode === 'dark' ? 'light' : 'dark',
        }))
      }
    >
      {state.mode}
    </button>
  );
}

Read and update the appearance with useNxdsAppearance:

<!-- src/components/ModeButton.vue -->
<script setup lang="ts">
import { useNxdsAppearance } from '@/components/appearance';

const { state, setState } = useNxdsAppearance();

function toggle() {
  setState((current) => ({
    ...current,
    mode: current.mode === 'dark' ? 'light' : 'dark',
  }));
}
</script>

<template>
  <button type="button" @click="toggle">{{ state.mode }}</button>
</template>

state and resolvedMode are refs, so the template updates when the theme changes.

Use useNxdsAppearance to build theme controls for your own settings page. To switch brands, set brandColor the same way. nxds builds the new palette and restyles every component at once, without a rebuild.

Brand color

brandColor sets the primary color. nxds builds the full primary palette from it, including hover and pressed shades.

// app/appearance-state.ts
import { DEFAULT_NXDS_APPEARANCE, type NxdsAppearanceState } from '@nxds/core';

export const defaultAppearance = {
  ...DEFAULT_NXDS_APPEARANCE,
  brandColor: '#2563eb',
} satisfies NxdsAppearanceState;

In light mode, the button uses your color, with very light colors darkened a little. In dark mode, dark brand colors get lighter so the button stays visible. A black brand becomes a white button in dark mode.

To let users pick a brand color, copy the appearance/brand-color-field component:

// components/brand-editor.tsx
'use client';

import { DEFAULT_BRAND_COLOR } from '@nxds/core';
import { useState } from 'react';

import { NxdsAppearanceBrandColorField } from '@/components/nxds/components/appearance/brand-color-field/brand-color-field';

export function BrandEditor() {
  const [brandColor, setBrandColor] = useState(DEFAULT_BRAND_COLOR);

  return (
    <NxdsAppearanceBrandColorField
      label="Brand color"
      value={brandColor}
      onChange={setBrandColor}
    />
  );
}

The field offers nine presets (Default, Indigo, Blue, Violet, Rose, Orange, Amber, Green and Teal), a color picker and a hex input. A color that matches no preset shows as Custom.

To build your own picker, @nxds/core exports the presets:

ExportReturns
BRAND_COLOR_PRESETSThe preset list
BASE_TONE_OPTIONSThe surface tone list
findBrandColorPreset(brandColor)The matching preset, or undefined for a custom color

findBrandColorPreset matches any opaque CSS color, such as #4F46E5, 4f46e5 or rgb(79 70 229). A translucent color returns undefined.

Surface tone

surfaceTone sets the gray behind your content. Every tone is close to neutral:

ToneLooks
neutralPure gray
stoneSlightly warm
graySlightly cool
zincCool, slightly violet
slateCoolest

In light mode, pages, cards and popovers stay white for every tone. The tone shows in hover and pressed states, muted surfaces and dark mode.

Contrast

lightContrast and darkContrast set how strong text, borders and hover states are. Each runs from 0 to 100, with 50 as the default.

  • Raise contrast for stronger text and clearer borders.
  • Lower it for a softer look. Text stays readable at 0.
  • Brand and status colors keep their color at every value.

Each mode keeps its own value, so a user can raise contrast in dark mode only. For the contrast targets behind each value, see Color.

Fonts

prefs.uiFont, prefs.headingFont and prefs.codeFont set the font-family stacks for UI text, headings and code.

// app/appearance-state.ts
import { DEFAULT_NXDS_APPEARANCE, type NxdsAppearanceState } from '@nxds/core';

export const defaultAppearance = {
  ...DEFAULT_NXDS_APPEARANCE,
  prefs: {
    ...DEFAULT_NXDS_APPEARANCE.prefs,
    uiFont:
      '"Inter Variable", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif',
    headingFont: '"Lora Variable", Georgia, serif',
  },
} satisfies NxdsAppearanceState;

headingFont is null by default, so headings use the UI font.

Load a font before you name it. Install its Fontsource package, then import it in your nxds CSS entry:

/* app/nxds.css */
@import '@fontsource-variable/inter';
@import '@fontsource-variable/lora';

init --font does both steps, and sets the font in your theme. FONT_OPTIONS in @nxds/core lists the fonts it offers, with each font's stack and package:

CategoryFonts
SansGeist, Inter, Figtree, DM Sans, Manrope, IBM Plex Sans
SerifLora, Playfair Display
MonoGeist Mono, JetBrains Mono

Icons

nxds components draw their own icons, such as a Select's chevron, from lib/icons.ts. That file draws Phosphor. To use another library, pick it when you set up:

npx nxds-cli@latest init --icons lucide
Library--icons
Phosphorphosphor
Lucidelucide
Tabler Iconstabler
HugeIconshugeicons
Remix Iconremix

Remix Icon uses the Remix Icon License, not MIT. It bars logo use and building a competing icon set.

Icons you pass to a component, such as a Button's icon, come from any library you like.

Shuffle

shuffleNxdsAppearance returns a random theme built from the curated options. It picks the brand preset, surface tone, density, corners, elevation, stroke, UI font and heading font.

// components/shuffle-button.tsx
'use client';

import { shuffleNxdsAppearance } from '@nxds/core';

import { useNxdsAppearance } from '@/components/nxds/components/appearance/provider';

export function ShuffleButton() {
  const { setState } = useNxdsAppearance();

  return (
    <button
      type="button"
      onClick={() => setState((current) => shuffleNxdsAppearance(current))}
    >
      Shuffle
    </button>
  );
}

Shuffle keeps mode, contrast and the other preferences. Every brand preset passes the contrast checks on every surface tone.

Shuffle picks from every font in FONT_OPTIONS. If your app loads only some of them, pass those as fonts:

shuffleNxdsAppearance(current, {
  fonts: FONT_OPTIONS.filter((font) => font.value === 'inter'),
});

To try it, select Shuffle in the theme panel on any docs page, or in the Playground. Then select Use this theme to copy the init command that sets it up.

Save the theme

The provider saves the appearance and restores it before the page paints. Install shows the setup for Next.js, and Set up Vite for Vite.

To change the theme inside one element, see Overrides.