@neobrut-vue/core

Colorful and accessible neo-brutalist components for Vue 3.
1,216
Install
npm install @neobrut-vue/core
Latest Version:0.6.0
Vue:3
License:MIT
Last Updated:Oct 2, 2026
Links: GitHub  ·  npm
Maintainer: avrdude

@neobrut-vue/core

npm CI license

Colorful and accessible neo-brutalist components for Vue 3.

Landing · Interactive docs · npm · GitHub · Issues

Landing and docs

The static landing is built from the interactive Vue playground:

bun install --frozen-lockfile
bun run build:site

Its output is playground/dist, including the static Astro documentation under /docs. Cloudflare Pages builds it from this repository with BUN_VERSION=1.3.4 and serves both at neobrut.avrdu.de.

Requirements

  • Vue 3.5 or newer
  • A client-side Vue application

Install

npm install @neobrut-vue/core

Import the stylesheet once in your application entry:

import { createApp } from 'vue'
import '@neobrut-vue/core/style.css'
import App from './App.vue'

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

Components are available as named imports:

<script setup lang="ts">
import { NbBadge, NbButton, NbCard } from '@neobrut-vue/core'
</script>

<template>
  <NbCard tone="primary" interactive>
    <template #header><NbBadge tone="accent">New</NbBadge></template>
    <h2>Make it loud.</h2>
    <template #footer><NbButton variant="accent">Launch</NbButton></template>
  </NbCard>
</template>

Components

Group Components
Actions and surfaces NbButton, NbButtonGroup, NbCopyButton, NbToggle, NbCard, NbAlert, NbBadge
Forms NbCheckbox, NbCombobox, NbField, NbFieldset, NbInput, NbInputGroup, NbLabel, NbNativeSelect, NbNumberInput, NbPinInput, NbRadioGroup, NbRating, NbSelect, NbSelectItem, NbSlider, NbSwitch, NbTagsInput, NbTextarea, NbToggleGroup, NbToggleGroupItem
Navigation NbBreadcrumbs, NbMenubar, NbMenubarMenu, NbMenubarItem, NbMenubarSeparator, NbNavigationMenu, NbPagination, NbStepper, NbStepperItem, NbTabs, NbTabsList, NbTabsTrigger, NbTabsContent
Overlays NbAlertDialog, NbCommand, NbContextMenu, NbDialog, NbDrawer, NbDropdownMenu, NbHoverCard, NbPopover, NbSheet, NbToast, NbTooltip
Data display NbAccordion, NbAccordionItem, NbAvatar, NbCarousel, NbCarouselSlide, NbCollapsible, NbEmptyState, NbMeter, NbProgress, NbSkeleton, NbSpinner, NbTable, NbTimeline, NbTimelineItem
Inline and layout NbLink, NbKbd, NbMarker, NbAspectRatio, NbScrollArea, NbSeparator

Links and actions

NbLink forwards native anchor attributes and can render a router component through as. Related actions stay semantic and keyboard accessible:

<script setup lang="ts">
import { ref } from 'vue'
import { NbButton, NbButtonGroup, NbKbd, NbLink, NbMarker, NbToggle } from '@neobrut-vue/core'

const pinned = ref(false)
</script>

<template>
  <p>
    <NbLink href="/docs" tone="accent">Read the docs</NbLink>
    and build something <NbMarker>loud</NbMarker>.
  </p>

  <NbButtonGroup label="Document actions">
    <NbButton>Save</NbButton>
    <NbButton variant="secondary">Share</NbButton>
  </NbButtonGroup>

  <NbToggle v-model="pinned" label="Pin project">Pin</NbToggle>
  <span>Search with <NbKbd>⌘ K</NbKbd></span>
</template>

NbCopyButton copies its text with the browser Clipboard API. It announces short success or failure feedback and emits copied or error; clipboard access requires a secure context such as HTTPS or localhost.

