Appearance
Getting started
@fivespark/ui works in any Vue 3.5 app that uses Tailwind CSS 4. A Nuxt 4 app adds the package's Nuxt module; every other Vue app installs a plugin. The stylesheet, Vite and ESLint files shown below are the ones the package is tested with, in a Vite app and a Nuxt app, before each release.
Install
sh
npm install @fivespark/ui tailwindcss @tailwindcss/viteThe stylesheet
Import the package's stylesheet after Tailwind, in the app's one Tailwind stylesheet:
css
@import "tailwindcss";
@import "@fivespark/ui/styles.css";This makes the Fivespark theme the app's theme. Tailwind's default colours, fonts, text sizes, radii and shadows are removed, so bg-red-500 and text-sm no longer exist; use the theme's, which the foundations list. The fonts come with the stylesheet. The theme follows the system's light or dark mode; data-theme="light" or data-theme="dark" on an element fixes one for it and everything inside it. Tailwind finds the classes the components use by itself, so nothing needs adding to its sources.
In a Vue app
Add Tailwind's plugin beside Vue's in vite.config.ts:
ts
import tailwindcss from '@tailwindcss/vite'
import vue from '@vitejs/plugin-vue'
import { defineConfig } from 'vite'
export default defineConfig({ plugins: [vue(), tailwindcss()] })Install the package's plugin once, where the app is created, and import each component where it is used:
ts
// main.ts
import './style.css'
import { createFivespark } from '@fivespark/ui'
import { createApp } from 'vue'
import { RouterLink } from 'vue-router'
import App from './App.vue'
import router from './router' // the app's own Vue Router
createApp(App).use(router).use(createFivespark({ link: RouterLink, locale: 'en' })).mount('#app')vue
<script setup lang="ts">
import { FsButton } from '@fivespark/ui'
</script>
<template>
<FsButton variant="primary" label="Save" />
</template>The plugin and both its options are optional:
linkis the app's link component, whichtoon a link or button goes through. Without one, links are a plain<a href>, which is what a site without a router wants.localeis the language the components speak and format numbers and dates in: a tag such as'nl'or'en-GB', or a ref or getter returning one, so that a change of language reaches every component. Without one, the components read<html lang>, then speak Dutch.
An app rendered on the server in a language other than Dutch must set locale: the server has no <html lang> to read, so it would render Dutch and the browser would switch to the other language.
In a Nuxt app
Add the package's module, the stylesheet and Tailwind's plugin to nuxt.config.ts, with the stylesheet in app/assets/main.css:
ts
// nuxt.config.ts
import tailwindcss from '@tailwindcss/vite'
export default defineNuxtConfig({
modules: ['@fivespark/ui/nuxt'],
css: ['~/assets/main.css'],
vite: { plugins: [tailwindcss()] },
})The module does what the plugin does in a Vue app, and more:
- Every component and composable is imported for you:
<FsButton>anduseToast()work in any component without an import. Importing them explicitly still works. - Links go through
NuxtLink. - The language is
@nuxtjs/i18n's when that module is installed, and follows it when it changes. Give each of its locales alanguage({ code: 'nl', language: 'nl-NL' }), or a language tag as itscode. Without i18n, set one withfivespark: { locale: 'en' }innuxt.config.ts; without that, the components read<html lang>, then speak Dutch.
The module needs Nuxt 4. In a Nuxt 3 app, import the components explicitly and install the plugin in a plugin file of your own:
ts
// plugins/fivespark.ts
import { NuxtLink } from '#components'
import { createFivespark } from '@fivespark/ui'
export default defineNuxtPlugin(nuxtApp => {
nuxtApp.vueApp.use(createFivespark({ link: NuxtLink, locale: 'nl' }))
})An app on pnpm without hoisting, or on Yarn Plug'n'Play, adds @nuxt/kit to its own dependencies.
Preloading the display font
The fonts load when the page's text first needs them, and until they arrive the text shows in a system font. To have the headings in Bricolage Grotesque sooner, preload the display font's latin file. Import its address from the package with Vite's ?url, so that the preload asks for the file the stylesheet loads and it is fetched once. Keep crossorigin: fonts are fetched in CORS mode, and a preload without it is fetched a second time.
In a Nuxt app, in app.vue. The link is rendered on the server, so the font loads while the page's script does:
ts
import bricolage from '@fivespark/ui/fonts/bricolage-grotesque-latin.woff2?url'
// The display font starts loading with the page, before the first paint asks for it.
useHead({ link: [{ rel: 'preload', as: 'font', type: 'font/woff2', href: bricolage, crossorigin: '' }] })In a Vue app rendered in the browser, in main.ts. The page has no text until the app mounts, so the gain is small:
ts
import bricolage from '@fivespark/ui/fonts/bricolage-grotesque-latin.woff2?url'
// The display font starts loading before the first render asks for it.
document.head.append(Object.assign(document.createElement('link'), { rel: 'preload', as: 'font', type: 'font/woff2', href: bricolage, crossOrigin: '' }))Preload only this file: the latin file covers Dutch, English, French and German, and a preload the page does not use costs a download for nothing.
The components' words
Some components carry words of their own, such as a button's "Loading" or a field's "required". They come in Dutch and English; any other language gets English. To change a component's words, use the prop it offers for them, which its page lists.
Lint for drift
The package's ESLint config reports where an app's code drifts from the system: a colour, size or radius outside the theme, a native element where a component exists, a component restyled by a class, or an import the package does not offer. Install ESLint and the two plugins it uses:
sh
npm install --save-dev eslint eslint-plugin-vue eslint-plugin-better-tailwindcssThen give the config the path of the app's Tailwind stylesheet. In a Vue app, eslint.config.js, with typescript-eslint installed too for the TypeScript in .vue files:
js
import fivespark from '@fivespark/ui/eslint'
import tseslint from 'typescript-eslint'
export default [
...fivespark('src/style.css'),
{ files: ['**/*.vue'], languageOptions: { parserOptions: { parser: tseslint.parser } } },
]In a Nuxt app with @nuxt/eslint in its modules, which sets up the TypeScript parser, eslint.config.mjs:
js
// @nuxt/eslint's config, written by `nuxt build`, with the package's after it.
import fivespark from '@fivespark/ui/eslint'
import withNuxt from './.nuxt/eslint.config.mjs'
export default withNuxt(fivespark('app/assets/main.css'))A class on a component may place it, with its margin, its place in a grid or flex layout, its size, position and display, but not change how it looks. If the app sets no-restricted-imports too, whichever comes last in its config replaces the other.
With a coding agent
The package carries docs written for coding agents, in dist/agents/: an index of the components and the rules an app follows, and a file per component with its props, an example and what it does not accept. They describe the version installed.
Point the agent at them. In the app's CLAUDE.md, import the index:
md
@node_modules/@fivespark/ui/dist/agents/AGENTS.mdFor agents that read AGENTS.md instead, add one line to the app's AGENTS.md. Point at the file, never copy it, since a copy goes out of date at the next upgrade:
md
Before using @fivespark/ui, read node_modules/@fivespark/ui/dist/agents/AGENTS.md.Lint each edit. With the lint above set up, this hook in the app's .claude/settings.json runs ESLint on each file Claude Code edits, and hands the errors back to Claude to fix:
json
{
"hooks": {
"PostToolUse": [
{
"matcher": "Edit|Write|MultiEdit",
"hooks": [
{
"type": "command",
"command": "cd \"$CLAUDE_PROJECT_DIR\" && f=$(jq -r '.tool_input.file_path // empty'); case \"$f\" in *.vue|*.ts|*.js|*.mjs) npx eslint --no-warn-ignored \"$f\" 1>&2 || exit 2;; esac"
}
]
}
]
}
}It needs jq, which macOS 15 and later include.