# Splitter

The line between two panes that the reader drags, or moves with the arrow keys, to give one pane more room and the other less.

Block: `smbk-splitter`. Status: experimental. Figma: the Figma file, https://www.figma.com/community/file/1688324308358870630. Desktop and mobile: differ by presentation only.

## Options

| Axis | Values | Default | Meaning |
|---|---|---|---|
| `orientation` | `vertical`, `horizontal` | `vertical` | Vertical is a line between two columns side by side, moved left and right. Horizontal is a line between two stacked panes, moved up and down. |

Parts: `smbk-splitter__line`, `smbk-splitter__grip`, `smbk-splitter__icon`

## Markup

```html
<div
  id="configure-pane"
  style="min-width:0;min-height:0;overflow:auto;padding:var(--smbk-space-inset-md);flex:0 0 var(--smbk-size-width-240)"
>
  <p style="margin:0;color:var(--smbk-color-text-default);font-family:var(--smbk-typography-label-md-font-family);font-size:var(--smbk-typography-label-md-font-size);line-height:var(--smbk-typography-label-md-line-height);font-weight:var(--smbk-typography-label-md-font-weight)">Files</p>
</div>
<div
  aria-label="Resize the files pane"
  aria-controls="configure-pane"
  role="separator"
  tabindex="0"
  aria-orientation="vertical"
  aria-valuenow="240"
  aria-valuemin="160"
  aria-valuemax="400"
  aria-valuetext="33%"
  class="smbk-splitter smbk-splitter--orientation-vertical"
>
  <span class="smbk-splitter__line"></span>
  <span class="smbk-splitter__grip" aria-hidden="true">
    <i
      class="ph ph-dots-six smbk-icon smbk-icon--size-md smbk-splitter__icon"
      aria-hidden="true"
    ></i>
  </span>
  <span class="smbk-splitter__line"></span>
</div>
<p style="margin:0;color:var(--smbk-color-text-default);font-family:var(--smbk-typography-label-md-font-family);font-size:var(--smbk-typography-label-md-font-size);line-height:var(--smbk-typography-label-md-line-height);font-weight:var(--smbk-typography-label-md-font-weight)">Preview</p>
```

## React

```tsx
import { Splitter } from './design-system/components/splitter/splitter.js';

<div id="configure-pane" style={{ minWidth: 0, minHeight: 0, overflow: "auto", padding: "var(--smbk-space-inset-md)", flex: "0 0 var(--smbk-size-width-240)" }}>
  <p style={{ margin: 0, color: "var(--smbk-color-text-default)", fontFamily: "var(--smbk-typography-label-md-font-family)", fontSize: "var(--smbk-typography-label-md-font-size)", lineHeight: "var(--smbk-typography-label-md-line-height)", fontWeight: "var(--smbk-typography-label-md-font-weight)" }}>Files</p>
</div>
<Splitter orientation="vertical" grip aria-label="Resize the files pane" aria-controls="configure-pane" min={160} max={400} defaultValue={240} />
<p style={{ margin: 0, color: "var(--smbk-color-text-default)", fontFamily: "var(--smbk-typography-label-md-font-family)", fontSize: "var(--smbk-typography-label-md-font-size)", lineHeight: "var(--smbk-typography-label-md-line-height)", fontWeight: "var(--smbk-typography-label-md-font-weight)" }}>Preview</p>
```

## Stylesheet