<script setup lang="ts">
import { NbCopyButton } from '@neobrut-vue/core'
</script>

<template>
  <NbCopyButton text="npm install @neobrut-vue/core" label="Copy install command" />
</template>

Forms

Form components use v-model, forward native input attributes, and expose labels, hints, and errors through the relevant ARIA relationships.

<script setup lang="ts">
import { ref } from 'vue'
import {
  NbCheckbox,
  NbInput,
  NbSelect,
  NbSelectItem,
  NbSwitch,
  NbTextarea,
} from '@neobrut-vue/core'

const name = ref('')
const tone = ref('primary')
const note = ref('')
const alerts = ref(true)
const accepted = ref(false)
</script>

<template>
  <form>
    <NbInput v-model="name" label="Name" required placeholder="Ada Lovelace" />

    <NbSelect v-model="tone" label="Favorite tone">
      <NbSelectItem value="primary">Electric blue</NbSelectItem>
      <NbSelectItem value="accent">Bubblegum pink</NbSelectItem>
    </NbSelect>

    <NbTextarea v-model="note" label="Note" hint="Keep it short and loud." />
    <NbSwitch v-model="alerts" label="Launch alerts" description="Notify me when it ships." />
    <NbCheckbox v-model="accepted" label="I accept the chaos" />
  </form>
</template>

Searchable, numeric, range, and segmented controls share the same field contract:

<script setup lang="ts">
import { ref } from 'vue'
import {
  NbCombobox,
  NbNumberInput,
  NbSlider,
  NbToggleGroup,
  NbToggleGroupItem,
  type NbComboboxOption,
} from '@neobrut-vue/core'

const frameworks: NbComboboxOption[] = [
  { value: 'vue', label: 'Vue' },
  { value: 'svelte', label: 'Svelte' },
]
const framework = ref('vue')
const seats = ref(3)
const volume = ref(65)
const alignment = ref('center')
</script>

<template>
  <NbCombobox v-model="framework" label="Framework" :options="frameworks" />
  <NbNumberInput v-model="seats" label="Seats" :min="1" :max="12" />
  <NbSlider v-model="volume" label="Volume" :step="5" show-value />
  <NbToggleGroup v-model="alignment" label="Alignment">
    <NbToggleGroupItem value="left">Left</NbToggleGroupItem>
    <NbToggleGroupItem value="center">Center</NbToggleGroupItem>
    <NbToggleGroupItem value="right">Right</NbToggleGroupItem>
  </NbToggleGroup>
</template>

NbInputGroup attaches prefixes, suffixes, or small actions to a native input. Give the input its normal label or accessible name:

<script setup lang="ts">
import { ref } from 'vue'
import { NbInputGroup } from '@neobrut-vue/core'

const handle = ref('')
</script>

<template>
  <label for="handle">Profile handle</label>
  <NbInputGroup>
    <template #start>@</template>
    <input id="handle" v-model="handle" autocomplete="username">
    <template #end>.dev</template>
  </NbInputGroup>
</template>

Tags, verification codes, and ratings use the same v-model pattern. A PIN value is an array of digits so leading zeroes survive; complete fires when every cell is filled. Set name when submitting tags or codes in a native form so the submitted field has a predictable key.

<script setup lang="ts">
import { ref } from 'vue'
import { NbPinInput, NbRating, NbTagsInput } from '@neobrut-vue/core'

const tags = ref(['Vue'])
const code = ref<number[]>([])
const rating = ref(3)
</script>

<template>
  <NbTagsInput v-model="tags" label="Skills" placeholder="Add a skill" name="skills" />
  <NbPinInput v-model="code" label="Verification code" :length="6" otp name="code" />
  <NbRating v-model="rating" label="Experience" name="rating" />
</template>

Navigation and disclosure

Tabs support controlled and uncontrolled state plus horizontal or vertical keyboard navigation:

<script setup lang="ts">
import { ref } from 'vue'
import { NbTabs, NbTabsContent, NbTabsList, NbTabsTrigger } from '@neobrut-vue/core'

