Toolbar

A grouping container for related controls with roving focus and arrow-key navigation.

View source
import type { JSX, ValidComponent } from "solid-js"
import * as Select from "@danielfrg/solid-ui/select"
import * as ToggleGroup from "@danielfrg/solid-ui/toggle-group"
import * as Toolbar from "@danielfrg/solid-ui/toolbar"
import styles from "./index.module.css"

const FONTS = ["Helvetica", "Arial"]

export function DemoToolbarHero() {
  return (
    <Toolbar.Root class={styles.toolbar}>
      <ToggleGroup.Root class={styles.group} aria-label="Alignment">
        <Toolbar.Button
          as={ToggleGroup.Item as ValidComponent}
          value="align-left"
          aria-label="Align left"
          class={styles.button}
        >
          Align Left
        </Toolbar.Button>
        <Toolbar.Button
          as={ToggleGroup.Item as ValidComponent}
          value="align-right"
          aria-label="Align right"
          class={styles.button}
        >
          Align Right
        </Toolbar.Button>
      </ToggleGroup.Root>

      <Toolbar.Separator class={styles.separator} />

      <Toolbar.Group class={styles.group} aria-label="Numerical format">
        <Toolbar.Button class={styles.button} aria-label="Format as currency">
          $
        </Toolbar.Button>
        <Toolbar.Button class={styles.button} aria-label="Format as percent">
          %
        </Toolbar.Button>
      </Toolbar.Group>

      <Toolbar.Separator class={styles.separator} />

      <Select.Root
        defaultValue="Helvetica"
        options={FONTS}
        itemComponent={(props) => (
          <Select.Item item={props.item} class={styles.item}>
            <Select.ItemIndicator class={styles.itemIndicator}>
              <CheckIcon class={styles.itemIndicatorIcon} />
            </Select.ItemIndicator>
            <Select.ItemLabel class={styles.itemText}>{props.item.rawValue}</Select.ItemLabel>
          </Select.Item>
        )}
      >
        <Toolbar.Button
          as={Select.Trigger as ValidComponent}
          class={`${styles.button} ${styles.select}`}
          aria-label="Font"
        >
          <Select.Value<string> class={styles.value}>{(state) => state.selectedOption()}</Select.Value>
          <Select.Icon class={styles.selectIcon}>
            <ChevronUpDownIcon />
          </Select.Icon>
        </Toolbar.Button>
        <Select.Portal>
          <Select.Content class={styles.popup}>
            <Select.Listbox class={styles.listbox} />
          </Select.Content>
        </Select.Portal>
      </Select.Root>

      <Toolbar.Separator class={styles.separator} />

      <Toolbar.Link class={styles.link} href="#">
        Edited 51m ago
      </Toolbar.Link>
    </Toolbar.Root>
  )
}

function ChevronUpDownIcon(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
  return (
    <svg width="8" height="12" viewBox="0 0 8 12" fill="none" stroke="currentcolor" stroke-width="1.5" {...props}>
      <path d="M0.5 4.5L4 1.5L7.5 4.5" />
      <path d="M0.5 7.5L4 10.5L7.5 7.5" />
    </svg>
  )
}

function CheckIcon(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
  return (
    <svg fill="currentcolor" width="10" height="10" viewBox="0 0 10 10" {...props}>
      <path d="M9.1603 1.12218C9.50684 1.34873 9.60427 1.81354 9.37792 2.16038L5.13603 8.66012C5.01614 8.8438 4.82192 8.96576 4.60451 8.99384C4.3871 9.02194 4.1683 8.95335 4.00574 8.80615L1.24664 6.30769C0.939709 6.02975 0.916013 5.55541 1.19372 5.24822C1.47142 4.94102 1.94536 4.91731 2.2523 5.19524L4.36085 7.10461L8.12299 1.33999C8.34934 0.993152 8.81376 0.895638 9.1603 1.12218Z" />
    </svg>
  )
}
.toolbar {
  box-sizing: border-box;
  display: flex;
  align-items: center;
  gap: 1px;
  border: 1px solid var(--color-gray-200);
  background-color: var(--color-gray-50);
  border-radius: 0.375rem;
  padding: 0.125rem;
  width: 37.5rem;
}

.group {
  display: flex;
  gap: 0.25rem;
}

.button {
  box-sizing: border-box;
  display: flex;
  align-items: center;
  justify-content: center;
  min-width: 2rem;
  height: 2rem;
  padding: 0;
  margin: 0;
  outline: 0;
  border: 0;
  border-radius: 0.25rem;
  background-color: transparent;
  color: var(--color-gray-600);
  user-select: none;
  font-family: inherit;
  font-size: 0.875rem;
  font-weight: 500;
}

.button:focus-visible {
  background-color: transparent;
  outline: 2px solid var(--color-blue);
  outline-offset: -1px;
}

@media (hover: hover) {
  .button:hover {
    background-color: var(--color-gray-100);
  }
}

.button:active {
  background-color: var(--color-gray-200);
}

.button[data-pressed] {
  background-color: var(--color-gray-100);
  color: var(--color-gray-900);
}