```css
/* smbk-splitter — Splitter. Figma "Splitter" 7117:70.
 * A handle between two panes: a hit area 24 across and as long as the panes, holding a line in two halves and,
 * between them, an optional grip, a raised pill with six dots. Dragging it, or the arrow keys, give one pane more
 * room; the wrapper keeps the value and the page sizes its panes from it. Under the pointer the hit area takes the
 * ghost fill and the line the strong border; while it is dragged the line turns brand and twice as thick, so the
 * change is not shown by color alone. Focus draws the system's ring around the hit area. Where the layout is narrow
 * the page stacks its panes and the splitter is not shown, which also takes it out of the Tab order. */

.smbk-splitter {
  box-sizing: border-box;
  position: relative;
  display: flex;
  flex: none;
  flex-direction: column;
  align-items: center;
  align-self: stretch;
  width: var(--smbk-size-control-xs);
  border-radius: var(--smbk-radius-control-xs);
  cursor: col-resize;
  touch-action: none;
  user-select: none;
  -webkit-user-select: none;
}

.smbk-splitter--orientation-horizontal {
  flex-direction: row;
  width: auto;
  height: var(--smbk-size-control-xs);
  cursor: row-resize;
}

/* The line: two halves, one either side of the grip, meeting where there is none. */
.smbk-splitter__line {
  flex: 1 1 0;
  width: var(--smbk-border-width-hairline);
  background: var(--smbk-color-border-groove-top);
}

.smbk-splitter--orientation-horizontal .smbk-splitter__line {
  width: auto;
  height: var(--smbk-border-width-hairline);
}

.smbk-splitter__grip {
  display: flex;
  flex: none;
  align-items: center;
  justify-content: center;
  width: var(--smbk-size-16);
  height: var(--smbk-size-24);
  border-radius: var(--smbk-radius-full);
  background: var(--smbk-color-surface-raised);
  box-shadow: var(--smbk-elevation-control);
}

.smbk-splitter--orientation-horizontal .smbk-splitter__grip {
  width: var(--smbk-size-24);
  height: var(--smbk-size-16);
}

.smbk-splitter__icon {
  font-size: var(--smbk-size-icon-sm);
  color: var(--smbk-color-icon-subtle);
}

/* ---- states: hover, focus, dragging, disabled ---- */

@media (hover: hover) {
  .smbk-splitter:not([aria-disabled='true']):hover {
    background: var(--smbk-color-action-ghost-background-hover);
  }

  .smbk-splitter:not([aria-disabled='true']):hover .smbk-splitter__line {
    background: var(--smbk-color-border-strong);
  }
}

.smbk-splitter:focus-visible {
  outline: var(--smbk-focus-ring-width) solid var(--smbk-color-border-focus);
  outline-offset: var(--smbk-focus-ring-offset);
}

.smbk-splitter[data-dragging]:not([aria-disabled='true']) {
  background: transparent;
}

.smbk-splitter[data-dragging]:not([aria-disabled='true']) .smbk-splitter__line {
  width: var(--smbk-border-width-emphasis);
  background: var(--smbk-color-border-brand);
}

.smbk-splitter--orientation-horizontal[data-dragging]:not([aria-disabled='true']) .smbk-splitter__line {
  width: auto;
  height: var(--smbk-border-width-emphasis);
}

.smbk-splitter[data-dragging]:not([aria-disabled='true']) .smbk-splitter__icon {
  color: var(--smbk-color-icon-brand);
}

.smbk-splitter[aria-disabled='true'] {
  cursor: default;
}

.smbk-splitter[aria-disabled='true'] .smbk-splitter__line {
  background: var(--smbk-color-border-control-disabled);
}

.smbk-splitter[aria-disabled='true'] .smbk-splitter__grip {
  background: transparent;
  box-shadow: none;
}

.smbk-splitter[aria-disabled='true'] .smbk-splitter__icon {
  color: var(--smbk-color-icon-disabled);
}

/* ---- touch: the hit area reaches the comfortable target without widening the gap between the panes ---- */

@media (pointer: coarse) {
  .smbk-splitter::before {
    content: '';
    position: absolute;
    inset-block: 0;
    inset-inline: calc((var(--smbk-size-control-xs) - var(--smbk-size-target-comfortable)) / 2);
  }

  .smbk-splitter--orientation-horizontal::before {
    inset-block: calc((var(--smbk-size-control-xs) - var(--smbk-size-target-comfortable)) / 2);
    inset-inline: 0;
  }
}

/* ---- narrow: the page stacks its panes, and the splitter is not shown ---- */

@container style(--smbk-layout: narrow) {
  .smbk-splitter {
    display: none;
  }
}

/* ---- forced colors: the line and the grip's edge in the text color ---- */

@media (forced-colors: active) {
  .smbk-splitter__line {
    forced-color-adjust: none;
    background: CanvasText;
  }

  .smbk-splitter__grip {
    border: var(--smbk-border-width-hairline) solid CanvasText;
  }

  .smbk-splitter[aria-disabled='true'] .smbk-splitter__line {
    background: GrayText;
  }

  .smbk-splitter:focus-visible {
    outline-color: CanvasText;
  }
}
```

## Tokens it reads

