ControlGroup

One flush surface that groups buttons, selects, and inputs into a strip with hairline dividers — the navbar preferences cluster as a component.

Usage

svelte
<script lang="ts">
  import { ListFilter } from "@lucide/svelte";
  import { css } from "@milandekruijf/pandacss/css";
  import {
    Button,
    ControlGroup,
    Input,
    InputGroup,
    Select,
  } from "@milandekruijf/ui/svelte";

  let query = $state("");
  let sort = $state("newest");
  const sorts = [
    { value: "newest", label: "Newest first" },
    { value: "oldest", label: "Oldest first" },
  ];
  const fieldClass = css({ flex: "1 1 0", minW: 0 });
  const iconClass = css({ w: 4, h: 4 });
</script>

<!-- Group flush buttons, selects, and inputs on one shared surface. -->
<ControlGroup ariaLabel="Document actions">
  <Button color="flush">Copy</Button>
  <Button color="flush">Export</Button>
</ControlGroup>

<ControlGroup full ariaLabel="Filter and sort">
  <InputGroup class={fieldClass}>
    {#snippet leading()}
      <ListFilter class={iconClass} aria-hidden="true" />
    {/snippet}
    <Input bind:value={query} placeholder="Filter…" />
  </InputGroup>
  <Select
    bind:value={sort}
    options={sorts}
    triggerVariant="flush"
    showChevron={false}
    ariaLabel="Sort by"
  />
</ControlGroup>

Examples

Grouped controls
<script lang="ts">
  import { ListFilter } from "@lucide/svelte";
  import { css } from "@milandekruijf/pandacss/css";
  import {
    Button,
    ControlGroup,
    Input,
    InputGroup,
    Select,
  } from "@milandekruijf/ui/svelte";

  let query = $state("");
  let sort = $state("newest");

  const sorts = [
    { value: "newest", label: "Newest first" },
    { value: "oldest", label: "Oldest first" },
  ];

  const stackClass = css({
    display: "flex",
    flexDirection: "column",
    gap: 4,
    minW: 0,
  });
  const fieldClass = css({ flex: "1 1 0", minW: 0 });
  const iconClass = css({ w: 4, h: 4 });
</script>

<div class={stackClass}>
  <ControlGroup ariaLabel="Document actions">
    <Button color="flush">Copy</Button>
    <Button color="flush">Export</Button>
  </ControlGroup>

  <ControlGroup full ariaLabel="Filter and sort">
    <InputGroup class={fieldClass}>
      {#snippet leading()}
        <ListFilter class={iconClass} aria-hidden="true" />
      {/snippet}
      <Input bind:value={query} placeholder="Filter…" />
    </InputGroup>
    <Select
      bind:value={sort}
      options={sorts}
      triggerVariant="flush"
      showChevron={false}
      ariaLabel="Sort by"
      class={css({ flexShrink: 0 })}
    />
  </ControlGroup>
</div>

Props

PropTypeDefaultDescription
full?booleanfalse
Stretches to the available width (e.g. a sidebar) instead of hugging its controls.
dividers?booleantrue
Draws a hairline between adjacent controls.
ariaLabel?string—
Accessible name for the group; also turns on `role="group"`.
rootRef?HTMLDivElement | null—
Bindable handle to the root element (`bind:rootRef`), e.g. to size an open menu.
class?string—
Extra classes merged after the recipe classes.
childrenSnippet—
The controls to group — buttons, selects, inputs, or a mix. Size flexible children yourself (e.g. `flex: 1`).

The group owns the `slate.3` surface; children stay flush (`Button color="flush"`, `Select triggerVariant="flush"`, an `Input` on the shared surface). Dividers are drawn on each control's start edge, so a full-width group never paints past the last control.