const activeTab = ref('account')
</script>

<template>
  <NbTabs v-model="activeTab">
    <NbTabsList>
      <NbTabsTrigger value="account">Account</NbTabsTrigger>
      <NbTabsTrigger value="billing">Billing</NbTabsTrigger>
    </NbTabsList>
    <NbTabsContent value="account">Account settings</NbTabsContent>
    <NbTabsContent value="billing">Billing settings</NbTabsContent>
  </NbTabs>
</template>

Use default-value instead of v-model for uncontrolled tabs. Accordions follow the same pattern:

<script setup lang="ts">
import { NbAccordion, NbAccordionItem } from '@neobrut-vue/core'
</script>

<template>
  <NbAccordion default-value="shipping">
    <NbAccordionItem value="shipping" title="How fast is shipping?">
      Loudly fast.
    </NbAccordionItem>
    <NbAccordionItem value="returns" title="Can I return it?">
      Absolutely.
    </NbAccordionItem>
  </NbAccordion>
</template>

For one revealable section, use NbCollapsible with default-open or v-model:open:

<script setup lang="ts">
import { NbCollapsible } from '@neobrut-vue/core'
</script>

<template>
  <NbCollapsible title="Advanced options">Extra controls go here.</NbCollapsible>
</template>

Dialogs and menus

NbDialog traps focus, closes on Escape, and restores focus to its trigger:

<script setup lang="ts">
import { ref } from 'vue'
import { NbButton, NbDialog } from '@neobrut-vue/core'

const open = ref(false)
</script>

<template>
  <NbButton @click="open = true">Open dialog</NbButton>
  <NbDialog v-model:open="open" title="Confirm launch">
    This action is delightfully irreversible.
    <template #footer>
      <NbButton variant="ghost" @click="open = false">Cancel</NbButton>
      <NbButton variant="accent" @click="open = false">Launch</NbButton>
    </template>
  </NbDialog>
</template>

Dropdown menus provide styled keyboard navigation through Reka UI:

<script setup lang="ts">
import {
  NbDropdownMenu,
  NbDropdownMenuItem,
  NbDropdownMenuSeparator,
} from '@neobrut-vue/core'
</script>

<template>
  <NbDropdownMenu label="Project actions">
    <template #trigger>Actions</template>
    <NbDropdownMenuItem>Duplicate</NbDropdownMenuItem>
    <NbDropdownMenuSeparator />
    <NbDropdownMenuItem destructive>Delete</NbDropdownMenuItem>
  </NbDropdownMenu>
</template>

Sheets, alert dialogs, and command menus use the same controlled or uncontrolled open-state pattern:

<script setup lang="ts">
import { ref } from 'vue'
import { NbAlertDialog, NbCommand, NbSheet, type NbCommandOption } from '@neobrut-vue/core'

const selected = ref('')
const commands: NbCommandOption[] = [
  { value: 'new', label: 'New component', group: 'Project', shortcut: '⌘N' },
  { value: 'publish', label: 'Publish package', group: 'Project', keywords: ['release'] },
]
</script>

<template>
  <NbSheet title="Edit component" side="right">
    <template #trigger>Edit</template>
    Sheet content
  </NbSheet>

  <NbAlertDialog
    title="Delete component?"
    description="This action cannot be undone."
    action-label="Delete"
    destructive
  >
    <template #trigger>Delete</template>
  </NbAlertDialog>

  <NbCommand v-model="selected" :options="commands" label="Project commands" />
</template>

NbContextMenu includes item, checkbox, radio, submenu, label, separator, and shortcut primitives. NbHoverCard preserves the semantics of its slotted trigger, while NbNavigationMenu accepts a small data-driven link tree.

Data display

<script setup lang="ts">
import { ref } from 'vue'
import { NbAvatar, NbPagination, NbProgress, NbTable } from '@neobrut-vue/core'