```json
{
  "size": {
    "16": {
      "$type": "dimension",
      "$value": "16px",
      "$description": "Raw size step, 16 px. Only for fixed sizes inside a component; semantic sizes (size/control, icon, mark) point here."
    },
    "24": {
      "$type": "dimension",
      "$value": "24px",
      "$description": "Raw size step, 24 px. Only for fixed sizes inside a component; semantic sizes (size/control, icon, mark) point here."
    },
    "control": {
      "xs": {
        "$type": "dimension",
        "$value": "{{size.24}}",
        "$description": "Height of the extra-small control: 24 in both densities, the smallest target allowed. Dashboard recipe: use compact density; 40 for the page’s one main action and for forms; 32 for toolbars and filters, the size used most; 28 for actions inside table rows and cards. On phones switch to comfortable density, or give small controls an invisible 44 px tap area (size/target/comfortable)."
      }
    },
    "icon": {
      "sm": {
        "$type": "dimension",
        "$value": "{{size.16}}",
        "$description": "Icon box, small."
      }
    },
    "target": {
      "comfortable": {
        "$type": "dimension",
        "$value": "{{size.44}}",
        "$description": "Comfortable touch target (44)."
      }
    }
  },
  "radius": {
    "control": {
      "xs": {
        "$type": "dimension",
        "$value": "{{radius.2}}",
        "$description": "Corners of controls, extra small."
      }
    },
    "full": {
      "$type": "dimension",
      "$value": "{{radius.9999}}",
      "$description": "Capsule: pills, chips, switches."
    }
  },
  "border-width": {
    "hairline": {
      "$type": "dimension",
      "$value": "{{border-width.1}}",
      "$description": "Thin lines: dividers and almost every edge."
    },
    "emphasis": {
      "$type": "dimension",
      "$value": "{{border-width.2}}",
      "$description": "Two-pixel edges for emphasis: a chosen card, a focus ring."
    }
  },
  "color": {
    "border": {
      "groove-top": {
        "$type": "color",
        "$value": "{{color.grey.300}}",
        "$description": "Upper line of an engraved divider or rim."
      },
      "strong": {
        "$type": "color",
        "$value": "{{color.grey.400}}",
        "$description": "Stronger edges and separators."
      },
      "focus": {
        "$type": "color",
        "$value": "{{color.red.600}}",
        "$description": "Keyboard focus ring on controls."
      },
      "brand": {
        "$type": "color",
        "$value": "{{color.red.600}}",
        "$description": "Brand-red edge; also the logo’s mark."
      },
      "control-disabled": {
        "$type": "color",
        "$value": "{{color.grey.300}}",
        "$description": "A disabled control’s edge."
      }
    },
    "surface": {
      "raised": {
        "$type": "color",
        "$value": "{{color.grey.50}}",
        "$description": "One floor above default: popovers, raised panels."
      }
    },
    "icon": {
      "subtle": {
        "$type": "color",
        "$value": "{{color.grey.700}}",
        "$description": "Quiet icons."
      },
      "brand": {
        "$type": "color",
        "$value": "{{color.red.600}}",
        "$description": "Brand-red icons, at the same contrast as brand text."
      },
      "disabled": {
        "$type": "color",
        "$value": "{{color.grey.400}}",
        "$description": "Icons in unavailable controls."
      }
    },
    "action": {
      "ghost": {
        "background-hover": {
          "$type": "color",
          "$value": "{{color.alpha.grey-800.15}}",
          "$description": "Ghost button: fill on hover."
        }
      }
    }
  },
  "elevation": {
    "control": {
      "$type": "shadow",
      "$value": {
        "light": "0px 0px 1px 0px #373d430d, 0px 1px 2px 0px #373d431a, 0px 2px 4px 0px #373d430d",
        "dark": "0px 0px 1px 0px #0000001a, 0px 1px 2px 0px #00000026, 0px 2px 4px 0px #0000001a"
      }
    }
  },
  "focus": {
    "ring": {
      "width": {
        "$type": "dimension",
        "$value": "{{border-width.2}}",
        "$description": "Thickness of the focus ring."
      },
      "offset": {
        "$type": "dimension",
        "$value": "{{space.2}}",
        "$description": "Gap between a control and its focus ring."
      }
    }
  }
}
```

## Purpose

The line between two panes that the reader moves to give one pane more room and the other less: a list and the item it opens, a preview and its settings, an editor and its output. The reader drags it, or focuses it and uses the arrow keys. It differs from the divider, which only separates two things and cannot be moved or focused.

