Tree Select

PRO

Select items from a hierarchy, such as an organization or folder structure.

Live preview
Interact with the component to see results here.
View code
HTML
<sy-tree-select
  placeholder="Choose a team"
  maxtagcount="2"
  expandable
  expandall
  line
  clearable
  name="team"
  id="sample"
  aria-label="Choose a team"
></sy-tree-select>

<script type="module">
  const component1 = document.querySelector("#sample");
  Object.assign(component1, {
    "nodes": [{
        "label": "Product",
        "value": "product",
        "children": [{
            "label": "Design",
            "value": "design"
          },
          {
            "label": "Engineering",
            "value": "engineering"
          }
        ]
      },
      {
        "label": "Business",
        "value": "business",
        "children": [{
            "label": "Sales",
            "value": "sales"
          },
          {
            "label": "Support",
            "value": "support"
          }
        ]
      }
    ]
  });
</script>

Usage guide

#

Use to select from hierarchical candidates, such as organizations or categories.

  1. Write the error message in the error slot.

    Inside the component, add <span slot="error">Error message</span>. You can include links and other guidance as well as text. For inputs supporting noNativeValidity, enable it to show this guidance instead of a browser popup. Switch always displays errors below the component.

  2. Report business rule failures with setCustomError().

    For example, if the server rejects a duplicate value, update the error slot and call setCustomError(). This method does not take a message argument. A custom error also blocks form submission.

  3. Clear the error after resolving it and check whether submission is allowed.

    Use clearCustomError() to clear a business rule error. Required-field and format validation remain active. await checkValidity() returns true or false; reportValidity() validates and displays errors. Read valid named values through FormData on submission.

  1. Prepare hierarchical data

    Pass a tree array with label, value, and children to nodes.

  2. Selection and expansion

    Use checkable for multiple selection and expandable for branch expansion. expandAll opens the whole tree initially.

  3. Manage selection results

    Use clearable to offer clearing and maxTagCount to control the number of visible selection tags.

Examples

#

Select from a hierarchy

Expand a parent and select a team.

View code
HTML
<sy-tree-select
  placeholder="Choose a team"
  expandable
  expandall
  line
  id="sample"
></sy-tree-select>

<script type="module">
  const component1 = document.querySelector("#sample");
  Object.assign(component1, {
    "nodes": [{
        "label": "Product",
        "value": "product",
        "children": [{
            "label": "Design",
            "value": "design"
          },
          {
            "label": "Engineering",
            "value": "engineering"
          }
        ]
      },
      {
        "label": "Business",
        "value": "business",
        "children": [{
            "label": "Sales",
            "value": "sales"
          },
          {
            "label": "Support",
            "value": "support"
          }
        ]
      }
    ]
  });
</script>

Multiple selection and tags

Use checkable to select multiple items.

View code
HTML
<sy-tree-select
  checkable
  expandall
  maxtagcount="1"
  defaultvalue="design,engineering,sales"
  clearable
  id="sample"
></sy-tree-select>

<script type="module">
  const component1 = document.querySelector("#sample");
  Object.assign(component1, {
    "nodes": [{
        "label": "Product",
        "value": "product",
        "children": [{
            "label": "Design",
            "value": "design"
          },
          {
            "label": "Engineering",
            "value": "engineering"
          }
        ]
      },
      {
        "label": "Business",
        "value": "business",
        "children": [{
            "label": "Sales",
            "value": "sales"
          },
          {
            "label": "Support",
            "value": "support"
          }
        ]
      }
    ]
  });
</script>

Keep the selected state

Show the current selection even in read-only or disabled states.

View code
HTML
<div style="display:grid;gap:16px;width:100%">
  <sy-tree-select
    defaultvalue="design"
    readonly
    id="readonly"
  ></sy-tree-select>
  <sy-tree-select
    defaultvalue="engineering"
    disabled
    id="disabled"
  ></sy-tree-select>
</div>

<script type="module">
  const component1 = document.querySelector("#readonly");
  Object.assign(component1, {
    "nodes": [{
        "label": "Product",
        "value": "product",
        "children": [{
            "label": "Design",
            "value": "design"
          },
          {
            "label": "Engineering",
            "value": "engineering"
          }
        ]
      },
      {
        "label": "Business",
        "value": "business",
        "children": [{
            "label": "Sales",
            "value": "sales"
          },
          {
            "label": "Support",
            "value": "support"
          }
        ]
      }
    ]
  });
  const component2 = document.querySelector("#disabled");
  Object.assign(component2, {
    "nodes": [{
        "label": "Product",
        "value": "product",
        "children": [{
            "label": "Design",
            "value": "design"
          },
          {
            "label": "Engineering",
            "value": "engineering"
          }
        ]
      },
      {
        "label": "Business",
        "value": "business",
        "children": [{
            "label": "Sales",
            "value": "sales"
          },
          {
            "label": "Support",
            "value": "support"
          }
        ]
      }
    ]
  });
