Skip to content

Theme & Tokens

BrutxUI uses CSS custom properties (design tokens) to control every visual aspect of the neobrutalist system. This makes customization as simple as overriding a single variable.

If you'd like to visually tweak values first and then copy the complete CSS variables, use the Theme Playground. It displays a product preview, component matrix, contrast checks, and token coverage.

Contrast note: Background/foreground pairs like --brutal-primary and --brutal-primary-foreground are used for selected/active states with small bold text (e.g. 10px calendar selected-day labels); keep the ratio ≥ 4.5:1 (WCAG AA normal text). When customizing a theme, run the contrast check in the Theme Playground to confirm pairs pass. The built-in Dark theme measures ~4.2:1 for this pair — close to but below AA; brighten the foreground color if strict compliance is required.

CSS Variables

All tokens are prefixed with --brutal- and defined at the :root level:

TokenLight ModeDark ModePurpose
--brutal-border-width3px3pxBorder width for all components
--brutal-border-color#000000#ffffffBorder color
--brutal-shadow-offset-x4px4pxShadow horizontal offset
--brutal-shadow-offset-y4px4pxShadow vertical offset
--brutal-shadow-color#000000#ffffffShadow color
--brutal-radius0px0pxBorder radius for rounded elements
--brutal-bg#ffffff#141414Background color
--brutal-fg#000000#ffffffForeground (text) color
--brutal-primary#FF6B6B#FF6B6BPrimary color (coral)
--brutal-primary-foreground#000000#000000Primary foreground text (high contrast black)
--brutal-secondary#4ECDC4#4ECDC4Secondary color (mint teal)
--brutal-secondary-foreground#000000#000000Secondary foreground text (high contrast black)
--brutal-accent#FFE66D#FFE66DAccent color (yellow)
--brutal-accent-foreground#000000#000000Accent foreground text (high contrast black)
--brutal-destructive#EF476F#EF476FDanger/error color
--brutal-destructive-foreground#000000#000000Destructive foreground text (high contrast black)
--brutal-success#7FB069#7FB069Success color
--brutal-success-foreground#000000#000000Success foreground text (high contrast black)
--brutal-muted#f3f4f6#1e1e1eMuted background
--brutal-muted-foreground#4B5563#9CA3AFMuted text color
--brutal-ring#000000#ffffffFocus ring color
--brutal-info#4A90D9#3B82F6Info color
--brutal-info-foreground#000000#000000Info foreground text (high contrast black)
--brutal-status-success#22c55e#22c55eStatus indicator success color
--brutal-status-success-foreground#000000#000000Status success foreground text
--brutal-status-warning#FFE66D#FFE66DStatus indicator warning color
--brutal-status-warning-foreground#000000#000000Status warning foreground text
--brutal-status-info#3b82f6#3b82f6Status indicator info color
--brutal-status-info-foreground#000000#000000Status info foreground text
--brutal-status-error#EF476F#EF476FStatus indicator error color
--brutal-status-error-foreground#000000#000000Status error foreground text
--brutal-overlayrgba(0, 0, 0, 0.5)rgba(0, 0, 0, 0.7)Overlay background
--brutal-overlay-subtlergba(0, 0, 0, 0.05)rgba(255, 255, 255, 0.05)Subtle overlay background
--brutal-placeholder#9CA3AF#6B7280Placeholder text color
--brutal-black#000000#000000Brand standard black
--brutal-yellow#FFE66D#FFE66DBrand standard yellow

Theme Presets

Classic (Default)

The iconic BrutxUI style. Thick borders, hard shadows, zero border radius, vibrant colors.

css
.theme-classic {
    --brutal-border-width: 3px;
    --brutal-border-color: #000000;
    --brutal-shadow-offset-x: 4px;
    --brutal-shadow-offset-y: 4px;
    --brutal-shadow-color: #000000;
    --brutal-radius: 0px;
    --brutal-bg: #ffffff;
    --brutal-fg: #000000;
    --brutal-primary: #FF6B6B;
    --brutal-secondary: #4ECDC4;
    --brutal-accent: #FFE66D;
    --brutal-destructive: #EF476F;
    --brutal-success: #7FB069;
    --brutal-muted: #f3f4f6;
    --brutal-muted-foreground: #4B5563;
    --brutal-ring: #000000;
    --brutal-info: #4A90D9;
    --brutal-overlay: rgba(0, 0, 0, 0.5);
    --brutal-placeholder: #9CA3AF;
}
.dark .theme-classic, .theme-classic.dark {
    --brutal-border-color: #ffffff;
    --brutal-shadow-color: #ffffff;
    --brutal-bg: #141414;
    --brutal-fg: #ffffff;
    --brutal-muted-foreground: #9CA3AF;
    --brutal-ring: #ffffff;
    --brutal-info: #3B82F6;
    --brutal-overlay: rgba(0, 0, 0, 0.7);
    --brutal-placeholder: #6B7280;
}

