Frontend CSS Guidelines

This guide documents CSS best practices and conventions for the PermaplanT frontend. It establishes consistent patterns for styling components, using Tailwind utilities, CSS custom properties, and third-party library integration.

1. CSS Units

Preferred Unit Usage

  • Tailwind utility classes (first choice)
    Use Tailwind utilities for most styling needs. They provide consistent, responsive design patterns out of the box.

  • rem (relative em)
    Use for spacing and typography to support user font size preferences and maintain scalable designs.

  • px (pixels)
    Use for precise fixed values, such as:

    • Borders
    • Icon sizes
    • Exact minimum sizes
    • Minimum touch targets of 36x36px
  • % (percentage)
    Use for relative widths inside containers to make layouts flexible and responsive.

Avoid vw and vh Units

Do not use vw (viewport width) or vh (viewport height) in your CSS. These units behave inconsistently and cause usability issues:

  • they behave inconsistently on mobile browsers (iOS and Android handle them differently)
  • They interact badly with browser UI bars (address bar, tabs, navigation controls)
  • They cause scrollbar-related inconsistencies
  • They do not fit well with Tailwind's breakpoint system

Note: At the time these guidelines were written, no vw or vh units were used in the PermaplanT codebase.

2. Styling Approach Hierarchy

Apply styling in this order of preference:

1. Tailwind Utility Classes (Primary)

Use Tailwind utilities for the vast majority of component styling. These provide:

  • consistent spacing and sizing
  • Responsive design with breakpoints
  • Hover, focus, and dark mode states
  • Consistent use of design tokens and theme values

The following example demonstrates that all styling is applied through Tailwind utility classes in className, including layout, spacing, colors, dark mode, and hover states.

Example:

export function ExampleComponent() {
  return (
    <div className="flex items-center justify-between p-4 rounded-lg bg-white dark:bg-neutral-200-dark">
      <h2 className="text-xl font-semibold">Title</h2>
      <button className="px-4 py-2 rounded bg-primary-500 text-white hover:bg-primary-600">
        Action
      </button>
    </div>
  );
}

2. CSS Custom Properties from globals.css (Secondary)

Use CSS custom properties (CSS variables) from globals.css for:

  • Shared theme values
  • Colors and typography
  • Dark mode support
  • Application-specific constants

Example:

.custom-element {
  background: var(--color-primary);
  color: var(--color-neutral-50);
  padding: 1rem;
  border-radius: var(--radius-lg);

  @variant dark {
    background: var(--color-neutral-200-dark);
    color: var(--color-neutral-900-dark);
  }
}

3. Custom Utilities in globals.css (Tertiary)

Use @utility directives for reusable styling patterns not well-covered by Tailwind utilities. These become available as Tailwind utility classes across the entire application.

Example:

@utility card {
  border-radius: var(--radius-lg);
  background: var(--color-white);
  filter: drop-shadow(var(--drop-shadow-card));

  @variant dark {
    background: var(--color-neutral-200-dark);
    filter: drop-shadow(var(--drop-shadow-card-dark));
  }
}

Usage:

<div className="card">Content</div>

4. CSS Modules (Quaternary)

Use CSS Modules for complex component-specific styling that would be hard to express cleanly with Tailwind classes.

Characteristics:

  • Scoped to individual components
  • Named *.module.css, preferably named after the component, e.g., PlantCard.module.css
  • Import and use with TypeScript/JavaScript

Example:

/* AreaOfPlantingsIndicator.module.css */
.info_text__container {
  font-size: 14px;
  line-height: 1.5;
  padding: 0.5em;
  border-radius: 0.5em;
  display: grid;
  column-gap: 0.5em;
  grid-template-columns: 30px 5px minmax(30px, auto) auto;
  pointer-events: none;
}

Usage:

import styles from "./AreaOfPlantingsIndicator.module.css";

export function AreaOfPlantingsIndicator() {
  return <div className={styles.info_text__container}>...</div>;
}

5. Inline Styles (Last Resort)

Inline styles are only acceptable for:

  • Dynamic values computed from JavaScript
  • Library-specific rendering (e.g., Konva canvas props)

Example:

// Dynamic positioning
<div style={{ left: `${xPosition}px`, top: `${yPosition}px` }} />

3. CSS-in-JS

CSS-in-JS is not a general PermaplanT styling pattern. The project uses @emotion/react exclusively for specific library integration needs. Do not introduce styled-components or emotion/styled without strong justification and project approval.

Emotion Integration (react-select only)

PermaplanT uses @emotion/react exclusively for integrating react-select library styles.

Important Details:

  • EmotionCacheProvider is used to control style insertion order
  • Emotion styles are inserted before Tailwind styles so Tailwind classes have precedence
  • This is a library integration requirement, not a general PermaplanT styling pattern
  • The cache provider is defined in Providers.tsx with an insertion point before Tailwind definitions

Do Not:

  • Use emotion for general component styling
  • Introduce styled-components or emotion/styled without explicit project approval
  • Treat CSS-in-JS as a supported styling approach for new code

4. CSS Organization and Naming

File Naming Conventions

