Styling Rules

Guidelines for CSS and Tailwind styling in the Eko codebase.

Tailwind v4 CSS Layers

Eko uses Tailwind v4, which introduces CSS @layer for utility classes. This has important implications for CSS specificity.

TL;DR

When overriding base component styles with padding/margin, use inline style props instead of Tailwind utility classes.

The Problem

In Tailwind v4, utility classes are wrapped in @layer utilities:

@layer utilities {
  .pl-11 { padding-left: 2.75rem; }
}

Our components.css contains unlayered base styles:

/* NOT in @layer - has higher precedence! */
:where(input, [data-slot="input"]) {
  padding: var(--space-2) var(--space-3); /* 8px 12px */
}

CSS Cascade Rule: Unlayered styles always beat layered styles, regardless of specificity.

This means:

  • className="pl-11" → Gets overridden by components.css (12px wins)
  • style={{ paddingLeft: '2.75rem' }} → Inline styles have highest specificity (44px wins)

When This Applies

ScenarioUse Tailwind Class?Use Inline Style?
New component with no base styles✅ YesNo need
Overriding <input> padding❌ No✅ Yes
Overriding <Input> component padding❌ No✅ Yes
Overriding styles from components.css❌ No✅ Yes
Adding non-conflicting styles (colors, borders)✅ YesNo need

Example: Search Input with Icon

// ❌ WRONG - pl-11 will be overridden by components.css
<div className="relative">
  <SearchIcon className="absolute left-3 top-1/2 -translate-y-1/2 size-4" />
  <input className="pl-11" placeholder="Search..." />
</div>

// ✅ CORRECT - inline style overrides everything
<div className="relative">
  <SearchIcon className="absolute left-3 top-1/2 -translate-y-1/2 size-4" />
  <input
    className="pr-3 py-2"
    style={{ paddingLeft: '2.75rem' }}
    placeholder="Search..."
  />
</div>

Spacing Reference

Tailwind ClassRem ValuePixel Value
pl-92.25rem36px
pl-102.5rem40px
pl-112.75rem44px
pl-123rem48px

Alternative: Use InputGroup

For inputs with icons/addons, prefer the InputGroup compound component pattern which uses flexbox instead of absolute positioning:

// ✅ BEST - InputGroup handles spacing automatically
<InputGroup>
  <InputGroupAddon align="inline-start">
    <SearchIcon className="size-4" />
  </InputGroupAddon>
  <InputGroupInput placeholder="Search..." />
</InputGroup>

Affected Components (as of 2026-01-27)

These components use inline styles to work around this issue:

  • packages/ui/src/components/layout/app-sidebar-search.tsx
  • packages/ui/src/components/data-table/data-table.tsx
  • packages/ui/src/components/docs/docs-search.tsx

Future Consideration

A more permanent fix would be to wrap components.css styles in a lower-precedence layer:

@layer base {
  :where(input) { padding: ... }
}

This would allow @layer utilities to override @layer base. However, this requires careful testing across all components.


Interactive Element Cursor Rules

Interactive elements (links, buttons, clickable items) must have consistent cursor behavior across their entire clickable area.

TL;DR

All interactive elements must include cursor-pointer select-none to prevent text cursor (caret) from appearing on hover.

The Problem

When an interactive element contains text, browsers can show the I-beam (text selection) cursor when hovering over the text portion, even if the element is clickable. This creates a confusing UX:

  • Hovering over icon/padding → pointer cursor ✓
  • Hovering over text → text cursor (caret) ✗

Users interpret the text cursor as "this text is selectable" rather than "this is clickable".

Root Causes

  1. Missing user-select: none: Text inside interactive elements remains selectable by default
  2. Child elements with cursor-text: Nested elements (like inline-editable fields) may override parent cursor
  3. Missing cursor-pointer: Buttons don't have pointer cursor by default; it must be explicitly set

Required Styles for Interactive Elements

// ✅ CORRECT - Interactive element with proper cursor/selection
<button className="cursor-pointer select-none ...">
  <Icon />
  <span>Click me</span>
</button>

// ✅ CORRECT - Link-like sidebar item
<a className="flex items-center cursor-pointer select-none ...">
  <Icon />
  <span>Navigation Item</span>
</a>

// ❌ WRONG - Missing cursor/selection styles
<button className="flex items-center ...">
  <Icon />
  <span>Click me</span>  {/* Text cursor appears here! */}
</button>

// ❌ WRONG - Child element overrides cursor
<a className="cursor-pointer select-none ...">
  <Icon />
  <span className="cursor-text">Editable Name</span>  {/* Breaks parent cursor! */}
</a>

Checklist for Interactive Components

When creating or modifying interactive components, verify:

  • Root element has cursor-pointer
  • Root element has select-none (maps to user-select: none)
  • No child elements override cursor (e.g., cursor-text, cursor-default)
  • Entire clickable area shows pointer on hover (test by hovering over text specifically)

Components That Must Follow This Rule

Component TypeExamples
Sidebar navigation itemsSidebarMenuButton, SidebarMenuSubButton
Inline-editable elementsInlineFolderRename (when in display mode)
Clickable cardsInteractive cards with text content
Custom buttonsAny button-like element with text
List items with actionsFolder items, menu items

Exception: Actual Text Inputs

For elements that ARE meant for text input/selection:

  • Text inputs (<input type="text">)
  • Textareas
  • Contenteditable elements
  • Elements in "edit mode" (e.g., inline rename when actively editing)

These SHOULD use cursor-text and allow selection.

Real Example: InlineFolderRename

The InlineFolderRename component displays folder names that are double-clickable to rename:

// ❌ WRONG - Used cursor-text to hint at double-click rename
<button className="cursor-text ...">
  {name}
</button>

// ✅ CORRECT - Use cursor-pointer; hint via title tooltip instead
<button
  className="cursor-pointer select-none ..."
  title="Double-click to rename"
>
  {name}
</button>

Rule Summary

IDRuleDescription
STY-101Inline padding overridesUse inline style for padding when overriding base input styles
STY-102Prefer InputGroupUse InputGroup pattern for inputs with icons/addons
STY-103CSS layer awarenessKnow that Tailwind v4 utilities are in @layer utilities
STY-104Interactive cursor stylesAll interactive elements must have cursor-pointer select-none
STY-105No child cursor overridesChild elements must not override parent's cursor on interactive elements