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:
EmotionCacheProvideris 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.tsxwith 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.csscontains 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";
- 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
| Prefix | Minimum Width |
|---|---|
| sm | 40rem (640px) |
| md | 48rem (768px) |
| lg | 64rem (1024px) |
| xl | 80rem (1280px) |
| 2xl | 96rem (1536px) |
Guidelines
- Start with mobile layout in your base classes
- Add responsive changes with Tailwind prefixes:
md:,lg:, etc. - Use
min-[Xpx]ormax-[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:
frontend-general-responsiveness.md- General layout and usability best practicesfrontend-component-responsiveness.md- PermaplanT component-specific responsive guidelines
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 darkdirective 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-*-darkvariables - Do not invent dark variants such as
--color-primary-dark; use existing theme values fromglobals.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 darkor 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, andtext-2xl. - Use the existing theme colors such as
primary-*,secondary-*,neutral-*, andneutral-*-dark. - Use existing custom utilities such as
button,input, andcardwhere 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:
- frontend-ui-usability.md - UI/UX patterns, typography sizes, color palettes, form styling, Z-index management
- frontend-general-responsiveness.md - General responsive layout patterns and Tailwind breakpoints
- frontend-component-responsiveness.md - Component-specific responsive guidelines (map, toolbar, navbar)
- frontend.md - General frontend development practices and structure
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.