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
styleprops 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
| Scenario | Use Tailwind Class? | Use Inline Style? |
|---|---|---|
| New component with no base styles | ✅ Yes | No 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) | ✅ Yes | No 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 Class | Rem Value | Pixel Value |
|---|---|---|
pl-9 | 2.25rem | 36px |
pl-10 | 2.5rem | 40px |
pl-11 | 2.75rem | 44px |
pl-12 | 3rem | 48px |
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.tsxpackages/ui/src/components/data-table/data-table.tsxpackages/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-noneto 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
- Missing
user-select: none: Text inside interactive elements remains selectable by default - Child elements with
cursor-text: Nested elements (like inline-editable fields) may override parent cursor - 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 touser-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 Type | Examples |
|---|---|
| Sidebar navigation items | SidebarMenuButton, SidebarMenuSubButton |
| Inline-editable elements | InlineFolderRename (when in display mode) |
| Clickable cards | Interactive cards with text content |
| Custom buttons | Any button-like element with text |
| List items with actions | Folder 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
| ID | Rule | Description |
|---|---|---|
| STY-101 | Inline padding overrides | Use inline style for padding when overriding base input styles |
| STY-102 | Prefer InputGroup | Use InputGroup pattern for inputs with icons/addons |
| STY-103 | CSS layer awareness | Know that Tailwind v4 utilities are in @layer utilities |
| STY-104 | Interactive cursor styles | All interactive elements must have cursor-pointer select-none |
| STY-105 | No child cursor overrides | Child elements must not override parent's cursor on interactive elements |