Pastel

A softer, friendlier style. Thinner borders, smaller shadows, rounded corners, muted tones.

css
.theme-pastel {
    --brutal-border-width: 2px;
    --brutal-border-color: #1e1e24;
    --brutal-shadow-offset-x: 3px;
    --brutal-shadow-offset-y: 3px;
    --brutal-shadow-color: #1e1e24;
    --brutal-radius: 8px;
    --brutal-bg: #faf9f6;
    --brutal-fg: #1e1e24;
    --brutal-primary: #d6c6e1;
    --brutal-secondary: #c5ded9;
    --brutal-accent: #fbe3b5;
    --brutal-destructive: #f3b0b0;
    --brutal-success: #cce2cb;
    --brutal-muted: #eae8e1;
    --brutal-muted-foreground: #6b6b78;
    --brutal-ring: #1e1e24;
    --brutal-info: #a8c8e8;
    --brutal-overlay: rgba(0, 0, 0, 0.4);
    --brutal-placeholder: #b0aeb5;
}
.dark .theme-pastel, .theme-pastel.dark {
    --brutal-border-color: #e5e5e5;
    --brutal-shadow-color: #e5e5e5;
    --brutal-bg: #18171c;
    --brutal-fg: #e5e5e5;
    --brutal-primary: #8a739b;
    --brutal-secondary: #6e8e88;
    --brutal-accent: #b28e56;
    --brutal-destructive: #9b5a5a;
    --brutal-success: #678465;
    --brutal-muted: #27252f;
    --brutal-muted-foreground: #8a8a99;
    --brutal-ring: #e5e5e5;
    --brutal-info: #5a7a9b;
    --brutal-overlay: rgba(0, 0, 0, 0.7);
    --brutal-placeholder: #5a5866;
}

Mono

Maximum contrast. Extra-thick borders, larger shadows, grayscale palette.

css
.theme-mono {
    --brutal-border-width: 4px;
    --brutal-border-color: #000000;
    --brutal-shadow-offset-x: 5px;
    --brutal-shadow-offset-y: 5px;
    --brutal-shadow-color: #000000;
    --brutal-radius: 0px;
    --brutal-bg: #ffffff;
    --brutal-fg: #000000;
    --brutal-primary: #000000;
    --brutal-secondary: #ffffff;
    --brutal-accent: #7a7a7a;
    --brutal-destructive: #333333;
    --brutal-success: #dddddd;
    --brutal-muted: #f0f0f0;
    --brutal-muted-foreground: #555555;
    --brutal-ring: #000000;
    --brutal-info: #666666;
    --brutal-overlay: rgba(0, 0, 0, 0.5);
    --brutal-placeholder: #888888;
}
.dark .theme-mono, .theme-mono.dark {
    --brutal-border-color: #ffffff;
    --brutal-shadow-color: #ffffff;
    --brutal-bg: #000000;
    --brutal-fg: #ffffff;
    --brutal-primary: #ffffff;
    --brutal-secondary: #000000;
    --brutal-accent: #888888;
    --brutal-destructive: #cccccc;
    --brutal-success: #222222;
    --brutal-muted: #1a1a1a;
    --brutal-muted-foreground: #aaaaaa;
    --brutal-ring: #ffffff;
    --brutal-info: #999999;
    --brutal-overlay: rgba(0, 0, 0, 0.7);
    --brutal-placeholder: #777777;
}

Warm

Warm neobrutalism blends raw aesthetics with warm visuals. Orange, brown, beige, and cream tones dominate, with slight border radius softening the hard edges.

