Appearance
Components
Few basic rules for (reusable) components:
We are following Headless Components Pattern
- this article is for React, but the same rules apply to Vue
- we typically use PrimeVue with unstyled mode as our headless library
- reason for this is complexity and accessibility, most of the time we don't want to write our own components and deal with these issues
We isolate external component libraries by wrapping them
- we don't use PrimeVue library directly, instead we use it as implementation detail and provide our own components
- this should apply to all external components, not just PrimeVue!
Where to put them
src/core/components/for global componentssrc/feature/XXX/components/for local or feature-specific components
WARNING
Components that are feature specific, but used by other features (eg.
EmployeeAutoComplete) should live in a feature module and be exported via barrel export fromsrc/feature/XXX/index.tsnot incore:ts// index.ts export { EmployeeAutoComplete } from "./components";
TIP
TODO:
- Accessibility
- css variables
Styling variants
Use tailwind-variants for reusable component styling. It gives us typed variants, default variants, compound variants, composition and slots, while still producing plain Tailwind classes.
The goal is to describe the visual API of a component in one place:
ts
import { tv, type VariantProps } from "tailwind-variants";
export const buttonVariants = tv({
slots: {
root: [
"inline-flex items-center justify-center gap-2 rounded-md font-medium",
"transition-colors disabled:pointer-events-none disabled:opacity-50",
],
icon: "size-4 shrink-0",
},
variants: {
severity: {
primary: {
root: "bg-primary text-primary-contrast enabled:hover:bg-primary-emphasis",
},
secondary: {
root: "bg-surface-100 text-surface-900 enabled:hover:bg-surface-200",
},
},
size: {
sm: {
root: "h-8 px-3 text-sm",
icon: "size-3.5",
},
md: {
root: "h-10 px-4 text-sm",
},
},
outlined: {
true: {
root: "border bg-transparent",
},
},
},
compoundVariants: [
{
severity: "primary",
outlined: true,
class: {
root: "border-primary text-primary enabled:hover:bg-primary/10",
},
},
],
defaultVariants: {
severity: "primary",
size: "md",
},
});
export type ButtonVariants = VariantProps<typeof buttonVariants>;
// Generates slot class helpers for given variants.
const classes = buttonVariants({ severity: "secondary", size: "sm" });
const rootClass = classes.root();
const iconClass = classes.icon();Prefer tailwind-variants for classes derived from our component props: severity, variant, size, outlined, icon placement and similar visual choices. Use slots when a component has multiple styled parts, like root, icon, label, trigger, panel or item.
Visual documentation
When creating reusable components you should document them. We have a system in place just for this. In order to document a component, you need to create a .story.vue file next to the component. For example - to document Button.vue - you place Button.story.vue next to it, with contents similar to this:
vue
<script lang="ts">
export const story = {
id: "button",
title: "Button",
group: "Core / Components",
description: "A button with different styles and variants.",
};
</script>
<template>This is where the documentation goes to.</template>Story is supposed to document props, different options and variants of how given component can be used.
Stories are then exposed under https://portal.fieldflo.dev/SLUG/demo/showcase.
*.story.vue files can be placed anywhere in the project - as long as it's in src directory.
INFO
We are NOT using Storybook or anything similar - those solutions are typically complex and heavy. We simply use sub-pages and AI to generate everything quickly.
Icons
We are using Lucide icon set. Working with them from Vue is pretty simple: just import them as components:
vue
<script setup lang="ts">
import { Info } from "lucide-vue-next";
</script>
<template>
<Info />
<Button icon="Info" />
</template>AI Agents
AI Agents like Claude Code often tend to create inline <svg> instead of using existing icons. Make sure it doesn't happen. Prefer existing lucide icons. If no icon is available, place them in /frontend/src/core/icons/ folder.
PrimeVue Icons
By default - Prime Vue uses font-based css icons (<span lass="pi pi-check" />) - this is a pretty outdated approach! Do not use them, instead prefer using Lucide. You may add support for this in our core components if they are based on PrimeVue.