Styling Overview
CPR uses a Tailwind-first approach to styling. Nearly all visual presentation is handled through Tailwind CSS utility classes applied directly in Vue templates, with a small set of reusable component classes defined in a global CSS entry point.
Core Principles
- Utility-first -- Prefer inline Tailwind classes over custom CSS. Most components contain zero
<style>blocks. - Component-based abstraction -- When a pattern repeats across many templates (buttons, inputs, cards), extract it into either a Vue component with computed classes or a
@layer componentsclass inmain.css. - Minimal custom CSS -- The only custom CSS lives in
app/assets/css/main.css. There is no separate SCSS/LESS pipeline. - Brand color system -- The primary brand color (
#117ea7) is applied consistently through a mix of Tailwind'sprimary-*palette and arbitrary values likebg-[#117ea7].
Technology Stack
| Layer | Tool |
|---|---|
| CSS framework | Tailwind CSS via @nuxtjs/tailwindcss |
| Configuration | Default Tailwind config (no tailwind.config.* file) |
| CSS entry point | app/assets/css/main.css |
| Component styling | Computed class strings in Vue <script setup> |
| Icons | @heroicons/vue (24px outline and solid variants) |
How Styles Are Organized
Global styles -- main.css
The global stylesheet sets up three Tailwind layers and a small amount of non-layer CSS:
@tailwind base; /* Tailwind's reset + base layer overrides */
@tailwind components; /* Reusable component classes (.btn-primary, .card, etc.) */
@tailwind utilities; /* All utility classes */@layer base-- Sets defaultbodybackground and text color.@layer components-- Defines shared component classes that are used across multiple templates (.btn-primary,.btn-secondary,.input-field,.card).- Non-layer CSS -- Custom scrollbar styling (
.custom-scrollbar).
Component-level styles -- Computed classes
UI components like UiButton, UiInput, and UiModal build their class strings dynamically via computed properties or inline expressions. This keeps variant logic (size, color, disabled state) co-located with the component:
<script setup lang="ts">
const buttonClasses = computed(() => [
// base
'font-medium rounded-lg transition-colors focus:outline-none focus:ring-2',
// variant
props.variant === 'primary'
? 'bg-[#117ea7] text-white hover:bg-[#0e6b8f]'
: 'bg-gray-200 text-gray-800 hover:bg-gray-300',
// size
props.size === 'sm' ? 'px-3 py-1.5 text-sm' : 'px-4 py-2',
])
</script>No scoped styles (in most cases)
The vast majority of CPR components have no <style> block. Tailwind utilities and computed classes cover all needs. If you find yourself reaching for a <style scoped> block, consider whether a Tailwind utility or a main.css component class would be a better fit.
File Map
app/
assets/
css/
main.css # Global styles, Tailwind layers, component classes
components/
ui/
UiButton.vue # Variant/size via computed classes
UiInput.vue # Error/disabled states via conditional classes
UiModal.vue # Branded header, transition animationsQuick Reference
| Need | Approach |
|---|---|
| One-off spacing, color, layout | Inline Tailwind utilities in the template |
| Repeated button/input/card pattern | Use the Ui* component or a @layer components class |
| Brand color on a new element | bg-[#117ea7] or text-[#117ea7] (arbitrary value) |
| Responsive breakpoint | sm: for small, lg: for desktop layout shifts |
| Hover/focus/disabled states | Tailwind state variants (hover:, focus:, disabled:) |
| Animations/transitions | transition-colors, transition-all, or Tailwind's built-in animation utilities |