css
.theme-warm {
    --brutal-border-width: 3px;
    --brutal-border-color: #5C3D2E;
    --brutal-shadow-offset-x: 4px;
    --brutal-shadow-offset-y: 4px;
    --brutal-shadow-color: #5C3D2E;
    --brutal-radius: 4px;
    --brutal-bg: #FFF8F0;
    --brutal-fg: #2D1810;
    --brutal-primary: #E8722A;
    --brutal-primary-foreground: #FFFFFF;
    --brutal-secondary: #8B6F47;
    --brutal-secondary-foreground: #FFFFFF;
    --brutal-accent: #F2C078;
    --brutal-accent-foreground: #2D1810;
    --brutal-destructive: #C0392B;
    --brutal-destructive-foreground: #FFFFFF;
    --brutal-success: #7B8B3A;
    --brutal-success-foreground: #FFFFFF;
    --brutal-muted: #F5EDE3;
    --brutal-muted-foreground: #6B5B4F;
    --brutal-ring: #E8722A;
    --brutal-info: #D4956A;
    --brutal-info-foreground: #FFFFFF;
    --brutal-overlay: rgba(45, 24, 16, 0.5);
    --brutal-placeholder: #B8A898;
}
.dark .theme-warm, .theme-warm.dark {
    --brutal-border-color: #C4A882;
    --brutal-shadow-color: #C4A882;
    --brutal-bg: #1A1410;
    --brutal-fg: #F5E6D3;
    --brutal-primary: #F59E4C;
    --brutal-primary-foreground: #1A1410;
    --brutal-secondary: #B8956A;
    --brutal-secondary-foreground: #1A1410;
    --brutal-accent: #FFD89B;
    --brutal-accent-foreground: #1A1410;
    --brutal-destructive: #E74C3C;
    --brutal-destructive-foreground: #FFFFFF;
    --brutal-success: #A3B556;
    --brutal-success-foreground: #1A1410;
    --brutal-muted: #2A2018;
    --brutal-muted-foreground: #B8A898;
    --brutal-ring: #F59E4C;
    --brutal-info: #E8B88A;
    --brutal-info-foreground: #1A1410;
    --brutal-overlay: rgba(0, 0, 0, 0.7);
    --brutal-placeholder: #6B5B4F;
}

Custom Tokens

Override tokens at the :root level for global changes:

css
:root {
    --brutal-primary: #8B5CF6;
    --brutal-secondary: #06B6D4;
    --brutal-radius: 4px;
}

Or scope overrides to a specific area:

css
.sidebar {
    --brutal-primary: #8B5CF6;
    --brutal-border-width: 2px;
}

You can also use the Theme Playground to select a base theme and adjust tokens, then copy the generated .theme-custom CSS into your project.

Tailwind Utilities

BrutxUI registers the following Tailwind utility classes that reference CSS variables:

Border

UtilityMaps To
border-3border-width: var(--brutal-border-width)
border-brutalborder-color: var(--brutal-border-color)
rounded-brutalborder-radius: var(--brutal-radius)

Shadow

UtilityMaps To
shadow-brutalFull offset shadow
shadow-brutal-smHalf offset shadow
shadow-brutal-lg1.5x offset shadow
shadow-brutal-xl2x offset shadow
shadow-brutal-primaryPrimary color shadow
shadow-brutal-secondarySecondary color shadow

Color

UtilityMaps To
bg-brutal-bgvar(--brutal-bg)
text-brutal-fgvar(--brutal-fg)
bg-brutal-primaryvar(--brutal-primary)
bg-brutal-secondaryvar(--brutal-secondary)
bg-brutal-accentvar(--brutal-accent)
bg-brutal-destructivevar(--brutal-destructive)
bg-brutal-successvar(--brutal-success)
bg-brutal-infovar(--brutal-info)
bg-brutal-mutedvar(--brutal-muted)
text-brutal-muted-foregroundvar(--brutal-muted-foreground)
text-brutal-placeholdervar(--brutal-placeholder)
bg-brutal-placeholdervar(--brutal-placeholder)
ring-brutal-ringvar(--brutal-ring)

Dark Mode

BrutxUI supports dark mode via the .dark class. When the dark class is applied to the <html> or <body> element, all CSS variables automatically switch to their dark mode values.

html
<html class="dark">
    <!-- Dark mode active -->
</html>

Toggle dark mode programmatically:

vue
<script setup>
import { ref } from 'vue'

const isDark = ref(false)

function toggleDark() {
    isDark.value = !isDark.value
    document.documentElement.classList.toggle('dark', isDark.value)
}
</script>

<template>
    <button @click="toggleDark">Toggle Dark Mode</button>
</template>

Theme presets also support dark mode via the .dark .theme-pastel or .theme-pastel.dark selectors.

useTheme Composable

BrutxUI provides the useTheme composable for switching themes and dark mode at runtime.

ts
import { useTheme } from 'brutx-ui-vue'

const {
    theme,
    colorMode,
    resolvedColorMode,
    isSystemDark,
    setTheme,
    setCustomVariable,
    removeCustomVariable,
    toggleColorMode,
    applyColorMode,
    initTheme,
} = useTheme()

API

