@djtobia/neobrutal-vue
@djtobia/neobrutal-vue
Neo-brutalist component library for Vue 3, themed with the Eugene Elite Dodgeball palette.
Install
npm install @djtobia/neobrutal-vue vue
Requires Vue 3.5 or newer.
Use
import { createApp } from 'vue'
import NeoBrutalVue, { useTheme } from '@djtobia/neobrutal-vue'
import '@djtobia/neobrutal-vue/style.css'
// Fonts are not bundled. The theme font is Roboto:
import '@fontsource/roboto/400.css'
import '@fontsource/roboto/700.css'
import '@fontsource/roboto/900.css'
createApp(App).use(NeoBrutalVue).mount('#app')
useTheme().init() // restores saved theme ('light' | 'dark' | 'candy')
Icons (Material Symbols)
NbIcon (and NbThemeToggle, NbNav item icons) render Material Symbols by name, so the font
must be loaded. The full variable font is about 4 MB, so load only the icons you use from Google
Fonts instead, a few kilobytes:
<link
rel="stylesheet"
href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:opsz,wght,FILL,GRAD@24,700,0..1,0&icon_names=dark_mode,light_mode,menu&display=block"
/>
Add each icon name you use to icon_names. (Or import 'material-symbols/outlined.css' from the
material-symbols package if you prefer self-hosting the whole font.)
Components can also be imported individually: import { NbButton } from '@djtobia/neobrutal-vue'.
Components
NbButton NbCard NbInput NbTextarea NbCheckbox NbBadge NbAlert NbModal NbDrawer
NbHeader NbFooter NbSidebar NbNav NbThemeToggle NbIcon NbImg NbHero NbTable
NbDataTable NbSpinner NbContainer NbRow NbCol NbSpacer
Using with vue-router
NbButton and NbNav render real links. To keep navigation client-side, pass your router's link
component instead of relying on href:
<script setup>
import { RouterLink } from 'vue-router'
const items = [
{ key: '/', label: 'Home', to: '/' },
{ key: '/about', label: 'About', to: '/about' }
]
</script>
<template>
<NbNav :items="items" :link-component="RouterLink" :model-value="$route.path" />
<NbButton :as="RouterLink" to="/contact">Contact us</NbButton>
</template>
Mobile navigation
NbDrawer is a modal side panel (focus trapped, Escape closes, focus returns to the opener). Pair it
with NbNav variant="list" and show the hamburger below a breakpoint using the opt-in utilities:
<NbNav class="d-none d-md-flex" :items="items" />
<NbButton class="d-md-none" aria-label="Open menu" @click="open = true">Menu</NbButton>
<NbDrawer v-model="open" title="Menu">
<NbNav variant="list" :items="items" />
</NbDrawer>
Layout
A 12 column flex grid, in the style of Bootstrap and Vuetify:
<NbContainer>
<NbRow>
<NbCol cols="12" md="8">Main</NbCol>
<NbCol cols="12" md="4">Sidebar</NbCol>
</NbRow>
</NbContainer>
| Component | Props |
|---|---|
NbContainer |
fluid (no max-width), tag |
NbRow |
dense (0.5rem gutters), no-gutters, align (start center end stretch baseline), justify (start center end space-between space-around space-evenly), tag |
NbCol |
cols, sm, md, lg, xl (1-12, auto, or true for equal width); offset, offset-sm...; order, order-sm... (0-12, first, last); align-self; tag |
NbSpacer |
grows to push siblings apart inside a flex container |
Breakpoints match Vuetify: sm 600px, md 960px, lg 1280px, xl 1920px (min-width). The gutter is
--nb-gutter (default 1.5rem). The same classes work without Vue: nb-row, nb-col-md-6,
nb-offset-2, nb-order-last, ...
Utility classes (opt in)
import '@djtobia/neobrutal-vue/utilities.css'
Vuetify-style names, all !important so they win over component styles:
| Group | Classes |
|---|---|
| Spacing (0-12, 1 unit = 0.25rem) | ma-4 px-2 mt-auto pa-0 ... (m/p + a t r b l x y s e), gaps ga-4 gr-2 gc-2 |
Display (+ sm md lg xl) |
d-none d-flex d-md-none d-md-block d-grid ... |
| Flex | flex-column flex-md-row flex-wrap flex-grow-1 justify-center justify-space-between align-center align-self-end |
| Typography | text-h1..text-h6 text-body-1 text-body-2 text-caption text-overline text-center text-uppercase text-truncate font-weight-bold |
| Sizing and position | w-100 h-50 fill-height min-h-screen position-relative overflow-hidden |
| Surfaces | bg-primary bg-accent bg-surface ... (text color is set for contrast), border border-t rounded-lg rounded-pill elevation-0..elevation-4 |
These are kept out of the main stylesheet so they can never clash with another utility framework.
The CSS is generated: edit scripts/generate-css.mjs and run npm run generate:css.
Theming
Everything reads CSS custom properties (--nb-primary, --nb-line, --nb-radius, ...)
defined in src/styles/tokens.css. Set data-theme on <html> or any wrapper to switch themes.
Custom themes
import { registerTheme, useTheme } from '@djtobia/neobrutal-vue'
registerTheme('sunset', {
extends: 'dark', // optional: 'light' (default) | 'dark' | 'candy'
primary: '#ff7a1a',
accent: '#7b2cbf',
radius: 8, // numbers are px
shadowOffset: 6
})
useTheme().setTheme('sunset')
- Only the tokens you set are overridden; the rest come from
extends. - Text colors on
primary(ink),accent(onAccent) anddanger(onDanger) are picked automatically for contrast when you don't set them and the color is a hex value. - Tokens:
bg fg surface line ink primary secondary accent success danger onAccent onDanger heading borderWidth radius shadowOffset font. registerTheme(name, def, { persist: true })saves it to localStorage;useTheme().init()restores it. CallunregisterTheme(name)to remove one.themeToCss(name, def)returns the generated CSS if you'd rather ship a stylesheet.- The Theming / Theme Builder story lets you pick colors live and copy the result.
checkThemeContrast(def)returns the token pairs that miss WCAG 2.2 AA (4.5:1 text, 3:1 for large headings and borders). The Theme Builder shows these live.
Accessibility
The library targets WCAG 2.2 Level AA. All three built-in themes pass automated axe-core checks
(wcag2a, wcag2aa, wcag21a, wcag21aa, wcag22aa) across every story.
What the components do for you:
- Contrast: text on every fill meets 4.5:1 in all built-in themes; custom
NbCard/NbBadgehex colors and custom-theme text colors get readable text automatically. - Keyboard: everything is operable with a keyboard, with a visible 3px focus outline. Sortable
headers are buttons;
NbDataTablerows become focusable and respond to Enter/Space when you listen forrow-click. - Modal:
role="dialog",aria-modal, named by its title (orlabel), moves focus in, traps Tab, closes on Escape, restores focus and locks page scroll. - Forms:
NbInputlinks label, hint and error (aria-describedby,aria-invalid), marks required fields, and passesautocompleteand other attributes to the<input>. - State is never color only: the current nav/sidebar item is underlined and has
aria-current="page"; errors have an icon and text. - Status messages:
NbAlertusesrole="alert"for danger/warning androle="status"otherwise;NbSpinneris a status;NbDataTableannounces its result range. - Motion: hover/press movement is disabled and spinners stop under
prefers-reduced-motion. - Reflow and size: layouts use
remand wrap at narrow widths; interactive targets are at least 24px.
What is up to you:
- Give content images a meaningful
alt(NbImgdefaults to"", i.e. decorative). - Keep one
h1per page (NbHerohasheading-level), and give eachNbNav/NbSidebara uniquelabel. - Text you place over an
NbImgorNbHerophoto:NbHerodarkens the image, but check your own overlays. - A sticky
NbHeadercan cover focused elements; addscroll-padding-topmatching its height. - Name tables with
caption(NbDataTable) or a<caption>inNbTable.
Develop
npm install
npm run storybook # component explorer on :6006
npm test # unit tests (vitest + jsdom)
npm run build # type-check + library build into dist/
Stories live in stories/ and tests in tests/; neither is part of the published package.
Related Packages
Agala UI — A zero-dependency Vue 3 component library with atomic design tokens,...
Agala Labs UI — accessible Vue 3 components, semantic design tokens, and theming...
The design system behind Hikarinagi: Vue 3 components built on Tailwind CSS v4.
Version History
| Version | Released | Vue | License |
|---|---|---|---|
| 0.2.1 | ^3.5.0 | MIT | |
| 0.2.0 | ^3.5.0 | MIT | |
| 0.1.0 | ^3.5.0 | MIT |
Pre-releases (1)
| Version | Released | License |
|---|---|---|
| 0.0.0-stage |