</script>

Events and payloads

Interact with the component and inspect the latest event name and detail. The code includes subscriptions to events emitted by this component and its children.

View code
HTML
<sy-tree-select
  placeholder="Choose a team"
  maxtagcount="2"
  expandable
  expandall
  line
  clearable
  name="team"
  id="sample"
  aria-label="Choose a team"
></sy-tree-select>
<output id="demo-events" aria-live="polite">Interact with the component to see
  its latest event and payload.</output>

<script type="module">
  const describeResult = value => JSON.stringify(value, (_key, item) => {
    if (item instanceof File) return {
      name: item.name,
      size: item.size,
      type: item.type
    };
    if (item instanceof Element) return `<${item.localName}>`;
    return item;
  }) ?? 'Done';
  const component1 = document.querySelector("#sample");
  Object.assign(component1, {
    "nodes": [{
        "label": "Product",
        "value": "product",
        "children": [{
            "label": "Design",
            "value": "design"
          },
          {
            "label": "Engineering",
            "value": "engineering"
          }
        ]
      },
      {
        "label": "Business",
        "value": "business",
        "children": [{
            "label": "Sales",
            "value": "sales"
          },
          {
            "label": "Support",
            "value": "support"
          }
        ]
      }
    ]
  });
  for (const name of ["changed"]) {
    document.addEventListener(name, event => {
      document.querySelector('#demo-events').textContent =
        `${event.target.localName} · ${name}: ${describeResult(event.detail)}`;
    });
  }
</script>

Loading hierarchical data

Use loading while fetching items. After receiving the response, set loading to false and update nodes.

View code
HTML
<sy-tree-select
  placeholder="Choose a team"
  maxtagcount="2"
  expandable
  expandall
  line
  clearable
  loading
  name="team"
  id="sample"
  aria-label="Choose a team"
></sy-tree-select>

<script type="module">
  const component1 = document.querySelector("#sample");
  Object.assign(component1, {
    "nodes": [{
        "label": "Product",
        "value": "product",
        "children": [{
            "label": "Design",
            "value": "design"
          },
          {
            "label": "Engineering",
            "value": "engineering"
          }
        ]
      },
      {
        "label": "Business",
        "value": "business",
        "children": [{
            "label": "Sales",
            "value": "sales"
          },
          {
            "label": "Support",
            "value": "support"
          }
        ]
      }
    ]
  });
  const component2 = document.querySelector("#sample");
  Object.assign(component2, {
    "loading": true
  });
</script>

Popup width and placement

Use nodeWidth to set item width and appendParent to place the popup within the parent. Open the selector to inspect long items and panel placement.

View code
HTML
<sy-tree-select
  placeholder="Choose a team"
  maxtagcount="2"
  expandable
  expandall
  line
  clearable
  nodewidth="280"
  appendparent
  name="team"
  id="sample"
  aria-label="Choose a team"
></sy-tree-select>

<script type="module">
  const component1 = document.querySelector("#sample");
  Object.assign(component1, {
    "nodes": [{
        "label": "Product",
        "value": "product",
        "children": [{
            "label": "Design",
            "value": "design"
          },
          {
            "label": "Engineering",
            "value": "engineering"
          }
        ]
      },
      {
        "label": "Business",
        "value": "business",
        "children": [{
            "label": "Sales",
            "value": "sales"
          },
          {
            "label": "Support",
            "value": "support"
          }
        ]
      }
    ]
  });
  const component2 = document.querySelector("#sample");
  Object.assign(component2, {
    "nodeWidth": 280,
    "appendParent": true
  });
</script>

Forms and validation

Submit an empty value to trigger required-field validation. Enter a value and submit to inspect the form data, then try enabling and clearing a custom error.

View code
HTML
<form style="display:grid;gap:16px">
  <sy-tree-select
    name="field"
    required
    placeholder="Choose a team"
    expandall
    id="sample"
  ><span slot="error">Choose a team.</span></sy-tree-select>
  <div style="display:flex;flex-wrap:wrap;gap:8px">
    <sy-button type="submit" variant="primary">Submit</sy-button>
    <sy-button type="reset">Reset</sy-button>
  </div>