Feature-specific CSS files:

  • Use only when needed for specific features
  • Named in camelCase, e.g., guidedTour.css, geoMap.css
  • Located in frontend/src/styles/

Component-specific styles (CSS Modules):

  • Named *.module.css, preferably named after the component, e.g., PlantCard.module.css
  • Located alongside the component file
  • Only for complex component-specific styling

Global styles:

  • globals.css contains Tailwind imports, custom utilities, theme variables, and reset styles
  • Use for application-wide styling needs

CSS Module Class Naming

CSS Module class names should describe the element or purpose clearly.

Use snake_case for descriptive class names, e.g.:

.info_text__container {
}

.list_item__title {
}```

For more complex modules, a BEM-like naming style is recommended.

### Import Order Conventions

Always follow this order for imports in component files:

1. **External library CSS first** (if any)

   ```tsx
   import "external-library-styles.css";
  1. Feature-specific or component-specific CSS (if any)
    import styles from "./PanelName.module.css";
    

Note: globals.css should be imported once at the application entry point (e.g., main.tsx), not in individual component files.

5. Responsive Design

Mobile-First Approach

Always start with the mobile layout, then add larger-screen changes using Tailwind prefixes.

Why mobile-first?

  • It's often simpler and faster to add complexity for larger screens
  • Mobile users experience better-optimized designs
  • It encourages intentional layout decisions

Tailwind Breakpoints

PrefixMinimum Width
sm40rem (640px)
md48rem (768px)
lg64rem (1024px)
xl80rem (1280px)
2xl96rem (1536px)

Guidelines

  • Start with mobile layout in your base classes
  • Add responsive changes with Tailwind prefixes: md:, lg:, etc.
  • Use min-[Xpx] or max-[Xpx] custom breakpoints only when standard Tailwind breakpoints are insufficient

Example:

export function PlantGrid() {
  return (
    <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
      {/* Plant items */}
    </div>
  );
}

Existing Responsive Documentation

For layout and component-specific guidance, refer to:

Multi-Screen Layouts

  • Single-column layouts are preferred on screens smaller than 768px (< md breakpoint)
  • Minimum touch target size should be 36x36px to ensure usability on touch devices

6. Dark Mode

Implementation Approach

Dark mode should be implemented using CSS custom properties where possible:

  • Use @variant dark directive in Tailwind-aware CSS files (like globals.css) for component definitions
  • Use scoped .dark ... selectors only when necessary for third-party library overrides that require explicit dark mode handling
  • Use dark-specific CSS variables where they exist, especially the neutral-*-dark variables
  • Do not invent dark variants such as --color-primary-dark; use existing theme values from globals.css
  • Components should support both light and dark themes

Guidelines

  • Use existing CSS custom properties for theme values instead of hardcoding colors
  • Apply dark mode variants using @variant dark or the Tailwind dark modifier
  • Reference existing color variables from globals.css
  • Test components in both light and dark modes

Example

/* In globals.css or component CSS */
.card {
  background: var(--color-white);
  color: var(--color-neutral-700);

  @variant dark {
    background: var(--color-neutral-200-dark);
    color: var(--color-white);
  }
}

Tailwind dark mode class support:

export function DarkModeExample() {
  return (
    <div className="bg-white dark:bg-neutral-200-dark text-neutral-700 dark:text-white p-4 rounded-lg">
      Content with dark mode support
    </div>
  );
}

7. PermaplanT-Specific CSS Patterns

This document focuses on CSS-specific implementation rules. General UI patterns such as typography hierarchy, color usage, form styling, and z-index management are documented separately in frontend-ui-usability.md.

When implementing styles, use the existing theme values and utilities from globals.css instead of redefining design decisions locally. In particular:

  • Use the existing typography scale through Tailwind classes such as text-sm, text-lg, text-xl, and text-2xl.
  • Use the existing theme colors such as primary-*, secondary-*, neutral-*, and neutral-*-dark.
  • Use existing custom utilities such as button, input, and card where they fit the component.
  • Check the existing z-index guidance before introducing new z-index values.

For full UI pattern details, see frontend-ui-usability.md.

8. Third-Party Library CSS Integration

General Rules

  • Prefer library theming APIs when available
  • Use CSS overrides only when necessary
  • Keep third-party overrides small and well-scoped
  • Use CSS custom properties instead of hardcoded colors

Shepherd.js (Guided Tour)

File: styles/guidedTour.css

  • Overrides library styles for PermaplanT theme consistency
  • Uses CSS custom properties for colors (e.g., var(--color-primary))
  • Customizes button styling, positioning, and highlighting

Example:

.shepherd-footer .shepherd-button {
  background: var(--color-primary);
  color: var(--color-white);
  border-radius: var(--radius-lg);
}

.dark .shepherd-footer .shepherd-button {
  background: var(--color-primary-700);
}

Leaflet (OpenStreetMap Maps)

File: styles/geoMap.css

  • Keeps overrides minimal
  • Customizes container height and dark mode styling
  • Prefers CSS variables over hardcoded colors

Example:

.leaflet-container {
  height: 100%;
  background-color: inherit !important;
}

