ux-css-layout
$
npx mdskill add microsoft/vscode/ux-css-layoutThis skill covers CSS file organization, naming, standard sizes, programmatic layout (SplitView, Grid, scrollable), responsive patterns, and text overflow handling.
SKILL.md
.github/skills/ux-css-layoutView on GitHub ↗
---
name: ux-css-layout
description: VS Code CSS conventions, file organization, class naming, standard sizes, SplitView/Grid layout, scrollable content, responsive layout, and text overflow/ellipsis patterns. Use when writing CSS, building layouts, or fixing text truncation issues.
---
This skill covers CSS file organization, naming, standard sizes, programmatic layout (SplitView, Grid, scrollable), responsive patterns, and text overflow handling.
---
## 1. File Organization
CSS files are **co-located** with their TypeScript components:
```
src/vs/base/browser/ui/button/
button.ts
button.css
src/vs/workbench/contrib/myFeature/browser/
myFeature.ts
media/
myFeature.css
```
Import CSS directly in the TS file:
```typescript
import './media/myFeature.css';
// or for base widgets:
import './button.css';
```
Workbench-level global styles live in `src/vs/workbench/browser/media/`.
## 2. Class Naming
- **`monaco-` prefix** for all major components: `.monaco-workbench`, `.monaco-split-view2`, `.monaco-scrollable-element`
- **Modifier classes**: `.monaco-split-view2.vertical`, `.monaco-split-view2.horizontal`
- **State classes**: `.visible`, `.focused`, `.active`, `.highlight`
- Feature-specific classes use kebab-case without prefix: `.my-feature`, `.outline-pane`, `.welcome-view-content`
## 3. Standard Sizes
| Element | Size |
|---------|------|
| Part title height | 35px |
| Title padding (horizontal) | 8px |
| Title label inner padding | 12px |
| Action area padding | 5px |
| Action icon size | 16px |
| Body font-size | 13px (workbench), 11px (HTML body) |
| Line height | 1.4em |
| Validation message font-size | 12px (line-height: 17px) |
> For `padding`/`margin`/`gap`, `border-radius`, `font-size`/`font-weight`,
> codicon size and border width, prefer the design-system **size tokens** over
> raw px — see [§10 Design-System Size Tokens](#10-design-system-size-tokens-spacing-radius-font-codicon-stroke).
> Canonical reference: `.github/instructions/design-tokens.instructions.md`
> (auto-injected for `src/vs/**/*.css`).
## 4. CSS Selector Quality
| Anti-pattern (flagged) | Correct pattern |
|------------------------|-----------------|
| ID selectors for styling (`#my-widget`) | Class selectors (`.my-widget`) |
| Overly specific selectors | Minimal specificity needed |
| Styles in the wrong file | Co-located with the component |
| Missing `min-width: 0` on flex children | Prevents truncation issues |
| Forgetting `pointer-events: none` on hidden overlays | Prevents click-through bugs |
## 5. SplitView Layout
**File**: `src/vs/base/browser/ui/splitview/splitview.ts`
For splitting views with draggable sashes (either horizontal or vertical):
```typescript
const splitView = new SplitView(container, {
orientation: Orientation.VERTICAL,
proportionalLayout: true,
styles: { separatorBorder: asCssVariable(sashBorder) }
});
// Each view implements IView:
const myView: IView = {
element: myDomNode,
minimumSize: 100,
maximumSize: Number.POSITIVE_INFINITY,
onDidChange: Event.None,
layout(size, offset) { /* resize content */ }
};
splitView.addView(myView, Sizing.Distribute);
```
Use `LayoutPriority.High` / `.Low` to control which views resize first when space is constrained. Use `snap: true` to allow views to snap closed.
## 6. Grid Layout
**File**: `src/vs/base/browser/ui/grid/grid.ts`
For 2D layouts (used by editor groups):
```typescript
const grid = new Grid(initialView);
grid.addView(newView, Sizing.Distribute, referenceView, Direction.Right);
```
## 7. Scrollable Content
Three classes for different needs:
| Class | When to Use |
|-------|-------------|
| `SmoothScrollableElement` | Animated scrolling (SplitView, ListView) |
| `DomScrollableElement` | Wrap existing DOM content (hovers, menus, breadcrumbs) |
| `ScrollableElement` | Basic single-direction scrollbar |
```typescript
const scrollable = new DomScrollableElement(contentNode, {
horizontal: ScrollbarVisibility.Auto,
vertical: ScrollbarVisibility.Auto
});
this._register(scrollable);
container.appendChild(scrollable.getDomNode());
scrollable.scanDomNode(); // call after content changes
```
## 8. Responsive Layout
VS Code does **not** use CSS media queries. Instead, it uses a **programmatic constraint-based layout system**:
- `IView.minimumSize` / `maximumSize` — views declare their size constraints.
- `SplitView` and `Grid` distribute space according to constraints and `LayoutPriority`.
- `ResizeObserver` is used for container-aware sizing (e.g., editor auto-layout).
- The window is treated as a fixed viewport; space is distributed via sash-based resizing.
When building a responsive component:
1. Set `minimumSize` / `maximumSize` appropriately.
2. Use `LayoutPriority.Low` for panels that should collapse first.
3. Use `snap: true` for panels that should snap closed when too small.
4. Fire `onDidChange` when your constraints change dynamically.
## 9. Text Overflow & Ellipsis
All text labels that can be truncated by a resizable container **must** use the ellipsis pattern. Clipped text without an ellipsis is a visual bug.
### Standard Ellipsis Pattern (CSS)
The three-property combo is required — all three must be present:
```css
.my-label {
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
```
### Common Locations That Need Ellipsis
| Element | Why |
|---------|-----|
| Part title labels (`h2`, breadcrumbs) | Sidebar/panel can be resized narrower than the title |
| View pane header titles | View containers can be narrow |
| List/tree row labels | Rows have a fixed width from the list container |
| Tab labels (editor tabs) | Many tabs shrink to fit |
| Button labels in welcome views | Buttons have `max-width` constraints |
| Status bar items | Many items compete for horizontal space |
| Notification message text | Notification toast/center has fixed width |
| Tooltip/hover headings | Hovers have max-width |
| Dropdown/select items | Select boxes have bounded width |
| Badge text / descriptions | Auxiliary text in constrained columns |
### Flex Container Gotchas
Flex children default to `min-width: auto`, which **prevents** `text-overflow: ellipsis` from working because the flex item refuses to shrink below its content width. Fix this by setting `min-width: 0` on the flex child:
```css
/* WRONG — ellipsis will NOT trigger inside a flex container */
.flex-parent {
display: flex;
}
.flex-parent > .label {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
/* CORRECT — add min-width: 0 so the flex item can shrink */
.flex-parent > .label {
min-width: 0; /* ← this is the fix */
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
```
This pattern is used throughout VS Code — for example, `.monaco-icon-label-container` sets `min-width: 0` and `flex: 1` to allow label text to truncate.
### Fixed vs Flexible Elements
When a row has both fixed-size elements (icons, action buttons) and flexible text:
```css
.row {
display: flex;
align-items: center;
}
.row > .icon {
flex-shrink: 0; /* icon never shrinks */
width: 16px;
}
.row > .label {
flex: 1; /* label takes remaining space */
min-width: 0; /* allows shrinking below content width */
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.row > .actions {
flex-shrink: 0; /* action buttons never shrink */
}
```
This is the standard pattern for tree rows, list items, tab labels, and view pane headers.
### Hover for Full Text
When text is truncated with ellipsis, the **full text must be accessible** via hover tooltip. Use `IHoverService.setupDelayedHover()` with the full untruncated text so users can read it:
```typescript
this._register(this.hoverService.setupDelayedHover(labelElement, {
content: fullLabelText,
}));
```
For `IconLabel` and list/tree renderers, this is handled automatically. For custom DOM, you must add it manually.
### Anti-Patterns (NEVER DO)
- **Never** let text clip without an ellipsis — if `overflow: hidden` is set, `text-overflow: ellipsis` must also be set.
- **Never** rely on a fixed pixel width for text that could be localized — localized strings vary in length.
- **Never** use `text-overflow: ellipsis` without `overflow: hidden` and `white-space: nowrap` — all three are required.
- **Never** forget `min-width: 0` on flex children that need to truncate.
- **Never** truncate text without providing a hover/tooltip for the full string.
---
## 10. Design-System Size Tokens (spacing, radius, font, codicon, stroke)
VS Code ships a design-system **size** ramp, registered in
`src/vs/platform/theme/common/sizes/baseSizes.ts` (agents font ramp in
`src/vs/sessions/common/sizes.ts`) and emitted as `--vscode-*` CSS variables.
When writing or editing CSS, prefer the token var over a raw px value wherever a
token exists. The full tables + rationale live in the auto-injected
`.github/instructions/design-tokens.instructions.md` (canonical source — keep
this section in sync with it). This section captures the **decision logic** for
deeper styling tasks.
> Every `--vscode-*` size var you reference must already exist in
> `build/lib/stylelint/vscode-known-variables.json` (`"sizes"` array,
> alphabetically sorted) or stylelint/hygiene fails. Adding a *new* token means
> adding it both in `baseSizes.ts` and that JSON file.
### Spacing — `padding`, `margin`, `gap`
Scale (px): `0, 2, 4, 6, 8, 10, 12, 16, 20, 24, 28, 32, 36, 40` →
`--vscode-spacing-sizeNone`, `--vscode-spacing-size20` … `--vscode-spacing-size400`
(token number = px × 10, so `size200` = 20px).
**What matters is the value, not the token.** Adopting the `var()` is optional —
a raw px value is fine **as long as it lands on the scale**. What breaks rhythm is
an **off-scale** value (3, 5, 7, 14, 26px…). Snap off-scale values to the nearest
scale value, **ties round up** (`5px → 6px`, `3px → 4px`, `1px → 2px`,
`26px → 28px`). Each length of a shorthand is checked independently
(`0 5px → 0 6px`). Leave `auto`, `%`, `em`/`rem`, `var()`/`calc()` untouched.
### Corner radius — `border-radius`
| px | Variable | Use |
|----|----------|-----|
| 2 | `--vscode-cornerRadius-xSmall` | very compact elements |
| 4 | `--vscode-cornerRadius-small` | controls (buttons, inputs) |
| 6 | `--vscode-cornerRadius-medium` | base / inner surfaces |
| 8 | `--vscode-cornerRadius-large` | prominent / outer surfaces |
| 12 | `--vscode-cornerRadius-xLarge` | very prominent surfaces |
| 9999 | `--vscode-cornerRadius-circle` | fully rounded (pills, dots) |
**Snap map** for off-scale literals (ties round **up**):
`2→xSmall`, `3,4→small`, `5,6→medium`, `7,8→large`, `10,11,12→xLarge`,
`14,16,18,20→xLarge`, `999→circle`.
- **Pills** (radius ≈ half the element height — e.g. `28h`/`14r`, `36h`/`18r`,
`22×22`/`11r`) → `--vscode-cornerRadius-circle`, **not** xLarge. The
literal-nearest token would square them and lose the fully-rounded intent.
- **Leave untouched:** `50%`, `0`, `0px`, `inherit`, any `calc()`/`var()`.
Preserve `!important`.
### Font size & weight
Generic UI chrome (fixed px): `13 → --vscode-bodyFontSize` (base),
`12 → --vscode-bodyFontSize-small`, `11 → --vscode-bodyFontSize-xSmall`.
Agents window (`src/vs/sessions/**`) ramp — pair a **size** token with a
**weight** token:
| px | Size var | Weight |
|----|----------|--------|
| 26 | `--vscode-agents-fontSize-heading1` | semiBold |
| 18 | `--vscode-agents-fontSize-heading2` | semiBold |
| 13 | `--vscode-agents-fontSize-heading3` | semiBold |
| 13 | `--vscode-agents-fontSize-body1` | regular |
| 11 | `--vscode-agents-fontSize-body2` | regular |
| 12 | `--vscode-agents-fontSize-label1` | regular |
| 11 | `--vscode-agents-fontSize-label2` | regular |
| 10 | `--vscode-agents-fontSize-label3` | regular |
The agents weight ramp is **two weights only**:
`--vscode-agents-fontWeight-regular` (400) and
`--vscode-agents-fontWeight-semiBold` (600).
- **No medium (500).** `font-weight: 500` is off the ramp — snap to `semiBold`.
Likewise `700`/`bold` → round to the nearer of 400/600.
- **"Strong" is not a separate size.** "Body 1 Strong" = the matching
`--vscode-agents-fontSize-*` size token + `semiBold`. Never add a strong *size*.
- `normal` ≡ 400 → `regular`. Leave `inherit`, `lighter`, `bolder`,
`var()`/`calc()` untouched.
### Codicon size — icon `font-size`
Codicons are **only ever 16px or 12px** — never `14px` or any in-between value.
| px | Variable | Use |
|----|----------|-----|
| 16 | `--vscode-codiconFontSize` (base) | default icon size |
| 12 | `--vscode-codiconFontSize-compact` | dense/inline chrome |
**Compact-glyph convention:** when sizing an icon at the compact 12px size, also
swap the registered glyph to its `*Compact` variant (e.g. `Codicon.close` →
`Codicon.closeCompact`, `Codicon.add` → `Codicon.addCompact`). CSS `font-size`
alone only scales the icon — it does **not** change to the visually-optimized
compact glyph; that requires changing the registered icon (Action2 `icon:` /
`renderIcon`). **Only swap the glyph when no CSS selector targets the original
glyph class** (e.g. `.codicon-close`); selectors keyed on the glyph class
(`.codicon-add`, `.codicon-chevron-down`) break when the class becomes
`-compact`, so update those selectors too (or size via a glyph-independent
wrapper class like `.monaco-button`). Some icons (settings/sliders, agent, vm,
info, lock, plus) have **no** compact variant — keep the regular glyph at 12px.
### Stroke — border width
A **single** stroke thickness: `1px` → `--vscode-strokeThickness`. Applies to the
`border: 1px solid <color>` shorthand and `border-width: 1px`. Other widths have
no token — leave them.
```css
/* prefer */ border: var(--vscode-strokeThickness) solid var(--vscode-widget-border);
/* avoid */ border: 1px solid var(--vscode-widget-border);
```
---
## Key Files
| Area | File |
|------|------|
| SplitView | `src/vs/base/browser/ui/splitview/splitview.ts` |
| Grid | `src/vs/base/browser/ui/grid/grid.ts` |
| Scrollbar | `src/vs/base/browser/ui/scrollbar/scrollableElement.ts` |
| Global workbench styles | `src/vs/workbench/browser/media/style.css` |