.button[aria-pressed] {
  padding: 0 0.75rem;
}

.button[role="combobox"],
.select {
  min-width: 8rem;
  justify-content: space-between;
  padding: 0 0.75rem;
}

.separator {
  width: 1px;
  height: 16px;
  margin: 0.25rem;
  background-color: var(--color-gray-300);
}

.link {
  color: var(--color-gray-500);
  font-family: inherit;
  font-size: 0.875rem;
  text-decoration: none;
  align-self: center;
  flex: 0 0 auto;
  margin-inline: auto 0.875rem;
}

.link:focus-visible {
  outline: 2px solid var(--color-blue);
  outline-offset: -2px;
  border-radius: var(--radius-sm);
}

@media (hover: hover) {
  .link:hover {
    color: var(--color-blue);
  }
}

.value {
  display: inline-flex;
  align-items: center;
}

.selectIcon {
  display: flex;
}

.popup {
  box-sizing: border-box;
  padding-block: 0.25rem;
  border-radius: 0.375rem;
  background-color: canvas;
  color: var(--color-gray-900);
  transform-origin: var(--transform-origin);
  transition:
    transform 150ms,
    opacity 150ms;
  overflow-y: auto;
  max-height: var(--available-height);
}

.popup[data-starting-style],
.popup[data-ending-style] {
  opacity: 0;
  transform: scale(0.9);
}

.popup[data-side="none"] {
  transition: none;
  transform: none;
  opacity: 1;
}

@media (prefers-color-scheme: light) {
  .popup {
    outline: 1px solid var(--color-gray-200);
    box-shadow:
      0 10px 15px -3px var(--color-gray-200),
      0 4px 6px -4px var(--color-gray-200);
  }
}

@media (prefers-color-scheme: dark) {
  .popup {
    outline: 1px solid var(--color-gray-300);
    outline-offset: -1px;
  }
}

.listbox {
  display: flex;
  flex-direction: column;
}

.item {
  box-sizing: border-box;
  outline: 0;
  line-height: 1rem;
  padding-block: 0.5rem;
  padding-left: 0.625rem;
  padding-right: 1rem;
  min-width: var(--anchor-width);
  display: grid;
  gap: 0.5rem;
  align-items: center;
  grid-template-columns: 0.75rem 1fr;
  cursor: default;
  user-select: none;
  scroll-margin-block: 1rem;
  font-size: 0.875rem;
}

@media (pointer: coarse) {
  .item {
    padding-block: 0.625rem;
  }
}

.item[data-highlighted] {
  z-index: 0;
  position: relative;
  color: var(--color-gray-50);
}

.item[data-highlighted]::before {
  content: "";
  z-index: -1;
  position: absolute;
  inset-block: 0;
  inset-inline: 0.25rem;
  border-radius: 0.25rem;
  background-color: var(--color-gray-900);
}

.itemIndicator {
  grid-column-start: 1;
}

.itemIndicatorIcon {
  display: block;
  width: 0.75rem;
  height: 0.75rem;
}

.itemText {
  grid-column-start: 2;
}
:root {
  --color-blue: oklch(45% 50% 264deg);
  --color-red: oklch(50% 55% 31deg);
  --color-gray-50: oklch(98% 0.25% 264deg);
  --color-gray-100: oklch(12% 9.5% 264deg / 5%);
  --color-gray-200: oklch(12% 9% 264deg / 7%);
  --color-gray-300: oklch(12% 8.5% 264deg / 17%);
  --color-gray-400: oklch(12% 8% 264deg / 38%);
  --color-gray-500: oklch(12% 7.5% 264deg / 50%);
  --color-gray-600: oklch(12% 7% 264deg / 67%);
  --color-gray-700: oklch(12% 6% 264deg / 77%);
  --color-gray-800: oklch(12% 5% 264deg / 85%);
  --color-gray-900: oklch(12% 5% 264deg / 90%);
  --color-gray-950: oklch(12% 5% 264deg / 95%);
}

@media (prefers-color-scheme: dark) {
  :root {
    --color-blue: oklch(69% 50% 264deg);
    --color-red: oklch(80% 55% 31deg);
    --color-gray-50: oklch(17% 0.25% 264deg);
    --color-gray-100: oklch(28% 0.75% 264deg / 65%);
    --color-gray-200: oklch(29% 0.75% 264deg / 80%);
    --color-gray-300: oklch(35% 0.75% 264deg / 80%);
    --color-gray-400: oklch(47% 0.875% 264deg / 80%);
    --color-gray-500: oklch(64% 1% 264deg / 80%);
    --color-gray-600: oklch(82% 1% 264deg / 80%);
    --color-gray-700: oklch(92% 1.125% 264deg / 80%);
    --color-gray-800: oklch(93% 0.875% 264deg / 85%);
    --color-gray-900: oklch(95% 0.5% 264deg / 90%);
    --color-gray-950: oklch(94% 0.375% 264deg / 95%);
  }
}

Usage

Use Toolbar.Group to cluster related items, Toolbar.Separator to divide sections, and Toolbar.Input or Toolbar.Link for non-button controls. Arrow keys move focus across all items.