.dark .leaflet-bar a {
  background-color: var(--color-neutral-200-dark);
}

react-select

Integration approach:

  • Uses Emotion only for style insertion order control (not for component styling)
  • Styling is handled via the library's theming API and default styles
  • Custom styling should use the library's built-in options rather than CSS overrides

Note: Emotion is configured to insert styles before Tailwind definitions so that library styles and Tailwind classes coexist properly. See EmotionCacheProvider in Providers.tsx for implementation details.

Konva (Canvas)

  • Canvas-based rendering (not DOM elements)
  • Do not style via CSS
  • Use JavaScript props for styling (fill, stroke, etc.)

Example:

<Rect
  width={100}
  height={100}
  fill={primaryColor}
  stroke="black"
  strokeWidth={2}
/>

9. Cross-References

The following guidelines documents provide additional context for CSS decisions:

10. Examples

Example 1: Responsive Component with Tailwind

A responsive component pattern using Tailwind utilities for layout and dark mode support. This demonstrates how to apply responsive classes and dark mode variants in real components:

export function PlantCard({ plant }) {
  return (
    <div className="grid grid-cols-1 gap-4 md:grid-cols-2 lg:grid-cols-3">
      <article className="p-4 rounded-lg bg-white dark:bg-neutral-200-dark shadow-md hover:shadow-lg transition-shadow">
        <h2 className="text-xl font-semibold mb-2 text-neutral-800 dark:text-white">
          {plant.name}
        </h2>
        <p className="text-sm text-neutral-600 dark:text-neutral-400 mb-4">
          {plant.description}
        </p>
        <button className="w-full px-4 py-2 rounded-lg bg-primary-500 text-white hover:bg-primary-600 focus:ring-2 focus:ring-primary-300">
          View Details
        </button>
      </article>
    </div>
  );
}

Example 2: Custom Utility in globals.css

The card utility is defined in globals.css and demonstrates the preferred pattern for reusable custom utilities.

/* In globals.css */
@utility card {
  border-radius: var(--radius-lg);
  background: var(--color-white);
  filter: drop-shadow(var(--drop-shadow-card));

  @variant dark {
    background: var(--color-neutral-200-dark);
    filter: drop-shadow(var(--drop-shadow-card-dark));
  }
}

Usage:

export function CardExample() {
  return (
    <div className="card p-4">
      <h3 className="text-lg font-semibold">Card Title</h3>
      <p>Card content goes here.</p>
    </div>
  );
}

Example 3: Component-Specific Styles with CSS Modules

Complex component layout using CSS Modules (existing code example):

/* AreaOfPlantingsIndicator.module.css */
.info_text__container {
  font-size: 14px;
  line-height: 1.5;
  padding: 0.5em;
  border-radius: 0.5em;
  display: grid;
  column-gap: 0.5em;
  grid-template-columns: 30px 5px minmax(30px, auto) auto;
  pointer-events: none;
}

.info_text__icon {
  width: 30px;
  height: 30px;
  display: flex;
  align-items: center;
  justify-content: center;
}

Note: This example uses px and em units because it requires precise grid layout with fixed column widths. CSS Modules are appropriate for complex layouts like this where Tailwind utilities alone would be cumbersome.

About em units: Use em when a value should scale relative to the current element's font size. In this example, 0.5em padding and 0.5em border-radius will scale with the font size, making the component proportionally balanced at any font size.

Usage:

import styles from "./AreaOfPlantingsIndicator.module.css";

export function AreaOfPlantingsIndicator() {
  return (
    <div className={styles.info_text__container}>
      <div className={styles.info_text__icon}>●</div>
      <div className={styles.info_text__content}>{/* content */}</div>
    </div>
  );
}

Example 4: CSS Custom Properties for Theming

Use existing CSS custom properties from globals.css instead of redefining theme values in component-specific CSS.

.themed-button {
  background: var(--color-primary);
  color: var(--color-white);
  border-radius: var(--radius-lg);
  transition: background-color 0.2s;

  &:hover {
    background: var(--color-primary-600);
  }

  @variant dark {
    background: var(--color-primary-700);
  }
}

Example 5: Third-Party Library Style Override

Customizing Shepherd.js (guided tour) styling while maintaining theme consistency:

/* In styles/guidedTour.css */
.shepherd-footer .shepherd-button {
  background: var(--color-primary);
  color: var(--color-white);
  border-radius: var(--radius-lg);
  font-size: var(--text-sm);
  font-weight: 500;
  border-width: 1px;
  transition: background-color 0.2s;
}

.shepherd-footer .shepherd-button:hover:not(:disabled) {
  background: var(--color-primary-800);
}

/* Dark mode support */
.dark .shepherd-content {
  background: var(--color-neutral-200-dark);
  color: var(--color-white);
}

.dark .shepherd-footer .shepherd-button {
  background: var(--color-primary-700);
}

Summary

Prioritize styling in this order: Tailwind utilities → CSS custom properties → custom utilities → CSS Modules → inline styles. This hierarchy ensures consistent, maintainable styling across the PermaplanT frontend.