const page = ref(1)
</script>

<template>
  <NbAvatar name="Ada Lovelace" size="lg" />
  <NbProgress :value="72" label="Package build" show-value />
  <NbPagination v-model:page="page" :total="120" :items-per-page="10" />

  <NbTable caption="Release status">
    <thead><tr><th>Package</th><th>Status</th></tr></thead>
    <tbody><tr><td>Core</td><td>Ready</td></tr></tbody>
  </NbTable>
</template>

Use NbProgress for completion, and the native NbMeter for a bounded measurement. NbStepper handles keyboard navigation and controlled or uncontrolled steps; render the active panel in your app. NbTimeline keeps event history in a semantic ordered list.

<script setup lang="ts">
import { ref } from 'vue'
import { NbMeter, NbStepper, NbStepperItem, NbTimeline, NbTimelineItem } from '@neobrut-vue/core'

const step = ref(1)
</script>

<template>
  <NbStepper v-model="step" label="Checkout">
    <NbStepperItem :step="1" title="Cart" description="Review items" />
    <NbStepperItem :step="2" title="Address" />
    <NbStepperItem :step="3" title="Payment" />
  </NbStepper>
  <section v-if="step === 1">Review your cart here.</section>

  <NbMeter label="Storage used" :value="72" show-value />
  <NbTimeline label="Release history">
    <NbTimelineItem title="Published" date="Oct 1" datetime="2026-10-01">Available on npm.</NbTimelineItem>
  </NbTimeline>
</template>

VeeValidate

VeeValidate is optional. Standard model and attribute bindings work without an adapter:

<script setup lang="ts">
import { useForm } from 'vee-validate'
import { NbInput } from '@neobrut-vue/core'

const { defineField, errors } = useForm({
  validationSchema: {
    email: (value: string) => value ? true : 'Email is required',
  },
})
const [email, emailAttrs] = defineField('email')
</script>

<template>
  <NbInput
    v-model="email"
    v-bind="emailAttrs"
    name="email"
    label="Email"
    :error="errors.email"
  />
</template>

Global registration

Named imports are preferred. For templates that need every component globally, install the optional plugin:

import { createApp } from 'vue'
import { NeoBrutalVue } from '@neobrut-vue/core'
import '@neobrut-vue/core/style.css'
import App from './App.vue'

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

Theming

Override CSS custom properties on :root or any wrapper. Components inherit the nearest values.

:root {
  --nb-color-paper: #fff8e7;
  --nb-color-ink: #171717;
  --nb-color-primary: #ffdf3f;
  --nb-color-secondary: #72ddc3;
  --nb-color-accent: #ff7b71;
  --nb-color-danger: #ff4d5a;
  --nb-border-width: 3px;
  --nb-shadow-x: 7px;
  --nb-shadow-y: 7px;
}

Accessibility

The components prefer native elements and established ARIA patterns. Labels, hints, errors, invalid states, dialog focus management, menu navigation, tabs, accordions, progress, pagination, toasts, and tooltips are covered by automated axe tests and browser checks.

Consumers are still responsible for meaningful labels, sensible content, heading order, and sufficient contrast after overriding theme tokens.

Development

bun install
bun run dev:playground
bun run check

bun run check runs the tests, TypeScript validation, and production build. npm pack --dry-run runs the same checks and prints the exact files that would be published.

License

MIT

Related Packages

@modekit/vue

Accessible, feature-rich UI components for Vue.

248 1
@cocoar/vue-ui

Cocoar Design System — a touch-first Vue 3 component library with 30+ accessible...

17,724 0
@rukkiecodes/vue

FusionUI — a soft, modern Vue 3 design library. Accessible, token-driven and SSR...

4,677
seeroad-ui

seeroad project ui library

3,733
ui

Version History

Version Released License
0.6.0 MIT
0.5.0 MIT
0.4.0 MIT
0.3.0 MIT
0.2.0 MIT