## Anatomy

- The block: the hit area, 24 across and as long as the panes, a separator the reader can focus. Vertical is a line between two columns side by side; horizontal is a line between two stacked panes.
- `line`: the line, in two halves, one either side of the grip. Without the grip the two halves meet.
- `grip`: an optional raised pill in the middle of the line, holding `icon`, six dots. On unless turned off.

## When to use

- Two panes whose best share of the space depends on the reader and the task, such as a wide preview with narrow settings, or the reverse.
- A pane the reader often wants larger for a moment: a file list, a log, an inspector.

## When not to use

- Two parts of a page that always keep the same share. Use a fixed layout, with a [divider](divider.md) between them if they need a line.
- A choice between a few set sizes, such as compact and wide. Use a [segmented control](segmented-control.md) or a [button](button.md) that toggles the pane.
- A phone, or any narrow space. The panes stack there, and the splitter is not shown.

## Behavior

The page owns the panes. The splitter keeps a value, the size in pixels of the pane it measures, and reports every change; the page sizes the pane from it, and the other pane takes the rest. By default the value measures the pane before the line (the left one in a left-to-right page, the upper one of two stacked panes); it can measure the pane after it instead. The splitter stops at the pane's smallest and largest sizes, whichever way it is moved. It does not remember its value: a page that wants the size kept stores it and passes it back.

Dragging with a mouse, a pen or a finger moves the line with the pointer. While it is dragged the line turns brand and twice as thick, and the grip's dots turn brand. Under the pointer the hit area takes a light fill and the line darkens. On a touch screen the hit area reaches 44 across without widening the gap between the panes.

Where the layout is narrow the splitter is not shown, which also takes it out of the Tab order; the page stacks its two panes there itself, under the same layout switch. A disabled splitter keeps its line, in the disabled color, with the grip's dots and no pill, and cannot be focused or moved.

## Accessibility
### What it does for you

- It is a separator the reader can focus, with its orientation, its value, its smallest and largest values, and the value in words. A screen reader hears "separator", its name and how far the pane is between its smallest and largest size, as a percentage, unless the page gives other words.
- A vertical line moves with Left and Right, which follow the line on the screen in a right-to-left page too; a horizontal line moves with Up and Down. Each press moves it 16 pixels unless the page sets another step. Home gives the pane its smallest size, End its largest.
- Dragging is not the only way to move it: the keys do it too.
- The focus ring is drawn around the whole hit area.
- The hit area is at least 24 across, so a pointer does not need precision to grab it.

### What you must do

- Name it for the pane it sizes, such as "Resize settings panel", and point `aria-controls` at that pane's id.
- A reader who uses a pointer and cannot drag needs another way, so a page where the size matters offers set sizes beside the panes, such as a button that widens a pane.
- In hand-written markup, give it the separator role, a Tab stop, `aria-orientation`, `aria-valuenow`, `aria-valuemin`, `aria-valuemax` and a name, and move it with script; without script it is a line that does not move.

### How to test

- Tab to it: the focus ring shows around the whole hit area, in both themes and in a forced-color mode.
- Press the arrow keys that follow the line, then Home and End: the pane grows, shrinks and stops at its smallest and largest sizes.
- With a screen reader, it is read as a separator by its name, with its value as a percentage.
- On a touch screen, the line is grabbed by a hit area 44 across.
- Check that the page offers set sizes, such as a button that widens a pane, for a reader who cannot drag.

### Keyboard

| Key | What it does |
|---|---|
| Tab | Moves focus to the splitter. It is not in the Tab order where the layout is narrow and the splitter is hidden, or when it is disabled. |
| Left Arrow or Right Arrow | On a vertical line, moves it by one step, following the line on the screen in a right-to-left page too. |
| Up Arrow or Down Arrow | On a horizontal line, moves it by one step. |
| Home | Gives the pane its smallest size. |
| End | Gives the pane its largest size. |

## Usage rules

### Do

- Give each pane a smallest size at which its content still works, and a largest that leaves the other pane usable.
- Name the splitter for the pane it sizes.
- Store the size where the reader expects it kept, and pass it back the next time.

### Don't

- Use a divider where the reader should be able to move the line; it cannot be focused or dragged.
- Hide the grip on a splitter that is hard to notice, such as one between two panes of the same color.
- Leave a splitter in a narrow layout; stack the panes instead.
