Skip to content

Styling Guide

All styles for the Lens frontend live in a single CSS file: foundation/static/main/src/foundation.css (9000+ lines). The app does not use CSS modules, styled-components, or any CSS-in-JS solution.

foundation.css
├── Status spinner, tooltips, toolbar helpers (lines 1-150)
├── Quick filter bar, rollup styles (lines 150-180)
├── Sync agent section (lines 180-300)
├── Inline editors (cell-editor-*, picker-*) (lines 300-600)
├── AG Grid theme overrides (.ag-theme-alpine) (lines 1000-1800)
├── Gantt chart (lines 3000-4000)
├── Inspector panel & accordion sections (lines 4000-4500)
├── Lens cards, lens menu (lines 5000-6000)
├── Column header menus (lines 7500-8200)
├── Pipeline studio (lines 8200-8500)
├── Onboarding, templates, import (lines 8500-9000)
└── Inline create panel, various features (lines 9000+)

All colors use ADS (Atlassian Design System) CSS custom properties with hex fallbacks:

/* Correct — always include a hex fallback */
color: var(--ds-text, #172B4D);
background: var(--ds-surface-sunken, #F7F8F9);
border-color: var(--ds-border, #DFE1E6);
/* Wrong — never use bare hex values for standard colors */
color: #172B4D;

The fallback ensures the app renders correctly when ADS tokens are unavailable (e.g., in test environments or if theme injection fails).

TokenPurposeFallback
--ds-textPrimary text#172B4D
--ds-text-subtleSecondary text#626F86
--ds-text-subtlestTertiary text#626F86
--ds-text-inverseText on dark backgrounds#FFFFFF
--ds-surfaceDefault background#FFFFFF
--ds-surface-sunkenRecessed background#F7F8F9
--ds-surface-hoveredHover background#EBECF0
--ds-background-selectedSelected row#DEEBFF
--ds-background-brand-boldPrimary action#0052CC
--ds-borderDefault border#DFE1E6
--ds-linkLinks#0052CC
--ds-iconStandard icons#505F79
--ds-icon-subtleSubtle icons#6B778C
--ds-icon-brandBrand-colored icons#0052CC

Certain semantic colors are hardcoded (not from ADS tokens) for specific feature meanings:

ColorHexUsage
Purple#6554C0Flex items, user avatar fallback backgrounds
Amber#D97706Milestones
Green#00875ASuccess states, toggles ON
Red#DE350B / #FF5630Danger states, destructive actions
Blue#0052CCPrimary actions, links, focus rings

The app supports 5 density modes, controlled by a CSS class on the grid container:

ModeCSS ClassRow HeightHeader Height
Comfortable.density-comfortable44px34px
Standard.density-standard36px32px
Compact.density-compact28px28px
Supercompact.density-supercompact22px22px
Micro.density-micro16px16px

Density is implemented via parent class selectors that override child component sizing:

/* Base (comfortable — the default) */
.sa-section { padding: 12px 16px; }
/* Compact overrides */
.density-compact .sa-section { padding: 2px 6px; }
.density-compact .sa-row { font-size: 11px; padding: 1px 0; }
/* Standard overrides */
.density-standard .quick-filter-bar { padding: 3px 10px; min-height: 28px; }

Row heights also have a “with description” variant when stacked description subtitles are enabled:

ModeWith Description
Comfortable62px
Standard54px
Compact42px
Supercompact32px
Micro16px (never shows descriptions)

Rule: Every new component must render correctly at all 5 density levels. Test at comfortable, compact, and micro at minimum.

CSS classes (in foundation.css) for:

  • Static layout and positioning
  • Theme-based colors using ADS tokens
  • Density overrides
  • Hover/focus/active states
  • AG Grid theme customization

Inline styles for:

  • Dynamic/computed values (indentation depth, progress bar width)
  • Conditional formatting (user-defined background/text colors)
  • Density-aware sizing that depends on runtime calculations
  • Icon sizing that varies by context
// Correct — dynamic value needs inline style
<div style={{ paddingLeft: depth * 24 }}>
// Correct — conditional formatting from user rules
<div style={{ backgroundColor: rule.bgColor, color: rule.textColor }}>
// Wrong — static style should be a CSS class
<div style={{ fontSize: 12, fontWeight: 600 }}>
SizeWeightUse
11px600, uppercase, letter-spacing: 0.04emSection headers in inspector
12px400-600Labels, badges, form hints, metadata
13px400Secondary body text, metadata
14px400Primary body text, grid cells (comfortable density)
16px600Page titles, card titles

Font weight conventions:

  • 600 — headers, labels, emphasized text
  • 500 — subheadings
  • 400 — body text

All menus and dropdowns use position: fixed with viewport clamping — never position: absolute relative to a parent:

// Typical pattern in menu components
function clampToViewport(rect: DOMRect): { top: number; left: number } {
const { innerWidth, innerHeight } = window;
let top = rect.bottom;
let left = rect.left;
// Clamp to viewport edges
if (top + menuHeight > innerHeight) top = rect.top - menuHeight;
if (left + menuWidth > innerWidth) left = innerWidth - menuWidth;
return { top: Math.max(0, top), left: Math.max(0, left) };
}

Close behavior: Menus close on outside mousedown (not click) via document.addEventListener('mousedown', ...). Using mousedown prevents the dropdown from closing and then immediately reopening when the trigger button is clicked.

Standard dimensions:

  • Width: 180px (column menus), varies for other menus
  • Max height: 300px with overflow-y: auto
  • Border radius: 8px
  • Shadow: box-shadow: 0 8px 16px rgba(9, 30, 66, 0.25) (elevation-3)
DurationEasingUse
100mseaseBackground color changes
150mseaseHover states, opacity reveals
200mseaseExpand/collapse, panel open/close, chevron rotation
250mscubic-bezier(0.4, 0, 0.2, 1)Panel slide transitions
/* Chevron rotation example */
.chevron { transition: transform 200ms ease; }
.chevron.expanded { transform: rotate(90deg); }
/* Hover example */
.toolbar-btn { transition: background-color 100ms ease; }
.toolbar-btn:hover { background-color: var(--ds-surface-hovered, #EBECF0); }

The app uses AG Grid’s legacy CSS theme (ag-theme-alpine) with extensive CSS variable overrides:

.ag-theme-alpine {
--ag-background-color: var(--ds-surface, #FFFFFF);
--ag-header-background-color: var(--ds-surface-sunken, #FAFBFC);
--ag-header-foreground-color: var(--ds-text-subtle, #626F86);
--ag-selected-row-background-color: var(--ds-background-selected, #DEEBFF);
--ag-border-color: var(--ds-border, #DFE1E6);
--ag-row-hover-color: transparent !important; /* Disabled — see hover section */
}

AG Grid has two independent hover mechanisms:

  1. JS-based hover (.ag-row-hover class): AG Grid calculates which row the mouse is over via Y-position math and adds .ag-row-hover to that row’s DOM element. This fires even in empty viewport space below the last data row, creating phantom hover highlights.

  2. CSS-based hover (.ag-row:hover): Standard CSS pseudo-class. Only fires when the mouse is actually over an .ag-row DOM element.

The solution:

// In HierarchyGrid.tsx:
<AgGridReact
suppressRowHoverHighlight={true} // Disable JS-based hover (#1)
theme="legacy" // Use CSS file themes
/>
/* In foundation.css: */
/* Kill any residual AG Grid hover coloring */
--ag-row-hover-color: transparent !important;
/* Neutralize JS-based hover pseudo-element */
.ag-theme-alpine .ag-row-hover:not(.ag-full-width-row)::before {
background-color: transparent !important;
background-image: none !important;
}
/* Pure CSS hover — only fires on real data rows */
.ag-theme-alpine .ag-row[row-id]:hover {
background: var(--ds-surface-hovered, #F4F5F7);
}

The [row-id] attribute selector ensures hover only applies to rows that AG Grid has assigned data to, preventing highlights in empty viewport space.

Rule: Never re-enable AG Grid’s JS hover (suppressRowHoverHighlight={false}) or set --ag-row-hover-color to a non-transparent value.

Drag handles are hidden by default and revealed on row hover via CSS:

.ag-theme-alpine .ag-row .ag-row-drag {
opacity: 0;
transition: opacity 0.15s ease;
}
.ag-theme-alpine .ag-row[row-id]:hover .ag-row-drag,
.ag-theme-alpine .ag-row-selected .ag-row-drag {
opacity: 1;
}

Similarly, selection checkboxes are hidden until hover or selection:

.ag-theme-alpine .ag-row .ag-selection-checkbox {
visibility: hidden;
}
.ag-theme-alpine .ag-row[row-id]:hover .ag-selection-checkbox,
.ag-theme-alpine .ag-row-selected .ag-selection-checkbox {
visibility: visible;
}

During cell editing, drag handles and interactive elements are suppressed:

.ag-editing-active .ag-row[row-id]:hover .ag-row-drag {
opacity: 0 !important;
}

The app uses a custom tooltip component (components/common/Tooltip.tsx) instead of ADS tooltips. Tooltips use position: fixed and are viewport-clamped.

.foundation-tooltip {
position: fixed;
z-index: 6000;
padding: 10px 12px;
border-radius: 8px;
background: rgba(23, 43, 77, 0.96);
color: var(--ds-text-inverse, #FFFFFF);
box-shadow: 0 12px 24px rgba(9, 30, 66, 0.2);
transition: opacity 140ms ease, transform 140ms ease;
}

Tooltips support placement (top, bottom, left, right) with slide-in animations and optional arrow indicators.

  • All icons are inline SVGs — no icon libraries
  • Standard viewBox="0 0 24 24", sized 14-16px (density-aware)
  • Priority icons: utils/priorityIcons.tsx (colored arrow SVGs)
  • Issue type icons: utils/issueTypeIcons.tsx (colored category SVGs)
  • Chevrons: inline SVG with rotate(90deg) when expanded, 200ms ease transition

Use AccordionSection with the following required props:

  • sectionId — unique ID for localStorage persistence
  • title — section header text
  • badge — optional count badge
  • defaultExpanded — initial state
  • onToggle — callback for expand/collapse

Use AccordionSection (same component) without inspector-specific props.

Use @atlaskit/button/new with spacing="compact" in toolbars and panels:

import Button from '@atlaskit/button/new';
// Primary action — one per visible context
<Button appearance="primary" spacing="compact">Create Lens</Button>
// Toolbar/header actions
<Button appearance="subtle" spacing="compact">Options</Button>
// Destructive actions
<Button appearance="danger" spacing="compact">Delete</Button>
html, body {
overscroll-behavior-x: none;
}

Prevents browser back/forward navigation on horizontal trackpad swipes, which would navigate away from the Forge iframe.

Z-IndexElement
6000Tooltips
3000Announcement popup
1000+Modal dialogs (via @atlaskit/modal-dialog)
100-200Menus, dropdowns
10Fixed headers
0Grid content