</form>
<div class="demo-actions"
  style="display:flex;flex-wrap:wrap;gap:8px;margin-top:16px"
>
  <sy-button id="action-0" size="small">Check validity</sy-button>
  <sy-button id="action-1" size="small">Show error</sy-button>
  <sy-button id="action-2" size="small">Clear error</sy-button>
</div>
<output id="demo-result" aria-live="polite"></output>

<script type="module">
  const describeResult = value => JSON.stringify(value, (_key, item) => {
    if (item instanceof File) return {
      name: item.name,
      size: item.size,
      type: item.type
    };
    if (item instanceof Element) return `<${item.localName}>`;
    return item;
  }) ?? 'Done';
  const component1 = document.querySelector("#sample");
  Object.assign(component1, {
    "nodes": [{
        "label": "Product",
        "value": "product",
        "children": [{
            "label": "Design",
            "value": "design"
          },
          {
            "label": "Engineering",
            "value": "engineering"
          }
        ]
      },
      {
        "label": "Business",
        "value": "business",
        "children": [{
            "label": "Sales",
            "value": "sales"
          },
          {
            "label": "Support",
            "value": "support"
          }
        ]
      }
    ]
  });
  const actionTarget0 = document.querySelector("#sample");
  document.querySelector('#action-0').addEventListener('click', async () => {
    const result = await actionTarget0.checkValidity();
    document.querySelector('#demo-result').textContent = describeResult(result);
  });
  const actionTarget1 = document.querySelector("#sample");
  document.querySelector('#action-1').addEventListener('click', async () => {
    const result = await actionTarget1.setCustomError();
    document.querySelector('#demo-result').textContent = describeResult(result);
  });
  const actionTarget2 = document.querySelector("#sample");
  document.querySelector('#action-2').addEventListener('click', async () => {
    const result = await actionTarget2.clearCustomError();
    document.querySelector('#demo-result').textContent = describeResult(result);
  });
  document.querySelector('form').addEventListener('submit', event => {
    event.preventDefault();
    document.querySelector('#demo-result').textContent = describeResult([...new FormData(event
      .currentTarget)]);
  });
</script>

Read validation results and error states

Validate before submitting or display errors on screen. Check an empty required field, then fill it to see how the return value changes. Querying the state distinguishes custom errors from required-field errors.

View code
HTML
<form style="display:grid;gap:16px">
  <sy-tree-select
    name="field"
    required
    placeholder="Choose a team"
    expandall
    id="sample"
  ><span slot="error">Choose a team.</span></sy-tree-select>
  <div style="display:flex;flex-wrap:wrap;gap:8px">
    <sy-button type="submit" variant="primary">Submit</sy-button>
    <sy-button type="reset">Reset</sy-button>
  </div>
</form>
<div class="demo-actions"
  style="display:flex;flex-wrap:wrap;gap:8px;margin-top:16px"
>
  <sy-button id="action-0" size="small">Check validity</sy-button>
  <sy-button id="action-1" size="small">Show error</sy-button>
  <sy-button id="action-2" size="small">Clear error</sy-button>
  <sy-button id="action-3" size="small">Validate with errors</sy-button>
  <sy-button id="action-4" size="small">Query validation state</sy-button>
</div>
<output id="demo-result" aria-live="polite"></output>