Property/MethodTypeDescription
themeRef<'classic' | 'pastel' | 'mono' | 'warm'>Current theme name (reactive)
colorModeRef<'light' | 'dark' | 'system'>Current color mode (reactive)
resolvedColorModeComputedRef<'light' | 'dark'>Actually applied color mode (resolved value when in system mode)
isSystemDarkRef<boolean>Whether the system is currently in dark mode
setTheme(name)(name: ThemeName) => voidSwitch theme, updating DOM class and localStorage simultaneously
setCustomVariable(name, value)(name: \--${string}`, value: string) => void`Set a custom CSS variable via document.documentElement.style.setProperty (added in Batch 8.3)
removeCustomVariable(name)(name: \--${string}`) => void`Remove a custom CSS variable (added in Batch 8.3)
toggleColorMode()() => voidCycle through light -> dark -> system
applyColorMode(mode)(mode: ColorMode) => voidSet a specific color mode
initTheme()() => voidRestore user preferences from localStorage, or follow system preferences
destroy()() => voidClean up matchMedia listeners (called automatically on component unmount)

Usage Examples

Call initTheme at the app entry point to restore the user's last selection:

ts
// main.ts
import { createApp } from 'vue'
import App from './App.vue'
import { useTheme } from 'brutx-ui-vue'

const { initTheme } = useTheme()
initTheme()

createApp(App).mount('#app')

Switch themes and dark mode within a component:

vue
<script setup lang="ts">
import { SelectValue } from 'reka-ui'
import { useTheme, Select, SelectTrigger, SelectContent, SelectItem, Button } from 'brutx-ui-vue'
import type { AcceptableValue } from 'reka-ui'

const { theme, colorMode, setTheme, toggleColorMode } = useTheme()

const themes = [
    { value: 'classic', label: 'Classic' },
    { value: 'pastel', label: 'Pastel' },
    { value: 'mono', label: 'Mono' },
    { value: 'warm', label: 'Warm' },
]

function handleThemeChange(value: AcceptableValue) {
    if (typeof value === 'string') setTheme(value)
}
</script>

<template>
    <Select :model-value="theme" @update:model-value="handleThemeChange">
        <SelectTrigger size="sm" class="w-auto min-w-[8rem]">
            <SelectValue />
        </SelectTrigger>
        <SelectContent>
            <SelectItem v-for="t in themes" :key="t.value" :value="t.value">
                {{ t.label }}
            </SelectItem>
        </SelectContent>
    </Select>
    <Button variant="default" size="sm" @click="toggleColorMode">
        {{ colorMode === 'light' ? 'Light' : colorMode === 'dark' ? 'Dark' : 'System' }}
    </Button>
</template>

Runtime Theme Customization

setCustomVariable and removeCustomVariable are used to dynamically write or clear any CSS variable at runtime (written to document.documentElement inline styles). They are ideal for scenarios like "user-defined primary color", "brand color live preview", or "runtime border width adjustment" without rebuilding CSS.

vue
<script setup lang="ts">
import { ref } from 'vue'
import { useTheme, Button, Input } from 'brutx-ui-vue'

const { setCustomVariable, removeCustomVariable } = useTheme()

const primaryColor = ref('#8B5CF6')
const borderWidth = ref('3px')

function applyPrimary() {
    setCustomVariable('--brutal-primary', primaryColor.value)
}

function applyBorderWidth() {
    setCustomVariable('--brutal-border-width', borderWidth.value)
}

function reset() {
    removeCustomVariable('--brutal-primary')
    removeCustomVariable('--brutal-border-width')
}
</script>

<template>
    <div class="flex flex-col gap-3">
        <div class="flex items-center gap-2">
            <Input v-model="primaryColor" size="sm" class="w-40" placeholder="Primary (e.g. #8B5CF6)" />
            <Button variant="primary" size="sm" @click="applyPrimary">Apply Primary</Button>
        </div>
        <div class="flex items-center gap-2">
            <Input v-model="borderWidth" size="sm" class="w-40" placeholder="Border width (e.g. 5px)" />
            <Button variant="default" size="sm" @click="applyBorderWidth">Apply Border</Button>
        </div>
        <Button variant="default" size="sm" @click="reset">Reset to Default</Button>
    </div>
</template>

Notes:

  • setCustomVariable directly calls document.documentElement.style.setProperty, so it will override variables with the same name defined in preset themes and :root. In SSR environments (typeof document === 'undefined'), the function safely skips execution.
  • removeCustomVariable calls removeProperty, which only removes inline-style variables with the same name and does not affect variables defined via CSS in stylesheets. Therefore, after "resetting to default", the value falls back to the current theme preset.
  • The name parameter must start with -- (enforced at compile time via the template literal type \--${string}``), otherwise a compile-time error will occur.

ColorModeSwitcher Component

BrutxUI provides an out-of-the-box ColorModeSwitcher component with three display modes.

vue
<script setup>
import { ColorModeSwitcher } from 'brutx-ui-vue'
</script>

<template>
    <!-- Icon mode (default) -->
    <ColorModeSwitcher />

    <!-- Button mode -->
    <ColorModeSwitcher display="button" />

    <!-- Dropdown select mode -->
    <ColorModeSwitcher display="select" />

    <!-- Hide the "system" option -->
    <ColorModeSwitcher :show-system="false" />
</template>

Props

PropTypeDefaultDescription
display'icon' | 'button' | 'select''icon'Display mode
showSystembooleantrueWhether to show the "system" option

Brute force builds.