<script type="module">
  const describeResult = value => JSON.stringify(value, (_key, item) => {
    if (item instanceof File) return {
      name: item.name,
      size: item.size,
      type: item.type
    };
    if (item instanceof Element) return `<${item.localName}>`;
    return item;
  }) ?? 'Done';
  const component1 = document.querySelector("#sample");
  Object.assign(component1, {
    "nodes": [{
        "label": "Product",
        "value": "product",
        "children": [{
            "label": "Design",
            "value": "design"
          },
          {
            "label": "Engineering",
            "value": "engineering"
          }
        ]
      },
      {
        "label": "Business",
        "value": "business",
        "children": [{
            "label": "Sales",
            "value": "sales"
          },
          {
            "label": "Support",
            "value": "support"
          }
        ]
      }
    ]
  });
  const actionTarget0 = document.querySelector("#sample");
  document.querySelector('#action-0').addEventListener('click', async () => {
    const result = await actionTarget0.checkValidity();
    document.querySelector('#demo-result').textContent = describeResult(result);
  });
  const actionTarget1 = document.querySelector("#sample");
  document.querySelector('#action-1').addEventListener('click', async () => {
    const result = await actionTarget1.setCustomError();
    document.querySelector('#demo-result').textContent = describeResult(result);
  });
  const actionTarget2 = document.querySelector("#sample");
  document.querySelector('#action-2').addEventListener('click', async () => {
    const result = await actionTarget2.clearCustomError();
    document.querySelector('#demo-result').textContent = describeResult(result);
  });
  const actionTarget3 = document.querySelector("#sample");
  document.querySelector('#action-3').addEventListener('click', async () => {
    const result = await actionTarget3.reportValidity();
    document.querySelector('#demo-result').textContent = describeResult(result);
  });
  const actionTarget4 = document.querySelector("#sample");
  document.querySelector('#action-4').addEventListener('click', async () => {
    const result = await actionTarget4.getStatus();
    document.querySelector('#demo-result').textContent = describeResult(result);
  });
  document.querySelector('form').addEventListener('submit', event => {
    event.preventDefault();
    document.querySelector('#demo-result').textContent = describeResult([...new FormData(event
      .currentTarget)]);
  });
</script>

Set validation with a message

setCustomValidity(message) sets an error when the message is not empty. Pass an empty string to clear that error. Required-field validation still applies.

View code
HTML
<form style="display:grid;gap:16px">
  <sy-tree-select
    name="field"
    required
    placeholder="Choose a team"
    expandall
    id="sample"
  ><span slot="error">Choose a team.</span></sy-tree-select>
  <div style="display:flex;flex-wrap:wrap;gap:8px">
    <sy-button type="submit" variant="primary">Submit</sy-button>
    <sy-button type="reset">Reset</sy-button>
  </div>
</form>
<div class="demo-actions"
  style="display:flex;flex-wrap:wrap;gap:8px;margin-top:16px"
>
  <sy-button id="action-0" size="small">Business rule error</sy-button>
  <sy-button id="action-1" size="small">Clear business rule error</sy-button>
  <sy-button id="action-2" size="small">Query state</sy-button>
</div>
<output id="demo-result" aria-live="polite"></output>

<script type="module">
  const describeResult = value => JSON.stringify(value, (_key, item) => {
    if (item instanceof File) return {
      name: item.name,
      size: item.size,
      type: item.type
    };
    if (item instanceof Element) return `<${item.localName}>`;
    return item;
  }) ?? 'Done';
  const component1 = document.querySelector("#sample");
  Object.assign(component1, {
    "nodes": [{
        "label": "Product",
        "value": "product",
        "children": [{
            "label": "Design",
            "value": "design"
          },
          {
            "label": "Engineering",
            "value": "engineering"
          }
        ]
      },
      {
        "label": "Business",
        "value": "business",
        "children": [{
            "label": "Sales",
            "value": "sales"
          },
          {
            "label": "Support",
            "value": "support"
          }
        ]
      }
    ]
  });
  const actionTarget0 = document.querySelector("#sample");
  document.querySelector('#action-0').addEventListener('click', async () => {
    const result = await actionTarget0.setCustomValidity(
      "This department is currently unavailable.");
    document.querySelector('#demo-result').textContent = describeResult(result);
  });
  const actionTarget1 = document.querySelector("#sample");
  document.querySelector('#action-1').addEventListener('click', async () => {
    const result = await actionTarget1.setCustomValidity("");
    document.querySelector('#demo-result').textContent = describeResult(result);
  });
  const actionTarget2 = document.querySelector("#sample");
  document.querySelector('#action-2').addEventListener('click', async () => {
    const result = await actionTarget2.getStatus();
    document.querySelector('#demo-result').textContent = describeResult(result);
  });
  document.querySelector('form').addEventListener('submit', event => {
    event.preventDefault();
    document.querySelector('#demo-result').textContent = describeResult([...new FormData(event
      .currentTarget)]);
  });
</script>

Custom error messages

Display a custom error for server-side duplicate checks or business rules. Change the message below and submit to verify that it is blocked. After clearing the error, required-field constraints must still be satisfied.

View code
HTML
<label for="error-message">Error message to display</label><input
  id="error-message"
  value="The server did not accept this value. Please review it."
  type="text"
>
<form style="display:grid;gap:16px">
  <sy-tree-select
    nonativevalidity
    name="field"
    required
    placeholder="Choose a team"
    expandall
    id="sample"
  ><span slot="error">Choose a team.</span></sy-tree-select>
  <div style="display:flex;flex-wrap:wrap;gap:8px">
    <sy-button type="submit" variant="primary">Submit</sy-button>
    <sy-button type="reset">Reset</sy-button>
  </div>
</form>
<div class="demo-actions">
  <sy-button id="show-custom-error" variant="primary">Show error</sy-button>
  <sy-button id="clear-custom-error">Clear error</sy-button>
</div>
<output id="custom-error-result" aria-live="polite"></output>
<output id="demo-result" aria-live="polite"></output>

<script type="module">
  const describeResult = value => JSON.stringify(value, (_key, item) => {
    if (item instanceof File) return {
      name: item.name,
      size: item.size,
      type: item.type
    };
    if (item instanceof Element) return `<${item.localName}>`;
    return item;
  }) ?? 'Done';
  const component1 = document.querySelector("#sample");
  Object.assign(component1, {
    "nodes": [{
        "label": "Product",
        "value": "product",
        "children": [{
            "label": "Design",
            "value": "design"
          },
          {
            "label": "Engineering",
            "value": "engineering"
          }
        ]
      },
      {
        "label": "Business",
        "value": "business",
        "children": [{
            "label": "Sales",
            "value": "sales"
          },
          {
            "label": "Support",
            "value": "support"
          }
        ]
      }
    ]
  });
  await (async function customErrorSetup(root) {
    const field = root.querySelector('#sample');
    const message = root.querySelector('[slot="error"]');
    const input = root.querySelector('#error-message');
    const output = root.querySelector('#custom-error-result');
    const update = async () => {
      message.textContent = input.value.trim() || _msg("m_4eea8cef81c6");
      await field.setCustomError();
      output.textContent = _msg("m_e00bd46236d3", [message.textContent]);
    };
    input.addEventListener('input', update);
    root.querySelector('#show-custom-error').addEventListener('click', update);
    root.querySelector('#clear-custom-error').addEventListener('click', async () => {
      await field.clearCustomError();
      output.textContent = await field.checkValidity() ? _msg("m_c454251167b2") : _msg(
        "m_5b8cb48627e5");
    });
    root.querySelector('form').addEventListener('invalid', () => {
      output.textContent = _msg("m_c702c7778166");
    }, true);
    root.querySelector('form').addEventListener('reset', () => {
      output.textContent = _msg("m_b90c2b4dc30f");
    });
    await update();
  })(document);
  document.querySelector('form').addEventListener('submit', event => {
    event.preventDefault();
    document.querySelector('#demo-result').textContent = describeResult([...new FormData(event
      .currentTarget)]);
  });
</script>

API reference

#

Hover over a name or focus it with the keyboard to see the code. On touch screens, tap the name.

<sy-tree-select>

Properties

Attribute / PropertyTypeDefaultDescription
TreeNode[][]Array of tree nodes with label, value, and children.

Pass objects and arrays as JavaScript properties.

booleanfalseSelect multiple items using checkboxes.
booleanfalseShow a button to clear the current value.
string''Initially selected value.
booleanfalseBlock user interaction and keyboard focus.
'error' | 'default''default'Visually represent default, success, warning, or error states.
booleanfalseAllow tree branches to be expanded and collapsed.
booleanfalseExpand all tree branches initially.
booleanfalseShow connectors between parent and child nodes.
booleanfalseIndicate that processing is in progress.
number0Number of selected tags shown at once. Set to 0 to show all.
number | nullnullDisplay width of tree nodes.
string''Placeholder shown when the value is empty.
booleanfalseControl how parent nodes are included in the selection result.
booleanfalseDisplay the current value while preventing edits.
booleanfalseFail form validation when no value is provided.
string''Field name used in FormData.
booleanfalseUse the app's error UI instead of the browser's built-in error display. checkValidity still performs validation.

Events

EventDetailDescription
CustomEvent<{ selectedItem: { value: string; label: string }[]; isValid: boolean }>Emitted when the value or selection state changes.

Methods

MethodReturnsDescription
Promise<void>Set custom validity with the specified message. An empty string clears the error.
Promise<boolean>Return true if the current constraints are satisfied, otherwise false.
Promise<boolean>Validate the current value and display errors to the user.
Promise<void>Set a custom error state.
Promise<void>Clear the custom error state.
Promise<"" | "custom" | "valueMissing">Return the current validation state.

Slots

SlotDescription
Validation error message.